Fixed tests. Need to check for damage from AI code

This commit is contained in:
2026-06-11 16:39:58 -06:00
parent df68358a2e
commit 6d2c2162f0
9 changed files with 149 additions and 78 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# Nathan Hinton 22 Dec 2025 # Main entry point for the space game server.
# Here is where I will have the server start # This script initializes the game engine and starts a multithreaded TCP server.
require 'socket' require 'socket'
require 'json' require 'json'
+6 -2
View File
@@ -1,16 +1,18 @@
# Nathan Hinton Jan 2026 # Defines a building that can be constructed on a planet.
# Game file for the buildings # Handles resource production and build power generation.
require 'securerandom' require 'securerandom'
# Load the ships into a constant: # Load the ships into a constant:
require 'yaml' require 'yaml'
# Configuration data for all available buildings
BUILDING_DATA = File.open('lib/game/data/buildings.yaml', 'r') do |fi| BUILDING_DATA = File.open('lib/game/data/buildings.yaml', 'r') do |fi|
YAML.safe_load(fi.read()) YAML.safe_load(fi.read())
end end
# Represents a building constructed on a planet. Provides resources or build power.
class Building class Building
attr_reader :name attr_reader :name
@@ -145,9 +147,11 @@ class Building
end end
# Error class for the buildings # Error class for the buildings
# Represents a building constructed on a planet. Provides resources or build power.
class BuildingError < StandardError class BuildingError < StandardError
end end
# Warning class for the buildings # Warning class for the buildings
# Represents a building constructed on a planet. Provides resources or build power.
class BuildingWarning < StandardError class BuildingWarning < StandardError
end end
+34 -5
View File
@@ -1,14 +1,19 @@
# Nathan Hinton 12 Jan 2026 # Parser for client commands.
# # Translates incoming JSON requests into game actions.
# 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.
# Handles the parsing of incoming JSON commands from clients.
class CommandParser class CommandParser
def initialize def initialize
end 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) def parse(client, command)
response = {'request_id' => command['request_id']} response = {'request_id' => command['request_id']}
case command['type'] case command['type']
@@ -24,6 +29,13 @@ class CommandParser
return response return response
end 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) def parse_hello(client, command)
response = {'type' => command['type']} response = {'type' => command['type']}
id = command['player_id'] id = command['player_id']
@@ -36,6 +48,11 @@ class CommandParser
return response return response
end end
# Parses a "query" type command.
# @param command [Hash] The command hash.
# @return [Hash] The response hash.
def parse_query(command) def parse_query(command)
response = {'type' => command['type']} response = {'type' => command['type']}
id = command['player_id'] id = command['player_id']
@@ -50,6 +67,11 @@ class CommandParser
return response return response
end end
# Parses a "command" type command.
# @param command [Hash] The command hash.
# @return [Hash] The response hash.
def parse_command(command) def parse_command(command)
response = {'type' => command['type']} response = {'type' => command['type']}
id = command['player_id'] id = command['player_id']
@@ -69,6 +91,7 @@ class CommandParser
response = {} response = {}
payload.keys.each do |key, value| payload.keys.each do |key, value|
case key case key
when 'build'
when 'connect' when 'connect'
game.connect_player(client, id, value) game.connect_player(client, id, value)
when 'faction' when 'faction'
@@ -85,6 +108,7 @@ class CommandParser
response = {} response = {}
payload.keys.each do |key, value| payload.keys.each do |key, value|
case key case key
when 'build'
when 'time' when 'time'
game.send_time(id, value) game.send_time(id, value)
when 'size' when 'size'
@@ -101,6 +125,7 @@ class CommandParser
response = {} response = {}
payload.keys.each do |key, value| payload.keys.each do |key, value|
case key case key
when 'build'
when 'resources' when 'resources'
game.get_player_resources(id, value) game.get_player_resources(id, value)
when 'planets' when 'planets'
@@ -123,6 +148,7 @@ class CommandParser
response = {} response = {}
payload.keys.each do |key, value| payload.keys.each do |key, value|
case key case key
when 'build'
else else
raise CommandParserError, "Unable to parse the type command, domain game, key '#{key}'" raise CommandParserError, "Unable to parse the type command, domain game, key '#{key}'"
end end
@@ -135,6 +161,7 @@ class CommandParser
response = {} response = {}
payload.keys.each do |key, value| payload.keys.each do |key, value|
case key case key
when 'build'
else else
raise CommandParserError, "Unable to parse the type command, domain player, key '#{key}'" raise CommandParserError, "Unable to parse the type command, domain player, key '#{key}'"
end end
@@ -143,5 +170,7 @@ class CommandParser
end end
end end
# Handles the parsing of incoming JSON commands from clients.
# Exception raised when a command cannot be parsed.
class CommandParserError < StandardError class CommandParserError < StandardError
end end
+20 -5
View File
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# Nathan Hinton, 22 Dec 2025 # Space game server engine.
# Game server for the space game # Coordinates game state, player connections, and world simulation.
require 'game/player' require 'game/player'
require 'game/planet' require 'game/planet'
@@ -160,7 +160,7 @@ module Game
home = free_planets.sample home = free_planets.sample
player = Player.new(player_id || SecureRandom.hex, faction) player = Player.new(player_id || SecureRandom.hex, faction)
player.create_new( player.set_resources(
@player_defaults['credits'], @player_defaults['credits'],
@player_defaults['metal'], @player_defaults['metal'],
@player_defaults['crystals'], @player_defaults['crystals'],
@@ -230,8 +230,8 @@ module Game
# @param msg [String] The error message # @param msg [String] The error message
# @return [String] JSON-encoded error response # @return [String] JSON-encoded error response
# #
def error_response(request_id, msg) def error_response(msg)
{ 'type' => 'error', 'request_id' => id, 'payload' => msg }.to_json { 'type' => 'error', 'request_id' => -1, 'payload' => msg }.to_json
end end
## ##
@@ -376,10 +376,20 @@ module Game
@players.find { |p| p.player_id == player_id } || false @players.find { |p| p.player_id == player_id } || false
end end
# Loads players from a list of IDs.
# @param ids [Array<String>] list of player IDs.
# @return [Array<Player>] the loaded players.
def load_players(ids) def load_players(ids)
ids.map { |id| Player.load_from_id(id) } ids.map { |id| Player.load_from_id(id) }
end end
# Loads planets from a configuration list or creates new ones.
# @param planets [Integer, Array<String>] number of planets to create or list of planet IDs.
# @return [Array<Planet>] the loaded or created planets.
def load_planets(planets) def load_planets(planets)
if planets.is_a?(Integer) # legacy config - integer means “create N” if planets.is_a?(Integer) # legacy config - integer means “create N”
Array.new(planets) { Planet.new } Array.new(planets) { Planet.new }
@@ -388,6 +398,11 @@ module Game
end end
end end
# Loads ships from a list of IDs.
# @param ids [Array<String>] list of ship IDs.
# @return [Array<Ship>] the loaded ships.
def load_ships(ids) def load_ships(ids)
ids.map { |id| Ship.load_from_id(id) } # Ship is a placeholder ids.map { |id| Ship.load_from_id(id) } # Ship is a placeholder
end end
+46 -27
View File
@@ -1,17 +1,27 @@
# Nathan Hinton 22 Dec 2025 # Represents a celestial body in the game that can be owned by a player.
# Planet file # It tracks its position and can hold various buildings.
require 'securerandom' 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 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) def initialize(width, height, depth)
@owner = nil @owner = nil
@buildings = [] @buildings = []
@@ -19,8 +29,11 @@ class Planet
@planet_id = SecureRandom.hex @planet_id = SecureRandom.hex
end end
# Returns if the planet can be claimed. Currently requires that there be no # Determines if the planet can be claimed by a player.
# buildings on the planet. # 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? def claimable?
if @owner.nil? if @owner.nil?
return true return true
@@ -30,45 +43,51 @@ class Planet
return false return false
end 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) def claim(player)
@owner = player @owner = player
end 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) def update(delta_time)
end end
# Returns information about the planet to be sent to the client.
# Function to return info to the client about a planet #
# @return [Hash] A hash containing the planet's ID and position.
#
def info_update def info_update
return { {
'id' => @planet_id, 'id' => @planet_id,
'position' => @position 'position' => @position
} }
end end
# Saves the planet to a file for persistence.
########################## #
##### UTIL FUNCTIONS ##### # @return [String] The planet ID that was saved.
########################## #
# Save a player to a file
def save_to_file def save_to_file
File.open("game_data/#{self.planet_id}_planet_.save", 'wb') do |fi| File.open("game_data/#{self.planet_id}_planet_.save", 'wb') do |fi|
# Load resources for the player:
fi.write(Marshal.dump(self)) fi.write(Marshal.dump(self))
end end
return self.planet_id self.planet_id
end 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) def self.load_from_save(fname)
File.open("game_data/#{fname}_planet_.save", 'rb') do |fi| File.open("game_data/#{fname}_planet_.save", 'rb') do |fi|
# Load resources for the player:
Marshal.load(fi.read) Marshal.load(fi.read)
end end
end end
end end
+13 -12
View File
@@ -1,5 +1,5 @@
# Nathan Hinton 22 Dec 2025 # Represents a player in the game.
# The main player file # Tracks resources, owned planets, ships, and researched technology.
require 'securerandom' require 'securerandom'
require 'yaml' require 'yaml'
@@ -8,6 +8,9 @@ require 'yaml'
class Player 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 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) def initialize(player_id, faction)
@player_id = player_id @player_id = player_id
@player_planets = [] @player_planets = []
@@ -26,20 +29,12 @@ class Player
@faction = faction @faction = faction
end end
# Create a new player. This is actually just setting it up. All the defaults def set_resources(credits, metal, crystals, fuel)
# 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)
@player_resources = { @player_resources = {
'credits' => credits, 'credits' => credits,
'metal' => metal, 'metal' => metal,
'crystals' => crystals, 'crystals' => crystals,
'fuel' => fuel 'fuel' => fuel,
} }
end end
@@ -164,10 +159,16 @@ class Player
@military_tech_build_power = data[:military_tech_build_power] || 0 @military_tech_build_power = data[:military_tech_build_power] || 0
end end
# Returns all buildings owned by the player.
# @return [Array<Building>] list of buildings.
def all_buildings def all_buildings
@buildings @buildings
end end
# Returns all researched technology for the player.
# @return [Array<String>] list of researched tech.
def all_tech def all_tech
@researched_tech @researched_tech
end end
+5 -2
View File
@@ -1,14 +1,16 @@
# Nathan Hinton 30 Dec 2025 # Represents a space-faring vessel owned by a player.
# Ship file for the game # Handles movement and validation of ship creation.
require 'securerandom' require 'securerandom'
# Load the ships into a constant: # Load the ships into a constant:
require 'yaml' require 'yaml'
# Configuration data for all available ships
SHIP_DATA = File.open('lib/game/data/ships.yaml', 'r') do |fi| SHIP_DATA = File.open('lib/game/data/ships.yaml', 'r') do |fi|
YAML.safe_load(fi.read()) YAML.safe_load(fi.read())
end end
# Represents a space-faring vessel owned by a player.
class Ship class Ship
attr_reader :owner, :position attr_reader :owner, :position
@@ -143,5 +145,6 @@ class Ship
end end
# Error class for ships # Error class for ships
# Represents a space-faring vessel owned by a player.
class ShipError < StandardError class ShipError < StandardError
end end
+14 -4
View File
@@ -36,8 +36,12 @@ require 'json'
HOSTNAME = 'localhost' HOSTNAME = 'localhost'
PORT = 2000 PORT = 2000
server_pid = spawn('rake serve', out: File::NULL, err: File::NULL)
sleep(0.5)
RSpec.describe "app.rb" do RSpec.describe "app.rb" do
let(:client) {TCPSocket.open(HOSTNAME, PORT)} let(:client) {TCPSocket.open(HOSTNAME, PORT)}
it 'responds with an error when a non JSON request is sent' do it 'responds with an error when a non JSON request is sent' do
client.puts 'this is non json test data' 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'}) 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 'type: hello' do
describe 'domain: player' do describe 'domain: player' do
describe 'payload: player_id, faction' 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 end
end end
@@ -73,5 +73,15 @@ RSpec.describe "app.rb" do
end end
describe 'type: error' do 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
end end
Process.kill('TERM', server_pid)
Process.wait(server_pid)
+9 -19
View File
@@ -1,6 +1,7 @@
# Nathan Hinton 24 Dec 2025 # Nathan Hinton 24 Dec 2025
# Test file for the player
# spec/player_spec.rb # spec/player_spec.rb
require 'rspec' require 'rspec'
require 'json' require 'json'
require 'game/player' # adjust the path if needed require 'game/player' # adjust the path if needed
@@ -46,16 +47,16 @@ RSpec.describe Player do
end end
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# create_new # set_resources
# ------------------------------------------------------------------ # ------------------------------------------------------------------
describe '#create_new' do describe '#set_resources' do
let(:credits) { 1000 } let(:credits) { 1000 }
let(:metal) { 500 } let(:metal) { 500 }
let(:crystals) { 200 } let(:crystals) { 200 }
let(:fuel) { 300 } let(:fuel) { 300 }
it 'stores the resources hash correctly' do 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( expect(subject.player_resources).to eq(
'credits' => credits, 'credits' => credits,
'metal' => metal, 'metal' => metal,
@@ -165,7 +166,7 @@ RSpec.describe Player do
let(:request_id) { 42 } let(:request_id) { 42 }
before do 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_planets << double('planet', info_update: 'test_data')
subject.player_ships << double('ship') subject.player_ships << double('ship')
end end
@@ -231,7 +232,7 @@ RSpec.describe Player do
let(:fuel) { 300 } let(:fuel) { 300 }
before do before do
subject.create_new(credits, metal, crystals, fuel) subject.set_resources(credits, metal, crystals, fuel)
end end
it 'writes a marshaled file and can read it back' do it 'writes a marshaled file and can read it back' do
@@ -245,7 +246,7 @@ RSpec.describe Player do
'credits' => credits, 'credits' => credits,
'metal' => metal, 'metal' => metal,
'crystals' => crystals, 'crystals' => crystals,
'fuel' => fuel 'fuel' => fuel,
) )
end end
end end
@@ -260,7 +261,7 @@ RSpec.describe Player do
let(:fuel) { 300 } let(:fuel) { 300 }
before do before do
subject.create_new(credits, metal, crystals, fuel) subject.set_resources(credits, metal, crystals, fuel)
end end
it 'returns a hash with the expected keys' do 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) expect(new_player.player_resources).to eq(subject.player_resources)
end end
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 end