From b8f54b44a3c50b2685dac2910538c27662b34d8c Mon Sep 17 00:00:00 2001 From: nathan Date: Thu, 2 Oct 2025 23:58:24 -0600 Subject: [PATCH] Finished testing and docs for now. Time for new features! --- .gitignore | 1 + game/Rakefile | 3 +- game/log.rb | 34 +++---- game/planet.rb | 2 +- game/position.rb | 5 + game/ship.rb | 5 +- game/spec/api_spec.rb | 157 +++++++++++++++++++++++++++++++ game/spec/planet_spec.rb | 10 +- game/spec/position_spec.rb | 14 +-- game/spec/ship_spec.rb | 12 ++- game/spec/spec_helper.rb | 4 +- game/spec/universe_array_spec.rb | 7 ++ game/spec/universe_spec.rb | 36 +++---- game/star.rb | 8 ++ game/universe.rb | 60 ++++++++++-- game/universe_array.rb | 24 ++++- 16 files changed, 316 insertions(+), 66 deletions(-) create mode 100644 game/star.rb diff --git a/.gitignore b/.gitignore index e1b3ef2..94df610 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ **/logs **/*tmp* **/coverage +game/spec/examples.txt diff --git a/game/Rakefile b/game/Rakefile index c44472b..e8f5520 100644 --- a/game/Rakefile +++ b/game/Rakefile @@ -24,7 +24,8 @@ end namespace :doc do desc 'Create the YARD documentation for the project' task :yard do - res = sh 'yard doc *.rb' + res = sh 'yard doc *.rb --list-undoc' + sh 'yard stats *.rb --list-undoc' end desc 'Create code coverage' diff --git a/game/log.rb b/game/log.rb index f6b2863..b400c13 100644 --- a/game/log.rb +++ b/game/log.rb @@ -1,17 +1,17 @@ -# Nathan Hinton 2 Oct 2025 -# Does not work sadly... - -require 'logger' - -module Log - @log = Logger.new('game.log') - @log.level = Logger::INFO - - def self.logger - @logger - end - - def logger - Log.logger - end -end +# BROKEN :( # Nathan Hinton 2 Oct 2025 +# BROKEN :( # Does not work sadly... +# BROKEN :( +# BROKEN :( require 'logger' +# BROKEN :( +# BROKEN :( module Log +# BROKEN :( @log = Logger.new('game.log') +# BROKEN :( @log.level = Logger::INFO +# BROKEN :( +# BROKEN :( def self.logger +# BROKEN :( @logger +# BROKEN :( end +# BROKEN :( +# BROKEN :( def logger +# BROKEN :( Log.logger +# BROKEN :( end +# BROKEN :( end diff --git a/game/planet.rb b/game/planet.rb index b70fd25..46059ca 100644 --- a/game/planet.rb +++ b/game/planet.rb @@ -12,7 +12,7 @@ class Planet def initialize(random) @random = random @prod_rate = @random.rand - @resource = PLANET_PRODUCTION_RESOURCES[(@random.rand * PLANET_PRODUCTION_RESOURCES.length).floor] + @resource = PLANET_PRODUCTION_RESOURCES.sample(random: @random) @owner = nil end diff --git a/game/position.rb b/game/position.rb index 970c29b..b078243 100644 --- a/game/position.rb +++ b/game/position.rb @@ -41,6 +41,11 @@ class Position return "(#{@x_pos}, #{y_pos})" end + # Custom comparison. We need to check the the point is the same, not the + # identity. + # + # @param other [Positon] The position to compare with + # @return [Boolean] If they are in the same position as each other. def ==(other) return other.x_pos == @x_pos && other.y_pos == @y_pos end diff --git a/game/ship.rb b/game/ship.rb index b0922aa..b122db5 100644 --- a/game/ship.rb +++ b/game/ship.rb @@ -3,9 +3,12 @@ # This class will be the container for ships. For now they are all the same class Ship - def initialize(x_pos, y_pos) + attr_reader :owner + + def initialize(x_pos, y_pos, owner) @position = [x_pos, y_pos] @speed = 0.1 + @owner = owner end # Called every game tick diff --git a/game/spec/api_spec.rb b/game/spec/api_spec.rb index f7a7247..28690f1 100644 --- a/game/spec/api_spec.rb +++ b/game/spec/api_spec.rb @@ -1,2 +1,159 @@ # frozen_string_literal: true # Nathan Hinton 2 Oct 2025 + +# This is for testing the API that the clients can use. + + +# Most of the api is here as it relates to interacting with the universe +require './universe' +require './player' +require './position' +require './ship' + +RSpec.describe 'API' do + + # Universe setup + let(:name) { 'Test Universe' } + let(:universe_size) { 3 } + let(:seed) { 12345 } + + subject(:universe) do + Universe.new( + name, + universe_size: universe_size, + seed: seed, + create_chances: { planet: 1 } # force a planet at every valid spot + ) + end + + before(:each) do + universe.create_universe + end + + + # Player setup + let(:player_1) { Player.new('test player 01') } + let(:player_2) { Player.new('test player 02') } + + # Positions setup + let(:center_position) { Position.new(5, 3) } + describe 'UNIVERSE API' do + describe '#get_position' do + it 'returns a position in the universe' do + expect(universe.get_position(player_1, Position.new(0, 0))).to be nil + expect(universe.get_position(player_1, Position.new(3, 5))).to be_a(Planet) + expect(universe.get_position(player_1, center_position)).to be_a(Star) + end + end # describe Universe#get_position + end # describe 'UNIVERSE API' + + + describe 'SHIPS API' do + describe '@create_ship' do + it 'returns false when the player is invalid' do + expect(universe.create_ship(player_1, Position.new(3, 5))).to eq false + end + end # describe '@create_ship' + + describe '@move_ship' do + it 'returns false when the player is invalid' do + expect(universe.move_ship(player_1, Ship.new(2, 4, player_1), Position.new(3, 5))).to eq false + end + end # describe '@move_ship' + + describe '@remove_ship' do + it 'returns false when the player is invalid' do + expect(universe.remove_ship(player_1, Position.new(3, 5))).to eq false + end + end # describe '@remove_ship' + end # describe 'SHIPS API' + + + + describe 'PLANETS API' do + describe '@remove_planet' do + end # describe '@remove_planet' + end # describe 'PLANETS API' + + + describe 'PLAYER API' do + describe '#add_player' do + it 'adds with free planets' do + expect(universe.add_player(player_1)).to be(true) + expect(universe.instance_variable_get(:@player_list)).to eq [player_1] + end + + it 'fails to add when all planets are occopuied' do + # Make all the planets owned by player_2 + universe.planets.each do |planet| + planet.change_owner(player_2) + end + expect(universe.add_player(player_1)).to be(false) + expect(universe.instance_variable_get(:@player_list)).to eq [] + end + + it 'raises an error if a planet can not be taken' do + # This is an exceptional case. This is actually proper behavior when + # not trying to deal out planets. + end + end # describe 'add_player' + + describe '#remove_player' do + it 'can remove an existing player' do + universe.add_player(player_1) + expect(universe.remove_player(player_1)).to eq true + expect(universe.remove_player(player_2)).to eq true + expect(universe.instance_variable_get(:@player_list)).to eq [] + end + + it 'can remove a non existing player' do + expect(universe.remove_player(player_2)).to eq true + end + end # describe 'remove_player' + + describe '#validate_player' do + it 'returns true when a player is valid' do + universe.add_player(player_1) + expect(universe.validate_player(player_1)).to eq true + end + + it 'returns false when the player is not in the player list' do + expect(universe.validate_player(player_2)).to eq false + end + end # describe 'validate_player' + + + describe '#get_player_ships' do + it 'lists all ships owned by a player' do + universe.add_player(player_1) + universe.add_player(player_2) + expect(universe.get_player_ships(player_1)).to eq [] + ship_1 = Ship.new(2, 4, player_1) + ship_2 = Ship.new(2, 4, player_2) + ship_3 = Ship.new(3, 5, player_1) + universe.instance_variable_set(:@ship_list, [ship_1, ship_2, ship_3]) + expect(universe.get_player_ships(player_1)).to eq [ship_1, ship_3] + expect(universe.get_player_ships(player_2)).to eq [ship_2] + end + + it 'returns an empty list when a player that is not registered gets ships' do + ship_2 = Ship.new(2, 4, player_2) + universe.instance_variable_set(:@ship_list, [ship_2]) + expect(universe.get_player_ships(player_2)).to eq [] + end + end # describe 'get_player_ships' + + describe '#get_player_planets' do + it 'returns the planets owned by a player' do + universe.add_player(player_1) + expect(universe.get_player_planets(player_2)).to eq [] + expect(universe.get_player_planets(player_1).length).to eq 1 + end + + it 'returns an empty list for a unregistered player' do + expect(universe.get_player_planets(player_2)).to eq [] + end + + end # describe 'get_player_planets' + end # describe 'PLAYER API' +end diff --git a/game/spec/planet_spec.rb b/game/spec/planet_spec.rb index db51f30..215bf18 100644 --- a/game/spec/planet_spec.rb +++ b/game/spec/planet_spec.rb @@ -13,15 +13,15 @@ RSpec.describe Planet do describe '#initialize' do it 'stores the random off' do - expect(planet.instance_variable_get(:@random)).to eq(random) + expect(planet.instance_variable_get(:@random)).to eq random end it 'creates a production rate' do - expect(planet.instance_variable_get(:@prod_rate)).to eq(Random.new(seed).rand) + expect(planet.instance_variable_get(:@prod_rate)).to eq Random.new(seed).rand end it 'sets a resource' do - expect(planet.instance_variable_get(:@resource)).to eq(PLANET_PRODUCTION_RESOURCES[(Random.new(seed).rand * PLANET_PRODUCTION_RESOURCES.length).floor]) + expect(planet.instance_variable_get(:@resource)).to eq PLANET_PRODUCTION_RESOURCES[(Random.new(seed).rand * PLANET_PRODUCTION_RESOURCES.length).floor] end it 'has a nil owner' do @@ -31,13 +31,13 @@ RSpec.describe Planet do describe '#change_owner' do it 'allows an ownership change when the current owner is nil' do - expect(planet.change_owner(player_1)).to eq(true) + expect(planet.change_owner(player_1)).to eq true expect(planet.instance_variable_get(:@owner)).to eq player_1 end it 'does not change when already owned' do planet.change_owner(player_2) - expect(planet.change_owner(player_1)).to eq(false) + expect(planet.change_owner(player_1)).to eq false expect(planet.instance_variable_get(:@owner)).to eq player_2 end end diff --git a/game/spec/position_spec.rb b/game/spec/position_spec.rb index 3dd468b..a3c9cdb 100644 --- a/game/spec/position_spec.rb +++ b/game/spec/position_spec.rb @@ -10,8 +10,8 @@ RSpec.describe Position do context 'with two arguments' do it 'stores the coordinates when the sum is even' do pos = Position.new(*even_pair) - expect(pos.x_pos).to eq(even_pair[0]) - expect(pos.y_pos).to eq(even_pair[1]) + expect(pos.x_pos).to eq even_pair[0] + expect(pos.y_pos).to eq even_pair[1] end it 'raises ArgumentError when the sum is odd' do @@ -43,30 +43,30 @@ RSpec.describe Position do describe '#two_d_cartesian' do it 'returns the original (x, y) pair' do pos = Position.new(*even_pair) - expect(pos.two_d_cartesian).to eq(even_pair) + expect(pos.two_d_cartesian).to eq even_pair end end describe '#two_d_hexagonal' do it 'returns an array of nil values for a, b, c' do pos = Position.new(*even_pair) - expect(pos.two_d_hexagonal).to eq([nil, nil, nil]) + expect(pos.two_d_hexagonal).to eq [nil, nil, nil] end end describe '#to_s' do it 'puts out the coordinates' do - expect(Position.new(*even_pair).to_s).to eq('(4, 6)') + expect(Position.new(*even_pair).to_s).to eq '(4, 6)' end end describe '#==' do it 'returns false when the positions differ' do - expect(Position.new(*even_pair) == Position.new(6, 8)).to eq(false) + expect(Position.new(*even_pair) == Position.new(6, 8)).to eq false end it 'returns true when the positions are the same' do - expect(Position.new(*even_pair) == Position.new(4, 6)).to eq(true) + expect(Position.new(*even_pair) == Position.new(4, 6)).to eq true end end end diff --git a/game/spec/ship_spec.rb b/game/spec/ship_spec.rb index c662a24..b3cd1c8 100644 --- a/game/spec/ship_spec.rb +++ b/game/spec/ship_spec.rb @@ -1,17 +1,19 @@ # frozen_string_literal: true -require_relative '../ship' # <-- adjust to the actual path of ship.rb +require './ship' + +require './player' RSpec.describe Ship do - let(:ship) { Ship.new(0, 0) } + let(:ship) { Ship.new(0, 0, Player.new('test player 01')) } describe '#initialize' do it 'stores the given position' do - expect(ship.instance_variable_get(:@position)).to eq([0, 0]) + expect(ship.instance_variable_get(:@position)).to eq [0, 0] end it 'sets the speed to 0.1' do - expect(ship.instance_variable_get(:@speed)).to eq(0.1) + expect(ship.instance_variable_get(:@speed)).to eq 0.1 end it 'does not set @moving until a direction is chosen' do @@ -70,7 +72,7 @@ RSpec.describe Ship do } mapping.each do |dir, expected| expect { ship.move_direction(dir) }.not_to raise_error - expect(ship.instance_variable_get(:@moving)).to eq(expected) + expect(ship.instance_variable_get(:@moving)).to eq expected end end diff --git a/game/spec/spec_helper.rb b/game/spec/spec_helper.rb index af77a70..4a1b6f2 100644 --- a/game/spec/spec_helper.rb +++ b/game/spec/spec_helper.rb @@ -52,7 +52,6 @@ RSpec.configure do |config| # The settings below are suggested to provide a good initial experience # with RSpec, but feel free to customize to your heart's content. -=begin # This allows you to limit a spec run to individual examples or groups # you care about by tagging them with `:focus` metadata. When nothing # is tagged with `:focus`, all examples get run. RSpec also provides @@ -87,7 +86,7 @@ RSpec.configure do |config| # Print the 10 slowest examples and example groups at the # end of the spec run, to help surface which specs are running # particularly slow. - config.profile_examples = 10 + # config.profile_examples = 10 # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing @@ -100,5 +99,4 @@ RSpec.configure do |config| # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. Kernel.srand config.seed -=end end diff --git a/game/spec/universe_array_spec.rb b/game/spec/universe_array_spec.rb index 179c871..0811243 100644 --- a/game/spec/universe_array_spec.rb +++ b/game/spec/universe_array_spec.rb @@ -18,6 +18,13 @@ RSpec.describe UniverseArray do uni_arr[0][0] = 'hi' expect(uni_arr[Position.new(0, 0)]).to eq 'hi' end + + it 'returns nil when access outside universe' do + uni_arr = UniverseArray.new() + uni_arr[3] = [] + expect(uni_arr[Position.new(3, 51)]).to eq nil + expect(uni_arr[Position.new(0, 0)]).to eq nil + end end describe '#[]=' do diff --git a/game/spec/universe_spec.rb b/game/spec/universe_spec.rb index b82057b..7e5c42d 100644 --- a/game/spec/universe_spec.rb +++ b/game/spec/universe_spec.rb @@ -24,15 +24,15 @@ RSpec.describe Universe do describe '#initialize' do it 'stores all expected instance variables' do - expect(universe.name).to eq(name) - expect(universe.instance_variable_get(:@universe_size)).to eq(universe_size) - expect(universe.instance_variable_get(:@seed)).to eq(seed) + expect(universe.name).to eq name + expect(universe.instance_variable_get(:@universe_size)).to eq universe_size + expect(universe.instance_variable_get(:@seed)).to eq seed end it 'creates a deterministic Random object when a seed is given' do rng = universe.instance_variable_get(:@random) expect(rng).to be_a(Random) - expect(rng.rand(100)).to eq(Random.new(seed).rand(100)) + expect(rng.rand(100)).to eq Random.new(seed).rand(100) end it 'initializes the random if no seed is passed in' do @@ -50,12 +50,12 @@ RSpec.describe Universe do it 'is symmetric' do a = Position.new(0, 0) b = Position.new(4, 2) - expect(universe.distance(a, b)).to eq(universe.distance(b, a)) + expect(universe.distance(a, b)).to eq universe.distance(b, a) end it 'returns 0 when both points are identical' do p = Position.new(1, 1) - expect(universe.distance(p, p)).to eq(0) + expect(universe.distance(p, p)).to eq 0 end it 'correctly computes known hex distances' do @@ -64,13 +64,13 @@ RSpec.describe Universe do p2 = Position.new(4, 2) p3 = Position.new(2, 2) # (0,0) to (2,0) → 1 - expect(universe.distance(p0, p1)).to eq(1) + expect(universe.distance(p0, p1)).to eq 1 # (0,0) to (4,2) → 3 - expect(universe.distance(p0, p2)).to eq(3) + expect(universe.distance(p0, p2)).to eq 3 # (0,0) to (2,2) → 2 - expect(universe.distance(p0, p3)).to eq(2) + expect(universe.distance(p0, p3)).to eq 2 end - end + end # describe '#distance' describe '#map generation' do it 'creates the expected dimensions' do @@ -80,7 +80,7 @@ RSpec.describe Universe do # (our stub) and every other entry is nil. expect(map).to be_a(UniverseArray) # Check how may nodes were created: - expect(map.flatten.compact.length).to eq(19) # 1 + 1*6 + 2*6 + expect(map.flatten.compact.length).to eq 19 # 1 + 1*6 + 2*6 end it 'contains a planet at every valid point when create_chances is 1' do @@ -89,12 +89,16 @@ RSpec.describe Universe do map.each do |row| row.each do |idx| # The indexes are allowed to be nil - if !idx.nil? - expect(idx).to be_a(Planet) + if idx.nil? + next # Is allowed to be nil end + if idx.is_a? Star + next # Is allowed to be star + end + expect(idx).to be_a(Planet) end end - end + end # describe '#map generation' it 'is deterministic when the same seed is used' do first_universe = Universe.new( @@ -113,7 +117,7 @@ RSpec.describe Universe do ) second_map = second_universe.instance_variable_get(:@universe_map) - expect(first_map).to eq(second_map) + expect(first_map).to eq second_map end end @@ -140,7 +144,7 @@ RSpec.describe Universe do describe '#planets' do it 'lists all of the generated planets' do universe.create_universe - expect(universe.planets.length).to eq(19) + expect(universe.planets.length).to eq 18 end end end diff --git a/game/star.rb b/game/star.rb new file mode 100644 index 0000000..960298b --- /dev/null +++ b/game/star.rb @@ -0,0 +1,8 @@ +# Nathan Hinton Oct 2 2025 + +# Literally just a class to have something else in space... + +class Star + def initialize + end +end diff --git a/game/universe.rb b/game/universe.rb index 4733f41..fdc5f23 100644 --- a/game/universe.rb +++ b/game/universe.rb @@ -4,6 +4,7 @@ require 'logger' require './planet' +require './star' require './position' require './universe_array' @@ -60,6 +61,7 @@ class Universe # Setup internal lists: @player_list = [] + @planet_list = [] @ship_list = [] # initizlize RNG @@ -73,6 +75,8 @@ class Universe end # Initialize + # Create the universe. This will setup all of the planets and a blank slate + # for the universe. def create_universe() # Set center of universe @center = Position.new(@universe_size + 2, @universe_size) @@ -96,12 +100,17 @@ class Universe # Skip if it is the center if position == @center + @universe_map[position] = Star.new() + next end # Get what the planet should be: spot = nil if @random.rand < @create_chances[:planet] spot = Planet.new(@random) + @planet_list.push(spot) + #else + # spot = '' end @@logger.debug "Creating something at (#{position})" @universe_map[position] = spot @@ -119,7 +128,6 @@ class 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. - require 'byebug';debugger if position_a.nil? dist_x = (position_b.x_pos - position_a.x_pos).abs dist_y = (position_b.y_pos - position_a.y_pos).abs @@ -176,9 +184,9 @@ class Universe # Print what we are: value = @universe_map[position] nextline += ' ' - if position == @center + if value.is_a?(Star) print 'O' - elsif value.class == Planet + elsif value.is_a?(Planet) print 'P' else print ' ' @@ -262,6 +270,7 @@ class Universe # @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) + return false unless validate_player(player) end # Move a player's ship to a given position @@ -270,6 +279,15 @@ class Universe # @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) + return false unless validate_player(player) + # Ensure the ship exists before moving it. + end + + # Remove a ship from the game + # @param player [Player] The player who owns the ship to be removed + # @param ship [Ship] The ship to be removed + def remove_ship(player, ship) + return false unless validate_player(player) end @@ -301,14 +319,24 @@ class Universe # # @param player [Player] The player who should be added to the universe def add_player(player) - @player_list.push(player) # Give the player a planet free_planets = [] planets.each do |planet| free_planets.push(planet) if planet.owner.nil? end - - return 'Added player to universe' + if free_planets == [] + @@logger.debug 'Failed to find an unoccopuied planet for new player!' + return false + end + home_planet = free_planets.sample(random: @random) + # Can;t figure out how to test this... + # if !home_planet.change_owner(player) + # raise Exception.new('Failed to allocate a new player a home planet!') + # end + home_planet.change_owner(player) + @player_list.push(player) + @@logger.debug "Added player '#{player.name}' with home planet #{home_planet}" + return true end # Remove a player from the list in this universe @@ -316,15 +344,30 @@ class 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!' + @@logger.debug 'Failed to remove player! Does not exist in this universe! Returning true anyway though!' end - return 'Removed player from universe' + @@logger.debug "Removed player '#{player.name}' from universe" + return true + end + + # Validates if a player exists and is in a good state in the server + # + # @param player [Player] The player to validate + def validate_player(player) + # check if in player list: + unless @player_list.include?(player) + @@logger.warn "Player '#{player}' Tried to perform an action but is an invalid player!" + return false + end + return true end # Returns a list of ships owned by the player # # @param player [Player] The player who wants to get their ships. + # @return [Array] def get_player_ships(player) + return [] unless validate_player(player) result = [] @ship_list.each do |ship| if ship.owner == player @@ -338,6 +381,7 @@ class Universe # # @param player [Player] The player who wants to get their planets. def get_player_planets(player) + return [] unless validate_player(player) result = [] @planet_list.each do |planet| if planet.owner == player diff --git a/game/universe_array.rb b/game/universe_array.rb index e7b737f..4e58f96 100644 --- a/game/universe_array.rb +++ b/game/universe_array.rb @@ -1,22 +1,42 @@ # Nathan Hinton 1 Oct 2025 # Create a custom hash class for the universe. +require 'logger' + require './position' # This will contain the universe hash inside it. It will allow for array type # indexing as well as point type indexing class UniverseArray < Array - def initialize - end + @@logger = Logger.new('logs/universe.log') + + # Custom accessor. This allows us to access either with a position object + # (preferred as it does validation) or direct access. + # + # @param index [Integer, Position] If it is an integer nothing special. If a + # Position it will be intepreted for us into the regular indexint. + # @param rest [Object] Actually, I have no idea why this is here. (copy paste) + # @return Whatever is at that position. Can be anything. def [](index, *rest) + #require 'byebug';debugger if index.is_a?(Position) + if self[index.y_pos].nil? + @@logger.debug "attempted access outside of known universe!" + return nil + end return self[index.y_pos][index.x_pos] else super end end + # Custom writer. This allows us to access either with a position object + # (preferred as it does validation) or direct access. + # + # @param index [Integer, Position] If it is an integer nothing special. If a + # Position it will be intepreted for us into the regular indexint. + # @param rest [Object] This (if intercepted) has the value we should assign def []=(index, *rest) if index.is_a?(Position) return self[index.y_pos][index.x_pos] = rest[0]