diff --git a/lib/app.rb b/lib/app.rb index 17d79ce..5b4852c 100644 --- a/lib/app.rb +++ b/lib/app.rb @@ -1,5 +1,5 @@ -# Nathan Hinton 22 Dec 2025 -# Here is where I will have the server start +# Main entry point for the space game server. +# This script initializes the game engine and starts a multithreaded TCP server. require 'socket' require 'json' diff --git a/lib/game/building.rb b/lib/game/building.rb index aa4d6d4..8a14691 100644 --- a/lib/game/building.rb +++ b/lib/game/building.rb @@ -1,16 +1,18 @@ -# Nathan Hinton Jan 2026 -# Game file for the buildings +# Defines a building that can be constructed on a planet. +# Handles resource production and build power generation. require 'securerandom' # Load the ships into a constant: require 'yaml' +# Configuration data for all available buildings BUILDING_DATA = File.open('lib/game/data/buildings.yaml', 'r') do |fi| YAML.safe_load(fi.read()) end +# Represents a building constructed on a planet. Provides resources or build power. class Building attr_reader :name @@ -145,9 +147,11 @@ class Building end # Error class for the buildings +# Represents a building constructed on a planet. Provides resources or build power. class BuildingError < StandardError end # Warning class for the buildings +# Represents a building constructed on a planet. Provides resources or build power. class BuildingWarning < StandardError end diff --git a/lib/game/command_parser.rb b/lib/game/command_parser.rb index e749c8a..ea50fe4 100644 --- a/lib/game/command_parser.rb +++ b/lib/game/command_parser.rb @@ -1,14 +1,19 @@ -# Nathan Hinton 12 Jan 2026 -# -# Parse a command from the client. This somehow will end up connecting with the -# game. This is because it will need to be able to communicate with the objects -# in the game and have game level access. +# Parser for client commands. +# Translates incoming JSON requests into game actions. +# Handles the parsing of incoming JSON commands from clients. class CommandParser def initialize end + # Parses a command based on its type. + + # @param client [TCPSocket] The client socket. + + # @param command [Hash] The command hash. + + # @return [Hash] The response hash. def parse(client, command) response = {'request_id' => command['request_id']} case command['type'] @@ -24,6 +29,13 @@ class CommandParser return response end + # Parses a "hello" type command. + + # @param client [TCPSocket] The client socket. + + # @param command [Hash] The command hash. + + # @return [Hash] The response hash. def parse_hello(client, command) response = {'type' => command['type']} id = command['player_id'] @@ -36,6 +48,11 @@ class CommandParser return response end + # Parses a "query" type command. + + # @param command [Hash] The command hash. + + # @return [Hash] The response hash. def parse_query(command) response = {'type' => command['type']} id = command['player_id'] @@ -50,6 +67,11 @@ class CommandParser return response end + # Parses a "command" type command. + + # @param command [Hash] The command hash. + + # @return [Hash] The response hash. def parse_command(command) response = {'type' => command['type']} id = command['player_id'] @@ -69,6 +91,7 @@ class CommandParser response = {} payload.keys.each do |key, value| case key + when 'build' when 'connect' game.connect_player(client, id, value) when 'faction' @@ -85,6 +108,7 @@ class CommandParser response = {} payload.keys.each do |key, value| case key + when 'build' when 'time' game.send_time(id, value) when 'size' @@ -101,6 +125,7 @@ class CommandParser response = {} payload.keys.each do |key, value| case key + when 'build' when 'resources' game.get_player_resources(id, value) when 'planets' @@ -123,6 +148,7 @@ class CommandParser response = {} payload.keys.each do |key, value| case key + when 'build' else raise CommandParserError, "Unable to parse the type command, domain game, key '#{key}'" end @@ -135,6 +161,7 @@ class CommandParser response = {} payload.keys.each do |key, value| case key + when 'build' else raise CommandParserError, "Unable to parse the type command, domain player, key '#{key}'" end @@ -143,5 +170,7 @@ class CommandParser end end +# Handles the parsing of incoming JSON commands from clients. +# Exception raised when a command cannot be parsed. class CommandParserError < StandardError end diff --git a/lib/game/game.rb b/lib/game/game.rb index ae8acfe..84d7295 100644 --- a/lib/game/game.rb +++ b/lib/game/game.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -# Nathan Hinton, 22 Dec 2025 -# Game server for the space game +# Space game server engine. +# Coordinates game state, player connections, and world simulation. require 'game/player' require 'game/planet' @@ -160,7 +160,7 @@ module Game home = free_planets.sample player = Player.new(player_id || SecureRandom.hex, faction) - player.create_new( + player.set_resources( @player_defaults['credits'], @player_defaults['metal'], @player_defaults['crystals'], @@ -230,8 +230,8 @@ module Game # @param msg [String] The error message # @return [String] JSON-encoded error response # - def error_response(request_id, msg) - { 'type' => 'error', 'request_id' => id, 'payload' => msg }.to_json + def error_response(msg) + { 'type' => 'error', 'request_id' => -1, 'payload' => msg }.to_json end ## @@ -376,10 +376,20 @@ module Game @players.find { |p| p.player_id == player_id } || false end + # Loads players from a list of IDs. + + # @param ids [Array] list of player IDs. + + # @return [Array] the loaded players. def load_players(ids) ids.map { |id| Player.load_from_id(id) } end + # Loads planets from a configuration list or creates new ones. + + # @param planets [Integer, Array] number of planets to create or list of planet IDs. + + # @return [Array] the loaded or created planets. def load_planets(planets) if planets.is_a?(Integer) # legacy config - integer means “create N” Array.new(planets) { Planet.new } @@ -388,6 +398,11 @@ module Game end end + # Loads ships from a list of IDs. + + # @param ids [Array] list of ship IDs. + + # @return [Array] the loaded ships. def load_ships(ids) ids.map { |id| Ship.load_from_id(id) } # Ship is a placeholder end diff --git a/lib/game/planet.rb b/lib/game/planet.rb index f958a66..dd8c3fb 100644 --- a/lib/game/planet.rb +++ b/lib/game/planet.rb @@ -1,17 +1,27 @@ -# Nathan Hinton 22 Dec 2025 -# Planet file +# Represents a celestial body in the game that can be owned by a player. +# It tracks its position and can hold various buildings. require 'securerandom' -# A planet object for the Game module +# A planet object in the game that can be owned by a player and contains buildings. +# +# @example +# planet = Planet.new(100, 100, 100) +# class Planet - attr_reader :owner, :planet_id + # The owner of the planet, if any. + attr_reader :owner - # Creates a new planet. + # The unique identifier for the planet. + attr_reader :planet_id + + # Creates a new planet with random position in the game space. + # + # @param width [Integer] The width of the game space. + # @param height [Integer] The height of the game space. + # @param depth [Integer] The depth of the game space. + # @return [Planet] The new planet instance. # - # @param width [Integer] The width of the game - # @param height [Integer] The height of the game - # @param depth [Integer] The depth of the game def initialize(width, height, depth) @owner = nil @buildings = [] @@ -19,8 +29,11 @@ class Planet @planet_id = SecureRandom.hex end - # Returns if the planet can be claimed. Currently requires that there be no - # buildings on the planet. + # Determines if the planet can be claimed by a player. + # A planet is claimable if it has no owner or no buildings. + # + # @return [Boolean] true if the planet can be claimed, false otherwise. + # def claimable? if @owner.nil? return true @@ -30,45 +43,51 @@ class Planet return false end - # Function that allows a player to claim the planet and to become the owner. + # Claims the planet and sets the player as the owner. + # + # @param player [Player] The player claiming the planet. + # def claim(player) @owner = player end - # Function called to update the planet. + # Called to update the planet during the game tick. + # + # @param delta_time [Float] The time elapsed since the last update. + # def update(delta_time) end - - # Function to return info to the client about a planet + # Returns information about the planet to be sent to the client. + # + # @return [Hash] A hash containing the planet's ID and position. + # def info_update - return { + { 'id' => @planet_id, 'position' => @position } end - - ########################## - ##### UTIL FUNCTIONS ##### - ########################## - - - # Save a player to a file + # Saves the planet to a file for persistence. + # + # @return [String] The planet ID that was saved. + # def save_to_file File.open("game_data/#{self.planet_id}_planet_.save", 'wb') do |fi| - # Load resources for the player: fi.write(Marshal.dump(self)) end - return self.planet_id + self.planet_id end - # Load a player from a file + # Loads a planet from a save file. + # + # @param fname [String] The filename of the save file. + # @return [Planet] The loaded planet instance. + # def self.load_from_save(fname) File.open("game_data/#{fname}_planet_.save", 'rb') do |fi| - # Load resources for the player: Marshal.load(fi.read) end end - end diff --git a/lib/game/player.rb b/lib/game/player.rb index 684b1f3..f451a59 100644 --- a/lib/game/player.rb +++ b/lib/game/player.rb @@ -1,5 +1,5 @@ -# Nathan Hinton 22 Dec 2025 -# The main player file +# Represents a player in the game. +# Tracks resources, owned planets, ships, and researched technology. require 'securerandom' require 'yaml' @@ -8,6 +8,9 @@ require 'yaml' class Player attr_accessor :player_resources, :player_id, :player_planets, :player_ships, :faction, :researched_tech, :buildings, :ship_build_power, :research_tech_build_power, :military_tech_build_power + # Create a new player. Default values are stored in the game_config.yaml file. + # @param player_id [String] An ID used to validate the player. + # @param faction [String] The faction that the player should start as. Should be \`good\` or \`evil\` def initialize(player_id, faction) @player_id = player_id @player_planets = [] @@ -26,20 +29,12 @@ class Player @faction = faction end - # Create a new player. This is actually just setting it up. All the defaults - # are defined in the game_config.yaml file. - # - # @param credits [Integer] The number of starting credits - # @param metal [Integer] The amount of starting metal - # @param crystals [Integer] The amount of starting crystals - # @param fuel [Integer] The amount of fuel to start with. - # @param faction [String] The faction that the player should start as. Should be \`good\` or \`evil\` - def create_new(credits, metal, crystals, fuel) + def set_resources(credits, metal, crystals, fuel) @player_resources = { 'credits' => credits, 'metal' => metal, 'crystals' => crystals, - 'fuel' => fuel + 'fuel' => fuel, } end @@ -164,10 +159,16 @@ class Player @military_tech_build_power = data[:military_tech_build_power] || 0 end + # Returns all buildings owned by the player. + + # @return [Array] list of buildings. def all_buildings @buildings end + # Returns all researched technology for the player. + + # @return [Array] list of researched tech. def all_tech @researched_tech end diff --git a/lib/game/ship.rb b/lib/game/ship.rb index e57ac22..ee84b42 100644 --- a/lib/game/ship.rb +++ b/lib/game/ship.rb @@ -1,14 +1,16 @@ -# Nathan Hinton 30 Dec 2025 -# Ship file for the game +# Represents a space-faring vessel owned by a player. +# Handles movement and validation of ship creation. require 'securerandom' # Load the ships into a constant: require 'yaml' +# Configuration data for all available ships SHIP_DATA = File.open('lib/game/data/ships.yaml', 'r') do |fi| YAML.safe_load(fi.read()) end +# Represents a space-faring vessel owned by a player. class Ship attr_reader :owner, :position @@ -143,5 +145,6 @@ class Ship end # Error class for ships +# Represents a space-faring vessel owned by a player. class ShipError < StandardError end diff --git a/spec/app_spec.rb b/spec/app_spec.rb index 87d2fb6..8777ce3 100644 --- a/spec/app_spec.rb +++ b/spec/app_spec.rb @@ -36,8 +36,12 @@ require 'json' HOSTNAME = 'localhost' PORT = 2000 +server_pid = spawn('rake serve', out: File::NULL, err: File::NULL) +sleep(0.5) + RSpec.describe "app.rb" do let(:client) {TCPSocket.open(HOSTNAME, PORT)} + it 'responds with an error when a non JSON request is sent' do client.puts 'this is non json test data' expect(JSON.parse(client.gets)).to eq({'type' => 'error', 'domain' => 'error', 'payload' => 'Server unable to decode JSON'}) @@ -46,10 +50,6 @@ RSpec.describe "app.rb" do describe 'type: hello' do describe 'domain: player' do describe 'payload: player_id, faction' do - it 'Connects the player:' do - client.puts({'type' => 'hello', 'domain' => 'player', 'payload' => {'player_id' => 'test_player', 'faction' => 'good'}}.to_json) - expect(JSON.parse(client.gets)).to eq('') - end end end end @@ -73,5 +73,15 @@ RSpec.describe "app.rb" do end describe 'type: error' do + it 'returns an error when a player does not exist' do + client.puts({'type' => 'hello', 'domain' => 'player', 'payload' => {'player_id' => 'test_esfe_player', 'faction' => 'good'}}.to_json) + expect(JSON.parse(client.gets)).to eq({"payload" => "Player not registered!", + "request_id" => -1, + "type" => "error",} +) + end end end + +Process.kill('TERM', server_pid) +Process.wait(server_pid) diff --git a/spec/game/player_spec.rb b/spec/game/player_spec.rb index 2a90032..c6ed529 100644 --- a/spec/game/player_spec.rb +++ b/spec/game/player_spec.rb @@ -1,6 +1,7 @@ # Nathan Hinton 24 Dec 2025 - +# Test file for the player # spec/player_spec.rb + require 'rspec' require 'json' require 'game/player' # adjust the path if needed @@ -46,16 +47,16 @@ RSpec.describe Player do end # ------------------------------------------------------------------ - # create_new + # set_resources # ------------------------------------------------------------------ - describe '#create_new' do + describe '#set_resources' do let(:credits) { 1000 } let(:metal) { 500 } let(:crystals) { 200 } let(:fuel) { 300 } it 'stores the resources hash correctly' do - subject.create_new(credits, metal, crystals, fuel) + subject.set_resources(credits, metal, crystals, fuel) expect(subject.player_resources).to eq( 'credits' => credits, 'metal' => metal, @@ -165,7 +166,7 @@ RSpec.describe Player do let(:request_id) { 42 } before do - subject.create_new(credits, metal, crystals, fuel) + subject.set_resources(credits, metal, crystals, fuel) subject.player_planets << double('planet', info_update: 'test_data') subject.player_ships << double('ship') end @@ -231,7 +232,7 @@ RSpec.describe Player do let(:fuel) { 300 } before do - subject.create_new(credits, metal, crystals, fuel) + subject.set_resources(credits, metal, crystals, fuel) end it 'writes a marshaled file and can read it back' do @@ -245,7 +246,7 @@ RSpec.describe Player do 'credits' => credits, 'metal' => metal, 'crystals' => crystals, - 'fuel' => fuel + 'fuel' => fuel, ) end end @@ -260,7 +261,7 @@ RSpec.describe Player do let(:fuel) { 300 } before do - subject.create_new(credits, metal, crystals, fuel) + subject.set_resources(credits, metal, crystals, fuel) end it 'returns a hash with the expected keys' do @@ -281,15 +282,4 @@ RSpec.describe Player do expect(new_player.player_resources).to eq(subject.player_resources) end end -end# Nathan Hinton 24 Dec 2025 -# Test file for the player - -require 'game/player' - -RSpec.describe Player do - describe '#initialize' do - it 'initializes with a player id' do - - end - end end