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
# 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'
+6 -2
View File
@@ -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
+34 -5
View File
@@ -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
+20 -5
View File
@@ -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<String>] list of player IDs.
# @return [Array<Player>] 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<String>] number of planets to create or list of planet IDs.
# @return [Array<Planet>] 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<String>] list of ship IDs.
# @return [Array<Ship>] the loaded ships.
def load_ships(ids)
ids.map { |id| Ship.load_from_id(id) } # Ship is a placeholder
end
+46 -27
View File
@@ -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
+13 -12
View File
@@ -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<Building>] list of buildings.
def all_buildings
@buildings
end
# Returns all researched technology for the player.
# @return [Array<String>] list of researched tech.
def all_tech
@researched_tech
end
+5 -2
View File
@@ -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
+14 -4
View File
@@ -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)
+9 -19
View File
@@ -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