From 72bd5fb9c037ef66760b1dd9c0718f717436b48a Mon Sep 17 00:00:00 2001 From: nathan Date: Thu, 2 Oct 2025 14:57:16 -0600 Subject: [PATCH] Working hard, decided that we need to commit to Git --- .gitignore | 3 + README.md | 98 ++++++++++++++ game/Rakefile | 27 ++++ game/game.rb | 8 ++ game/helpers.rb | 36 ++++++ game/log.rb | 1 + game/planet.rb | 15 +++ game/ship.rb | 60 +++++++++ game/spec/ship_spec.rb | 95 ++++++++++++++ game/universe.rb | 284 +++++++++++++++++++++++++++++++++++++++++ game/universe_hash.rb | 32 +++++ 11 files changed, 659 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 game/Rakefile create mode 100644 game/game.rb create mode 100644 game/helpers.rb create mode 100644 game/log.rb create mode 100644 game/planet.rb create mode 100644 game/ship.rb create mode 100644 game/spec/ship_spec.rb create mode 100644 game/universe.rb create mode 100644 game/universe_hash.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af6e1a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/doc +**/.byebug* +**/.yard* diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d54974 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# Goals: + + +Make a multiplayer game that will run in the browser. This game should be a +planet conquest type game where you can create a lobby and then players can +join. Once you start a game only those players can play. This game should be +time based. You will start with a system of planets and then over time you will +gain ships and resources. The goal is to either eliminate all of the other +players or to control a portion of the universe. As you explore you can +encounter other planets and players. Each planet will have specific +bonuses. Some can produce ships faster, others will produce more resources and +in others you can find bonuses for different kinds of ships. I want to make it +so that you can also upgrade the planets to improve the value of them. + +The maximum player count in a game will be 20 however I want this to be scalable. + +# Thoughts: + + +I do not want the players to directly communicate with each other. I want each +one to talk to the server and then the server to use an API for getting updates +for each player and their stuff. This means that potentially there could be +multiple clients for the game however I want to start with a web based client +as I feel that is easiest for me. + +Each player should be able to log in and then see what games they are playing +in and select a game to resume. The game will continue even when the player is +not logged in. This means that the player could be attacked even when they are +gone. This means that the worlds need to be persistent. + +As a follow up that means that each world will be always running. This means +that if there is a main loop for the game it should be very lightweight so that +it does not consume massive amounts of resources. + +I think that each universe should be randomly generated that way the players +have to explore it. + +I do eventually want to have AI players however for now I just want human +players. Make sure that you help me to keep this easy as I am programming. + +Here are the resources that I am thinking of for now: + - Food + - Metal + - Fuel/energy + +How do we have resources travel be twee planets? I am not sure yet. Maybe have +another type of ship called a transport that can transport resources. This +should be automated though so that needed resources are transported +auto-magically. + +I also want there to be some form of tech tree that the player can choose and +can research. Maybe add some rare resources. + +# Vocab: + + + - Ship: a space ship. There are several different types: + - Explorer: Has a high vision range and can see far. It can not however + attack or defend + - Fighter: This is a ship that can fight other ships. It will also have sub + classes as well + - Colonizer: This ship is used to colonize a planet and is defenseless while + flying. However when a planet is colonized the ship can be used to defend + the planet + - Universe: The entire game space. It contains planets and other objects + - Planet: A planet that can be colonized/owned by a player. It can produce + resources and ships + - Planet Defense: Structures that can be built on a planet that will defend it + from attack. + + +# Final thoughts: + + +I am comfortable with Python and Ruby mostly. I would rather separate the game +into the server stuff which will be API driven. I have done some Ruby on Rails +however I am not very good at it. This means that I would prefer if we used +rails for it to be purely an API. + +I also am very inexperienced at authentication. I think for now I will just +have player names. Anyone can use any name. This does mean however that if two +people use the same name they will be indistinguishable in the server. Probably +just taking a password and hashing it on the client and then using that hash as +a 'token' of sorts would fix this. I am not super concerned with player account +security however the server does need to be secure. + +# API overview: + + +The `Universe` class will contain everything in the universe. This means planets, ships, and other stelar objects. This will be the API that I can think of: + + - universe/ + - ships/ + - create/ + - update/ + - move/ + - planets/ + - update/ diff --git a/game/Rakefile b/game/Rakefile new file mode 100644 index 0000000..3ce6633 --- /dev/null +++ b/game/Rakefile @@ -0,0 +1,27 @@ +# Nathan Hinton 2 Oct 2025 +# Rake file + + +desc 'Run RSpec tests' +task :test do + begin + require 'rspec/core/rake_task' + RSpec::Core::RakeTask.new(:spec) + ENV['RUN_ALL_TESTS'] = 'true' + Rake::Task['spec'].invoke + rescue LoadError + puts 'Failed to load RSpec to run tests... Try installing rspec' + end +end + + +desc 'Run the server for clients to connect to' +task :serve do + raise NotImplementedError.new('Server task not yet implimented!') +end + + +desc 'Create the YARD documentation for the project' +task :doc do + res = sh 'yard doc *.rb' +end diff --git a/game/game.rb b/game/game.rb new file mode 100644 index 0000000..c527341 --- /dev/null +++ b/game/game.rb @@ -0,0 +1,8 @@ +# Nathan Hinton 1 Oct 2025 + + +# Mostly for testing right now: +require './universe' + +universe = Universe.new('test universe') +universe.print_universe(simple: true) diff --git a/game/helpers.rb b/game/helpers.rb new file mode 100644 index 0000000..8011230 --- /dev/null +++ b/game/helpers.rb @@ -0,0 +1,36 @@ +# Nathan Hinton 2 Oct 2025 + + +# A helper class for storing and converting universe point formats: +class Position + attr_reader :x_pos, :y_pos, :a, :b, :c + def initialize(*args) + if args.length == 2 + # initialize with x, y position: + if args.sum.odd? + raise ArgumentError.new('A position created in (x, y) must sum to an even number!') + end + x_pos = args[0] + y_pos = args[1] + elsif args.length == 3 + # initialize with a, b, c position + raise NotImplementedError.new('(a, b, c) positioning not implimented!') + else + raise ArgumentError.new('Creation of a position requires two or three integer args for a position.') + end + end + + # Returns the corrdinates in the 2d plane in a 3 axis notation. + # + # @return [Array] The (a, b, c) position + def two_d_hexagonal + return [a, b, c] + end + + # Returns the coordinates in the 2d plane + # + # @return [Array] The (x, y) position + def two_d_cartesian + return [x_pos, y_pos] + end +end diff --git a/game/log.rb b/game/log.rb new file mode 100644 index 0000000..ffb46dc --- /dev/null +++ b/game/log.rb @@ -0,0 +1 @@ +# Nathan Hinton 2 Oct 2025 diff --git a/game/planet.rb b/game/planet.rb new file mode 100644 index 0000000..1f72587 --- /dev/null +++ b/game/planet.rb @@ -0,0 +1,15 @@ +# Nathan Hinton 1 Oct 2025 + +PLANET_PRODUCTION_RESOURCES = [:ships] + +# This class holds information about a planet. +class Planet + attr_reader :owner + # Create a planet + def initialize(random) + @random = random + @prod_rate = @random.rand + @resource = PLANET_PRODUCTION_RESOURCES[(@random.rand * PLANET_PRODUCTION_RESOURCES.length).floor] + @owner = nil + end +end diff --git a/game/ship.rb b/game/ship.rb new file mode 100644 index 0000000..b0922aa --- /dev/null +++ b/game/ship.rb @@ -0,0 +1,60 @@ +# Nathan Hinton 2 Oct 2025 + + +# This class will be the container for ships. For now they are all the same +class Ship + def initialize(x_pos, y_pos) + @position = [x_pos, y_pos] + @speed = 0.1 + end + + # Called every game tick + def tick + if @moving != [0, 0] + + end + end + + # Move function. This will move a ship. For now you can only move one hop, + # there is no way to move more than one at the moment. This position should + # be only *one* spot away from where we are. + # + # @param x_pos [Integer] The target x position + # @param y_pos [Integer] The target y position + def move(x_pos, y_pos) + #Validate coordinates: + if (x_pos + y_pos).odd? + raise ArgumentError.new('The x, y coordinate for movement must add to be even!') + end + + # Ensure we are not too far: + if ((x_pos + y_pos) - @position.sum).abs != 2 + raise ArgumentError.new('The x, y coordinates for moevement must be to an adjcent space!') + end + + end # Function move + + # Move_direction. Takes a direction and then sets the moving variable equal that direction. + # + # @param direction [Integer] A value between (0, 5) that determines the direction (think unit circle) + def move_direction(direction) + if (direction < 0 || direction > 5) + raise ArgumentError.new('When moving using direction based movement, x must be between (0..5)!') + end + # This means that the direction can range from 0 to 5 (think of unit circle) + case direction + when 0 + @moving = [2, 0] # Right + when 1 + @moving = [1, 1] # Right up + when 2 + @moving = [-1, 1] # Left up + when 3 + @moving = [-2, 0] # Left + when 4 + @moving = [-1, -1] # Left down + when 5 + @moving = [1, -1] # Right down + end + end # Function move_direction +end diff --git a/game/spec/ship_spec.rb b/game/spec/ship_spec.rb new file mode 100644 index 0000000..b45bf1b --- /dev/null +++ b/game/spec/ship_spec.rb @@ -0,0 +1,95 @@ +# Nathan Hinton 2 Oct 2025 + +# Test file for the ship object + +require './ship' + +RSpec.describe Ship do + let(:initial_x) { 0 } + let(:initial_y) { 0 } + subject(:ship) { Ship.new(initial_x, initial_y) } + + describe '#initialize' do + it 'stores the given position' do + expect(ship.instance_variable_get(:@position)).to eq([initial_x, initial_y]) + end + + it 'sets the speed to 0.1' do + expect(ship.instance_variable_get(:@speed)).to eq(0.1) + end + + it 'does not set @moving until a direction is chosen' do + expect(ship.instance_variable_get(:@moving)).to be_nil + end + end + + describe '#tick' do + it 'does nothing (but does not raise an exception)' do + expect { ship.tick }.not_to raise_error + end + end + + describe '#move' do + context 'when coordinates are valid' do + it 'accepts adjacent moves whose sum is even' do + # adjacent positions that satisfy the rules + expect { ship.move(1, 1) }.not_to raise_error # sum = 2 + expect { ship.move(2, 0) }.not_to raise_error # sum = 2 + expect { ship.move(0, 2) }.not_to raise_error # sum = 2 + expect { ship.move(-1, -1) }.not_to raise_error # sum = -2 + end + end + + context 'when the x + y sum is odd' do + it 'raises an ArgumentError' do + expect { ship.move(0, 1) }.to raise_error( + ArgumentError, + /must add to be even/ + ) + end + end + + context 'when the target is not adjacent' do + it 'raises an ArgumentError' do + # distance > 1: difference of sums != 2 + expect { ship.move(3, 3) }.to raise_error( + ArgumentError, + /must be to an adjcent space/ + ) + expect { ship.move(0, 0) }.to raise_error( + ArgumentError, + /must be to an adjcent space/ + ) + end + end + end + + describe '#move_direction' do + it 'accepts direction values between 0 and 5' do + (0..5).each do |direction| + expect { ship.move_direction(direction) }.not_to raise_error + # verify the @moving vector matches the spec + expected = case direction + when 0 then [2, 0] + when 1 then [1, 1] + when 2 then [-1, 1] + when 3 then [-2, 0] + when 4 then [-1, -1] + when 5 then [1, -1] + end + expect(ship.instance_variable_get(:@moving)).to eq(expected) + end + end + + it 'raises an ArgumentError for invalid directions' do + expect { ship.move_direction(-1) }.to raise_error( + ArgumentError, + /must be between \(0..5\)/ + ) + expect { ship.move_direction(6) }.to raise_error( + ArgumentError, + /must be between \(0..5\)/ + ) + end + end +end diff --git a/game/universe.rb b/game/universe.rb new file mode 100644 index 0000000..fb22efe --- /dev/null +++ b/game/universe.rb @@ -0,0 +1,284 @@ +# Nathan Hinton 1 Oct 2025 + + +require './planet' + +# Universe class. Holds a universe and provides the api for it. Universes are +# created on a 2d grid. I would like it to be hexagonal movement type so that +# it feels less like a grid and more like a circle. This means that we will +# have some interesting point systems. For now I am thinking that we will have +# three directions, a, b, c which will corespond to moving along one of the +# lines to another system. Here is an ascii art: +# +# +# (0, 0, 1) (0, 1, 0) +# \ / +# \ / +# \ / +# (+c) (+b) +# \ / +# \ / +# \ / +# (-1, 0, 0) <--- (-a) --- (0, 0, 0) --- (+a) ---> (1, 0, 0) +# / \ +# / \ +# / +# (-b) (-c) +# / \ +# / \ +# / \ +# (0, -1, 0) (0, 0, -1) +# +# +# This means that we are adding vector together. I do not think that this will +# work super well though with a system that is other than 2D. I think that will +# be fine. +# +# After thinking some more I will have the universe class hold everything in +# the universe. It will be in charge of holding all of the ships and +# planets. Then the Player class will be connected to the ships and planets +# that they own/control +# +# @param name [String] The name of the game +# @param universe_size [Integer] Controlls the size of the generated universe. This is a radius from the center point. +class Universe + def initialize(name, universe_size: 4, create_chances: {planet: 1}, seed: nil, verbose: true, debug: false) + @name = name + @universe_size = universe_size + @create_chances = create_chances + @verbose = verbose + @debug = debug + + # Setup internal lists: + @player_list = [] + @ship_list = [] + + # initizlize RNG + if seed.nil? + @random = Random.new() + @seed = @random.seed + else + @seed = seed + @random = Random.new(@seed) + end + + # Set center of universe + @center = Position.new([@universe_size + 2, @universe_size ]) + + puts "Generating Universe '#{@name}' with seed #{@seed}" if @verbose + puts "Center is #{@center}" + @universe_map = [] + # Generate the universe. First generate the x values: + (0 .. (@universe_size * 2)).each do |y_pos| + @universe_map[y_pos] = [] + (0 .. (@universe_size * 2 + 2)).each do |x_pos| + # Make the grid work right, x is actually skipping every other. + x_pos *= 2 + x_pos += 1 if y_pos.odd? + position = Position.new([x_pos, y_pos]) + + # Skip if the node is too far + if distance(@center, position) >= @universe_size + next + end + + # Get what the planet should be: + spot = nil + if @random.rand < @create_chances[:planet] + spot = Planet.new(@random) + end + puts "Creating something at (#{position.x_pos} #{position.y_pos})" if @debug + @universe_map[position.y_pos][position.x_pos] = spot + end # Create y points + end # Create x points + end # Function initialize + + # Returns the distance between points. This is returned as a number which + # indicates how far you have to go to get to a point. This is actually + # supurisingly hard... + # + # @param position_a [Position] A start point in the universe + # @param position_b [Position] Another point in the universe + # @return [Integer] The distance between points in the universe. + def distance(position_a, position_b) + result = 0 # We can just add the abs diff of each direction. + dist_x = (position_b.x_pos - position_a.x_pos).abs + dist_y = (position_b.y_pos - position_a.y_pos).abs + + # Sanity check, our coordinates should add to be even: + if (dist_x + dist_y).odd? + raise RangeError.new('When calculating the distance we calculated a non even number. This is not allowed!') + end + + res = (dist_x / 2) + dist_y + if dist_x >= dist_y + res = (dist_x + dist_y) / 2 + end + puts "Distance: #{point_a} is #{point_b} #{res} units away" if @debug + return res + end # Function distance + + # Prints the universe to the screen. + def print_universe(simple: false) + (0 .. (@universe_size * 2)).each do |y_pos| + # Print empty space before chart (for angles) + nextline = '' + if y_pos < @universe_size + nextline += ' ' * ((@universe_size - y_pos).abs - 1) + else + nextline += ' ' * ((@universe_size - y_pos).abs) + end + + # Print more empty space for the odd places (provides offset) + if y_pos.odd? + print ' ' + end + + #Actually loop through everything + (0 .. (@universe_size * 2)).each do |x_pos| + # Make the grid work right, x is actually skipping every other. + x_pos *= 2 + x_pos += 1 if y_pos.odd? + + # Print empty space if we are outside the universe (For items) + if distance(@center, [x_pos, y_pos]) >= @universe_size + print ' ' + next + end + + # Print what we are: + position = @universe_map[y_pos][x_pos] + nextline += ' ' + if [x_pos, y_pos] == @center + print 'O' + elsif position.class == Planet + print 'P' + else + print ' ' + end + + # Calculate what we should print on the next line and if we should print dashes + if y_pos < @universe_size + nextline += '/ \\' + end + if distance(@center, [x_pos + 2, y_pos]) < @universe_size + print '---' + if y_pos >= @universe_size + nextline += '\\ /' + end + end + end # For each col in row + puts '' + if (y_pos + 1) / 2 < @universe_size && !simple + puts nextline + end + end # For each row + end # Function print_universe + + ################################################## + ################### UNIVERSE ##################### + ################################################## + + # This is where API stuff for the universe should go + + # Gets a position from the universe for a player. Eventually this will take + # into account that the player can not actually see the whole universe + # + # @param player [Player] The player who is asking for the position + # @param position [Position] The position that is being requested + def get_position(player, position) + + end + + # Sets a position as explored for a player. This should be called as ships + # move around and explore the universe. + # + # @param player [Player] The player who should be set + # @param position [Position] The position that is being modified + def explore_position(player, position) + end + + + ################################################## + ##################### SHIPS ###################### + ################################################## + + # Create a ship in the universe. This ship is registered to a specific player + # + # @param player [Player] The player who owns the ship + # @param position [Position] The position that the ship should be created at + def create_ship(player, position) + end + + # Move a player's ship to a given position + # + # @param player [Player] The player whose ship should be moved + # @param ship [Ship] The ship that should be moved + # @param position [Position] The position the ship should be moved to. + def move_ship(player, ship, position) + end + + + ################################################## + ##################### PLANETS #################### + ################################################## + + # As all planets are created when the galaxy is generated there are no create + # planet functions. It is possible that we want to remove them though + + # Remove a planet from the universe + # + # @param planet [Planet] The planet that should be removed + # @param position [Position] The position that it should be removed from + def remove_planet(planet, position) + end + + ################################################## + ##################### PLAYER ##################### + ################################################## + + # Add a player to the player list in the universe + # + # @param player [Player] The player who should be added to the universe + def add_player(player) + @player_list.push(player) + return 'Added player to universe' + end + + # Remove a player from the list in this universe + # + # @param player [Player] The player that should be removed from the player list. + def remove_player(player) + if @player_list.delete(player).nil? + return 'Failed to remove player! Does not exist in this universe!' + end + return 'Removed player from universe' + end + + # Returns a list of ships owned by the player + # + # @param player [Player] The player who wants to get their ships. + def get_player_ships(player) + result = [] + @ship_list.each do |ship| + if ship.owner == player + result.push(ship) + end + end + return result + end # Function get_player_ships + + # Returns a list of planets owned by the player + # + # @param player [Player] The player who wants to get their planets. + def get_player_planets(player) + result = [] + @planet_list.each do |planet| + if planet.owner == player + result.push(planet) + end + end + return result + end + +end # Class Universe diff --git a/game/universe_hash.rb b/game/universe_hash.rb new file mode 100644 index 0000000..03e8622 --- /dev/null +++ b/game/universe_hash.rb @@ -0,0 +1,32 @@ +# Nathan Hinton 1 Oct 2025 +# Create a custom hash class for the universe. + + +# This will contain the universe hash inside it. It will allow for array type +# indexing as well as point type indexing +class UniverseHash + def initialize() + @universe_hash = {} + end + + # Setter for values in the hash + def =[](key, value) + if key.class == Array + @universe_hash[key[0]] = {} if @universe_hash[key[0]].nil? + @universe_hash[key[0]][key[1]] = {} if @universe_hash[key[0]][key[1]].nil? + @universe_hash[key[0]][key[1]][key[2]] = value + else + raise NotImplementedError.new('You can only set based on point values.') + end + end + + def [](key) + if key.class == Array + return @universe_hash[key[0]][key[1]][key[2]] + elsif key.class == Integer + return @universe_hash[key] + else + raise NotImplementedError + end + end +end