pushing befor major changes

This commit is contained in:
2025-12-20 15:07:58 -07:00
parent 057164122a
commit f4c5ba1f14
31 changed files with 438 additions and 1502 deletions
+1
View File
@@ -1,2 +1,3 @@
gem sinatra
gem sinatra-cross_origin
gem yard-sinatra
+2 -2
View File
@@ -23,8 +23,8 @@ end
namespace :doc do
desc 'Create the YARD documentation for the project'
task :yard do
res = sh 'yard doc *.rb'
sh 'yard stats *.rb --list-undoc'
res = sh 'yard doc --plugin yard-sinatra *.rb'
sh 'yard stats --plugin yard-sinatra *.rb --list-undoc'
end
desc 'Create code coverage'
+12
View File
@@ -62,6 +62,18 @@ class Game
@players.delete_at(idx)
end
# Finds if a player is in this game
#
# @return [Boolean]
def find_player(player_name)
@players.each do |player|
if player.name == player_name
return true
end
end
return false
end
# Checks if the given id matches a player in the game. Returns the player if
# so. otherwise raises an error.
#
+119
View File
@@ -0,0 +1,119 @@
# Nathan Hinton 19 Dec 2025
# Runs the server for the game
require 'sinatra'
require 'sinatra/cross_origin'
require 'json'
require './game'
require './player'
configure do
enable :cross_origin
end
# CORS headers for all routes
before do
headers 'Access-Control-Allow-Origin' => '*', # Allow all domains (you can change '*' to a specific domain for security)
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS', # Allow only specific methods
'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
end
test = Game.new(width: 10, height: 10, planets: 20, name: 'TEST')
test.add_player(Player.new(name: 'hi'))
games = {'TEST' => test}
# Get the server status
#
# @return [String] UP
get '/status' do
return 'UP'
end
# Creates a new game
#
# @param name [String] Required. The name of the game
# @param width [Integer] The width of the game space
# @param height [Integer] The height of the game space
# @param planets [Integer] The number of planets to have in the game
get '/create_game' do
puts "Creating game named #{params['name']}"
name = params['name']
width = params['width'] || 10
width = width.to_i
height = params['height'] || 10
height = height.to_i
planets = params['planets'] || 20
planets = planets.to_i
games.update({name => Game.new(width: width, height: height, planets: planets, name: name)})
return 'OK'
end
# Gets games available
#
# @param None
# @return [Array] list of game names
get '/list_games' do
return games.keys.to_json
end
# old: # Returns true if the given player is part of the game
# old: #
# old: # @params game [String] Goes in the splat
# old: # @params player_name [String] The name of the player to validate
# old: # @return [Boolean]
# old: get '/game/*/players' do
# old: game = validate_game(params['splat'][0])
# old: if game
# old: {"status" => game.find_player(params['player_name'])}.to_json
# old: end
# old: return {"status" => false}.to_json
# old: end
# Returns a player id for a given game
#
# @params game [String] Goes in the splat
# @params player_name [String] The name of the player to add
# @return [String] The ID for that player
get '/game/*/player_id' do
game = validate_game(params['splat'][0])
if game
player = game.find_player(params['player_name'])
if player
return {"status" => player.get_id}.to_json
end
end
return {"status" => false}.to_json
end
# Returns data about how the game is set up
# Returns the planets owned by a player
#
# @params game [String] Goes in the splat
# @params id [String] URL argument for player id.
get '/game/*/get_captured_planets' do
game = validate_game(params['splat'][0])
if game
begin
return {'planets' => game.get_captured_planets(params['id'])}.to_json
end
end
return {"status" => false}.to_json
end
# Validates that a game exists:
#
# @param key [String] The name of the game to check for
# @return [Game] The game the key belongs to. (Or nil if not found)
def validate_game(name)
return games[name]
end
View File
+169 -49
View File
@@ -1,55 +1,175 @@
# Nathan Hinton 19 Dec 2025
# Runs the server for the game
# Nathan Hinton 20 Dec 2025
# The main game server. This will handle the clients and the game in one go
require 'sinatra'
require 'sinatra/cross_origin'
require 'async'
require 'async/websocket'
require 'json'
require 'yaml'
require './game'
configure do
enable :cross_origin
end
# CORS headers for all routes
before do
headers 'Access-Control-Allow-Origin' => '*', # Allow all domains (you can change '*' to a specific domain for security)
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS', # Allow only specific methods
'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
end
games = {}
# Get the server status
get '/status' do
return 'UP'
end
# Creates a new game
#
# @param name [String] Required. The name of the game
# @param width [Integer] The width of the game space
# @param height [Integer] The height of the game space
# @param planets [Integer] The number of planets to have in the game
get '/create_game' do
puts "Creating game named #{params['name']}"
name = params['name']
width = params['width'] || 10
width = width.to_i
height = params['height'] || 10
height = height.to_i
planets = params['planets'] || 20
planets = planets.to_i
games.update({name => Game.new(width: width, height: height, planets: planets, name: name)})
return 'OK'
end
# Gets games available
#
# @return [Array] list of game names
get '/list_games' do
return games.keys.to_json
class GameServer
def initialize(conf_path)
@clients = {}
File.open(conf_path, 'r') do |fi|
world_config = YAML.safe_load(fi.read)
end
puts world_config
@world = World.new(
width: world_config['width'],
height: world_config['height'],
planets: world_config['planets'],
game_id: world_config['game_id'],
name: world_config['name']
)
end
def start(endpoint)
Async do |task|
Async::WebSocket::Server.open(endpoint) do |connection|
task.async do
handle_connection(connection)
end
end
task.async {simulation_loop}
end
end
def handle_connection(ws)
player = nil
while message == ws.read
data = JSON.parse(message)
response = nil
case data['type']
when 'hello'
player = handle_hello(ws, data)
when 'querey'
response = handle_query(ws, data)
when 'command'
response = handle_command(ws, data)
else
response = send_error('UNKNOWN_TYPE', 'Unknown message type')
end
ws.write(JSON.dump(response)) if response
end
rescue => e
puts "Connection error: #{e.message}"
ensure
disconnect(player)
end
def handle_hello(ws, data)
token = data.dig("payload", "auth_token")
player = authenticate(token)
@clients[player.id] = ws
ws.write(
JSON.dump(
{
type: "update",
domain: "all",
payload: {
player_id: player_id,
server_time: @world.time,
game_speed: @world.game_speed
player: player.snapshot
}
}
)
)
return player
end
def handle_query(player, data)
domain = data["domain"]
payload =
case domain
when "player"
player.snapshot
when "ship"
ship_id = data.dig("payload", "ship_id")
@world.ship(ship_id)&.snapshot
else
return error("UNKNOWN_DOMAIN", "Unknown query domain")
end
{
type: "update",
domain: domain,
payload: payload
}
end
def handle_command(player, data)
domain = data["domain"]
payload = data["payload"]
case domain
when "ship"
validate_and_queue_ship_command(player, payload)
else
return error("UNKNOWN_DOMAIN", "Unknown command domain")
end
{
type: "update",
domain: "command",
payload: { status: "accepted" }
}
rescue ValidationError => e
error("INVALID_COMMAND", e.message)
end
def validate_and_queue_ship_command(player, payload)
ship = @world.ship(payload["ship_id"])
raise ValidationError, "Ship not found" unless ship
raise ValidationError, "Not your ship" unless ship.owner == player
ship.set_target(payload["target"])
end
def broadcast_event(type, domain, payload)
message = JSON.dump({
type: type,
domain: domain,
payload: payload
})
@clients.values.each do |ws|
ws.write(message)
end
end
def simulation_loop
last = Time.now
loop do
now = Time.now
delta_real = now - last
delta_sim = delta_real * @world.game_speed
@world.advance(delta_sim)
last = now
sleep 0.05 # 20 tps
end
end
def error(code, message)
{
type: "error",
domain: "system",
payload: {
code: code,
message: message
}
}
end
end
+33 -31
View File
@@ -1,34 +1,36 @@
example_id | status | run_time |
---------------------------------- | ------ | --------------- |
./spec/building_spec.rb[1:1:1] | passed | 0.00024 seconds |
./spec/building_spec.rb[1:1:2] | passed | 0.00044 seconds |
./spec/building_spec.rb[1:1:3] | passed | 0.00059 seconds |
./spec/building_spec.rb[1:2:1] | passed | 0.00024 seconds |
./spec/building_spec.rb[1:2:2] | passed | 0.00208 seconds |
./spec/game_spec.rb[1:1:1] | passed | 0.00011 seconds |
./spec/game_spec.rb[1:1:2] | passed | 0.00011 seconds |
./spec/game_spec.rb[1:1:3] | passed | 0.00011 seconds |
./spec/game_spec.rb[1:1:4] | passed | 0.00014 seconds |
./spec/game_spec.rb[1:2:1] | passed | 0.00014 seconds |
./spec/game_spec.rb[1:2:2] | passed | 0.00016 seconds |
./spec/game_spec.rb[1:3:1] | passed | 0.00019 seconds |
./spec/game_spec.rb[1:3:2] | passed | 0.00026 seconds |
./spec/game_spec.rb[1:4:1] | passed | 0.00013 seconds |
./spec/game_spec.rb[1:4:2] | passed | 0.00016 seconds |
./spec/game_spec.rb[1:5:1] | passed | 0.00016 seconds |
./spec/planet_spec.rb[1:1:1] | passed | 0.00018 seconds |
./spec/planet_spec.rb[1:1:2] | passed | 0.00013 seconds |
./spec/planet_spec.rb[1:3:1] | passed | 0.00015 seconds |
./spec/planet_spec.rb[1:4:1] | passed | 0.00018 seconds |
./spec/planet_spec.rb[1:4:2] | passed | 0.00021 seconds |
./spec/planet_spec.rb[1:5:1] | passed | 0.00168 seconds |
./spec/planet_spec.rb[1:5:2] | passed | 0.00039 seconds |
./spec/planet_spec.rb[1:6:1] | passed | 0.00873 seconds |
./spec/player_spec.rb[1:1:1] | passed | 0.00035 seconds |
./spec/player_spec.rb[1:2:1] | passed | 0.00052 seconds |
./spec/player_spec.rb[1:2:2] | passed | 0.00044 seconds |
./spec/building_spec.rb[1:1:1] | passed | 0.00009 seconds |
./spec/building_spec.rb[1:1:2] | passed | 0.00008 seconds |
./spec/building_spec.rb[1:1:3] | passed | 0.00015 seconds |
./spec/building_spec.rb[1:2:1] | passed | 0.00012 seconds |
./spec/building_spec.rb[1:2:2] | passed | 0.00013 seconds |
./spec/game_spec.rb[1:1:1] | passed | 0.00006 seconds |
./spec/game_spec.rb[1:1:2] | passed | 0.00006 seconds |
./spec/game_spec.rb[1:1:3] | passed | 0.00006 seconds |
./spec/game_spec.rb[1:1:4] | passed | 0.00008 seconds |
./spec/game_spec.rb[1:2:1] | passed | 0.00022 seconds |
./spec/game_spec.rb[1:2:2] | passed | 0.00012 seconds |
./spec/game_spec.rb[1:3:1] | passed | 0.00008 seconds |
./spec/game_spec.rb[1:3:2] | passed | 0.00011 seconds |
./spec/game_spec.rb[1:4:1] | passed | 0.00007 seconds |
./spec/game_spec.rb[1:4:2] | passed | 0.00009 seconds |
./spec/game_spec.rb[1:5:1] | passed | 0.00008 seconds |
./spec/game_spec.rb[1:6:1] | passed | 0.00006 seconds |
./spec/game_spec.rb[1:6:2] | passed | 0.0001 seconds |
./spec/planet_spec.rb[1:1:1] | passed | 0.00005 seconds |
./spec/planet_spec.rb[1:1:2] | passed | 0.00006 seconds |
./spec/planet_spec.rb[1:3:1] | passed | 0.00014 seconds |
./spec/planet_spec.rb[1:4:1] | passed | 0.00009 seconds |
./spec/planet_spec.rb[1:4:2] | passed | 0.00082 seconds |
./spec/planet_spec.rb[1:5:1] | passed | 0.00007 seconds |
./spec/planet_spec.rb[1:5:2] | passed | 0.00036 seconds |
./spec/planet_spec.rb[1:6:1] | passed | 0.00446 seconds |
./spec/player_spec.rb[1:1:1] | passed | 0.0004 seconds |
./spec/player_spec.rb[1:2:1] | passed | 0.00242 seconds |
./spec/player_spec.rb[1:2:2] | passed | 0.00028 seconds |
./spec/simple_game_spec.rb[1:1] | passed | 0.00956 seconds |
./spec/space_object_spec.rb[1:1:1] | passed | 0.00012 seconds |
./spec/space_object_spec.rb[1:3:1] | passed | 0.00023 seconds |
./spec/space_object_spec.rb[1:4:1] | passed | 0.00013 seconds |
./spec/space_object_spec.rb[1:6:1] | passed | 0.00012 seconds |
./spec/space_object_spec.rb[1:1:1] | passed | 0.00006 seconds |
./spec/space_object_spec.rb[1:3:1] | passed | 0.00008 seconds |
./spec/space_object_spec.rb[1:4:1] | passed | 0.00006 seconds |
./spec/space_object_spec.rb[1:6:1] | passed | 0.00007 seconds |
+13
View File
@@ -84,4 +84,17 @@ RSpec.describe Game do
expect(game.get_captured_planets(player.get_id)).to eq game.instance_variable_get(:@planets)
end
end
describe '#find_player' do
it 'returns false if the player is not in the game' do
game = Game.new()
expect(game.find_player('bill')).to eq false
end
it 'returns true if the player is in the game' do
game = Game.new(planets: 1)
game.add_player(Player.new(name: 'bob'))
expect(game.find_player('bob')).to eq true
end
end
end
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
rerun 'ruby server.rb'
+83
View File
@@ -0,0 +1,83 @@
# Chatting with GPT:
I think that I will modify some of how I am doing things. We will have the game do things differently. We will have a main loop that will process events and then we will use delta time and a `game speed` variable to control things.
Here is my newly defined API:
# API
Transport: WebSocket
Encoding: JSON
Direction: Bidirectional
Every message has:
```
{
"type": "...",
"domain": "...",
"request_id": integer
"payload": {}
}
```
## Message types:
### `hello`
- Direction: Client --> Server
- Purpose: initiates the connection and has authentication
- Behavior:
- Must be responded to immediately
- Response must be an `update` type with a full update (domain `all`)
### `query`
- Direction: Client --> Server
- Purpose: request read-only data
- Behavior:
- Must be responded to immediately
- Does not mutate server state
- Response must be an `update` type with the requested domain
### `command`
- Direction: Client --> Server
- Purpose: Request a state change
- Behavior:
- Server validates the command immediately
- If the command is valid the server applies the command and acknowledges with an `event` with the resulted action.
- If invalid the server responds with an `error` and includes the command and an error message.
### `update`
- Direction: Server --> Client
- Purpose: Deliver the game state to the client
- Behavior:
- For a full state update (ie player login) the `all` domain is used. Otherwise it is a subdomain.
- Used as a response to `hello`, `query`.
- Full updates are sent periodically for client synchnorization.
### `event`
- Direction: Server --> Client
- Purpose: Notify the client of events
- Behavior:
- Represents something that *happened*
- Not a full state snapshot
- Can not be requested by the client
### `error`
- Direction: Server --> Client
- Purpose: Report a failure to process a `query` or `command`
- Behavior:
- Does not mutate state
- Describe why the request failed.
## Notes
**The server is authoritative for all game state. Clients may not infer or simulate authoritative outcomes beyond visual interpolation.**
-1
View File
@@ -1 +0,0 @@
--require spec_helper
-36
View File
@@ -1,36 +0,0 @@
# 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
namespace :doc do
desc 'Create the YARD documentation for the project'
task :yard do
res = sh 'yard doc *.rb --list-undoc'
sh 'yard stats *.rb --list-undoc'
end
desc 'Create code coverage'
task :coverage do
ENV['COVERAGE'] = 'true'
Rake::Task[:test].invoke
end
end
-14
View File
@@ -1,14 +0,0 @@
# Nathan Hinton 1 Oct 2025
# This file basiacally creates a universe and runs a simulated game in it.
require './universe'
require './player'
universe = Universe.new('test universe', universe_size: 3)
universe.create_universe
universe.print_universe()
planets = universe.planets
player = Player.new('test player')
puts player.name
-17
View File
@@ -1,17 +0,0 @@
# 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
-40
View File
@@ -1,40 +0,0 @@
# Nathan Hinton 1 Oct 2025
PLANET_PRODUCTION_RESOURCES = [:ships]
# This class holds information about a planet.
class Planet
attr_reader :owner
# Create a planet
#
# @param random [Random] The random generator for the game (ensures seeds can be duplicated)
def initialize(random)
@random = random
@prod_rate = @random.rand
@resource = PLANET_PRODUCTION_RESOURCES.sample(random: @random)
@owner = nil
end
# Change the ownership of a planet. Should check for things like the planet
# has no defenders left and other fun stuff (later)
#
# @param owner [Player] The owner of the planet
# @return [Boolean] If the operation was successful or not
def change_owner(owner)
# For now only unclaimed planets can be owned
if @owner.nil?
@owner = owner
return true
end
return false
end
# Incriment the planet. Used to add resources to the Player's global resource
# storage
def tick
if !@owner.nil?
@oener.add_resources({@resource => @prod_rate})
end
end
end
-25
View File
@@ -1,25 +0,0 @@
# Nathan Hinton 2 Oct 2025
# Class for the player. For now does not do much.
class Player
attr_reader :name, :resources
# initializes with just the player name.
#
# @param name [String] The name of the player
def initialize(name)
@name = name
@resources = {:ships => 10, :metal => 100}
end
def add_resources(resources)
resources.keys.each do |resource_key|
if @resources[resource_key].nil?
@resources[resource_key] = resources[resource_key]
else
@resources[resource_key] += resources[resource_key]
end
end
end
end
-52
View File
@@ -1,52 +0,0 @@
# 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<Integer>] The (a, b, c) position
def two_d_hexagonal
return [a, b, c]
end
# Returns the coordinates in the 2d plane
#
# @return [Array<Integer>] The (x, y) position
def two_d_cartesian
return [x_pos, y_pos]
end
# Returns the string formatted version
#
# @return [String] Returns a string representing the point.
def to_s
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
end
-63
View File
@@ -1,63 +0,0 @@
# Nathan Hinton 2 Oct 2025
# This class will be the container for ships. For now they are all the same
class Ship
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
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
-159
View File
@@ -1,159 +0,0 @@
# 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
-52
View File
@@ -1,52 +0,0 @@
# Nathan Hinton 2 Oct 2025
require './planet'
require './player'
RSpec.describe Planet do
let(:seed) { 12345 }
let(:random) { Random.new(seed) }
let(:planet) { Planet.new(random) }
let(:player_1) { Player.new('tester 01') }
let(:player_2) { Player.new('tester 02') }
describe '#initialize' do
it 'stores the random off' do
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
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]
end
it 'has a nil owner' do
expect(planet.instance_variable_get(:@owner)).to eq nil
end
end # describe #initialize
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.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.instance_variable_get(:@owner)).to eq player_2
end
end
describe '#tick' do
it 'adds resources to the Players storage' do
planet.change_owner(player_1)
planet.tick
expect(player_1.resources).to eq('')
end
end
end
-16
View File
@@ -1,16 +0,0 @@
# Nahtan Hinton 3 Oct 2025
require './player'
RSpec.describe Player do
let(:player_1) { Player.new('tester 01') }
describe '#initialize' do
it 'sets the player name' do
expect(player_1.instance_variable_get(:@name)).to eq 'tester 01'
end
it 'gives initial resources' do
expect(player_1.instance_variable_get(:@resources)).to eq ({:ships => 10, :metal => 100})
end
end
end
-72
View File
@@ -1,72 +0,0 @@
# frozen_string_literal: true
require './position'
RSpec.describe Position do
let(:even_pair) { [4, 6] } # 4+6 = 10 → even
let(:odd_pair) { [3, 4] } # 3+4 = 7 → odd
describe 'initialisation' 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]
end
it 'raises ArgumentError when the sum is odd' do
expect { Position.new(*odd_pair) }
.to raise_error(ArgumentError, /must sum to an even number/)
end
end
context 'with three arguments' do
it 'raises NotImplementedError' do
expect { Position.new(1, 2, 3) }
.to raise_error(NotImplementedError, /positioning not implimented/)
end
end
context 'with an unsupported number of arguments' do
it 'raises ArgumentError when no arguments are supplied' do
expect { Position.new }
.to raise_error(ArgumentError, /requires two or three integer args/)
end
it 'raises ArgumentError when too many arguments are supplied' do
expect { Position.new(1, 2, 3, 4) }
.to raise_error(ArgumentError, /requires two or three integer args/)
end
end
end
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
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]
end
end
describe '#to_s' do
it 'puts out the coordinates' do
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
end
it 'returns true when the positions are the same' do
expect(Position.new(*even_pair) == Position.new(4, 6)).to eq true
end
end
end
-90
View File
@@ -1,90 +0,0 @@
# frozen_string_literal: true
require './ship'
require './player'
RSpec.describe Ship do
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]
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 (no exception) when called' do
expect { ship.tick }.not_to raise_error
end
end
describe '#move' do
context 'when coordinates are valid (even sum, adjacent)' do
it 'accepts positions that satisfy the rules' do
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 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
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 0..5 and sets @moving correctly' do
mapping = {
0 => [2, 0],
1 => [1, 1],
2 => [-1, 1],
3 => [-2, 0],
4 => [-1, -1],
5 => [1, -1]
}
mapping.each do |dir, expected|
expect { ship.move_direction(dir) }.not_to raise_error
expect(ship.instance_variable_get(:@moving)).to eq expected
end
end
it 'raises 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
-102
View File
@@ -1,102 +0,0 @@
# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
# The generated `.rspec` file contains `--require spec_helper` which will cause
# this file to always be loaded, without a need to explicitly require it in any
# files.
#
# Given that it is always loaded, you are encouraged to keep this file as
# light-weight as possible. Requiring heavyweight dependencies from this file
# will add to the boot time of your test suite on EVERY test run, even for an
# individual file that may not need all of that loaded. Instead, consider making
# a separate helper file that requires the additional dependencies and performs
# the additional setup, and require it from the spec files that actually need
# it.
#
# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
if ENV['COVERAGE'] == 'true'
require 'simplecov'
SimpleCov.start
end
RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
config.expect_with :rspec do |expectations|
# This option will default to `true` in RSpec 4. It makes the `description`
# and `failure_message` of custom matchers include text for helper methods
# defined using `chain`, e.g.:
# be_bigger_than(2).and_smaller_than(4).description
# # => "be bigger than 2 and smaller than 4"
# ...rather than:
# # => "be bigger than 2"
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
# rspec-mocks config goes here. You can use an alternate test double
# library (such as bogus or mocha) by changing the `mock_with` option here.
config.mock_with :rspec do |mocks|
# Prevents you from mocking or stubbing a method that does not exist on
# a real object. This is generally recommended, and will default to
# `true` in RSpec 4.
mocks.verify_partial_doubles = true
end
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
# have no way to turn it off -- the option exists only for backwards
# compatibility in RSpec 3). It causes shared context metadata to be
# inherited by the metadata hash of host groups and examples, rather than
# triggering implicit auto-inclusion in groups with matching metadata.
config.shared_context_metadata_behavior = :apply_to_host_groups
# The settings below are suggested to provide a good initial experience
# with RSpec, but feel free to customize to your heart's content.
# 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
# aliases for `it`, `describe`, and `context` that include `:focus`
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
config.filter_run_when_matching :focus
# Allows RSpec to persist some state between runs in order to support
# the `--only-failures` and `--next-failure` CLI options. We recommend
# you configure your source control system to ignore this file.
config.example_status_persistence_file_path = "spec/examples.txt"
# Limits the available syntax to the non-monkey patched syntax that is
# recommended. For more details, see:
# https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
config.disable_monkey_patching!
# This setting enables warnings. It's recommended, but in some cases may
# be too noisy due to issues in dependencies.
config.warnings = true
# Many RSpec users commonly either run the entire suite or an individual
# file, and it's useful to allow more verbose output when running an
# individual spec file.
if config.files_to_run.one?
# Use the documentation formatter for detailed output,
# unless a formatter has already been configured
# (e.g. via a command-line flag).
config.default_formatter = "doc"
end
# 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
# 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
# the seed, which is printed after each run.
# --seed 1234
config.order = :random
# Seed global randomization in this process using the `--seed` CLI option.
# Setting this allows you to use `--seed` to deterministically reproduce
# test failures related to randomization by passing the same `--seed` value
# as the one that triggered the failure.
Kernel.srand config.seed
end
-12
View File
@@ -1,12 +0,0 @@
# If you are getting an error `uninitialized constant StringIO` make sure that
# your code is in an `it` block.
# Helper that captures STDOUT.
def capture_stdout
output = StringIO.new
$stdout = output
yield
output.string
ensure
$stdout = STDOUT
end
-45
View File
@@ -1,45 +0,0 @@
# Nathan Hinton 2 Oct 2025
require './universe_array'
RSpec.describe UniverseArray do
describe '#[]' do
it 'works with indexes' do
uni_arr = UniverseArray.new()
uni_arr[0] = []
uni_arr[0][0] = 'hi'
expect(uni_arr[0][0]).to eq 'hi'
end
it 'works with positions' do
uni_arr = UniverseArray.new()
uni_arr[0] = []
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
it 'works with indexes' do
uni_arr = UniverseArray.new()
uni_arr[0] = []
uni_arr[0][0] = 'hi'
expect(uni_arr[0][0]).to eq 'hi'
end
it 'works with positions' do
uni_arr = UniverseArray.new()
uni_arr[0] = []
uni_arr[Position.new(0, 0)] = 'hi'
expect(uni_arr[Position.new(0, 0)]).to eq 'hi'
end
end
end
-163
View File
@@ -1,163 +0,0 @@
# frozen_string_literal: true
require './universe'
require './planet'
require './position'
require './universe_array'
require './spec/stdout_helper'
RSpec.describe Universe do
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
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
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)
end
it 'initializes the random if no seed is passed in' do
uni = Universe.new(name)
rng = uni.instance_variable_get(:@random)
expect(rng).to be_a(Random)
end
end
describe '#create_universe' do
end
describe '#distance' 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)
end
it 'returns 0 when both points are identical' do
p = Position.new(1, 1)
expect(universe.distance(p, p)).to eq 0
end
it 'correctly computes known hex distances' do
p0 = Position.new(0, 0)
p1 = Position.new(2, 0)
p2 = Position.new(4, 2)
p3 = Position.new(2, 2)
# (0,0) to (2,0) → 1
expect(universe.distance(p0, p1)).to eq 1
# (0,0) to (4,2) → 3
expect(universe.distance(p0, p2)).to eq 3
# (0,0) to (2,2) → 2
expect(universe.distance(p0, p3)).to eq 2
end
end # describe '#distance'
describe '#map generation' do
it 'creates the expected dimensions' do
universe.create_universe
map = universe.instance_variable_get(:@universe_map)
# Each valid (x,y) pair inside the radius contains a Planet
# (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
end
it 'contains a planet at every valid point when create_chances is 1' do
universe.create_universe
map = universe.instance_variable_get(:@universe_map)
map.each do |row|
row.each do |idx|
# The indexes are allowed to be nil
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 # describe '#map generation'
it 'is deterministic when the same seed is used' do
first_universe = Universe.new(
name,
universe_size: universe_size,
seed: seed,
create_chances: { planet: 0.1 }
)
first_map = first_universe.instance_variable_get(:@universe_map)
second_universe = Universe.new(
name,
universe_size: universe_size,
seed: seed,
create_chances: { planet: 0.1 }
)
second_map = second_universe.instance_variable_get(:@universe_map)
expect(first_map).to eq second_map
end
end
describe '#print_universe' do
it 'prints the center point as “O”' do
out = capture_stdout do
universe.create_universe
universe.print_universe
end
expect(out).to include('O')
end
it 'can print blank spaces for nothing' do
out = capture_stdout do
uni = Universe.new(name, create_chances: {planet: 0.0})
uni.create_universe
uni.print_universe
end
expect(out).to include('O')
expect(out).to include('- -')
end
end
describe '#planets' do
it 'lists all of the generated planets' do
universe.create_universe
expect(universe.planets.length).to eq 18
end
end
describe '#tick' do
it 'runs a tick of the game' do
universe.create_universe
player_1 = Player.new('test player 01')
universe.add_player(player_1)
universe.add_ship(player_1, Position.new(3, 5))
ships = universe.get_player_ships(player_1)
universe.move_ship(player_1, ships[0], Position.new(2, 4))
# Now we tick the universe and the ship should move a little and the player should gain resources.
expect{universe.tick}.to_not raise_error()
end
end
end
-8
View File
@@ -1,8 +0,0 @@
# Nathan Hinton Oct 2 2025
# Literally just a class to have something else in space...
class Star
def initialize
end
end
-406
View File
@@ -1,406 +0,0 @@
# Nathan Hinton 1 Oct 2025
require 'logger'
require './planet'
require './star'
require './position'
require './universe_array'
# 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
# Setup logger for this class:
@@logger = Logger.new('logs/universe.log')
attr_reader :name
def initialize(name, universe_size: 4, create_chances: {planet: 1}, seed: nil)
@name = name
@universe_size = universe_size
@create_chances = create_chances
# Setup internal lists:
@player_list = []
@planet_list = []
@ship_list = []
# initizlize RNG
if seed.nil?
@random = Random.new()
@seed = @random.seed
else
@seed = seed
@random = Random.new(@seed)
end
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)
@@logger.info "Generating Universe '#{@name}' with seed #{@seed}"
@@logger.info "Center is #{@center}"
@universe_map = UniverseArray.new()
# 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
# 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
end # Create y points
end # Create x points
# Remove any empty nodes:
end # Function create_universe
# 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
res = (dist_x / 2) + dist_y
if dist_x >= dist_y
res = (dist_x + dist_y) / 2
end
@@logger.debug "Distance: #{position_a} and #{position_b} are #{res} units away"
return res
end # Function distance
# Prints the universe to the screen.
def print_universe(simple: false)
(0 .. (@universe_map.length)).each do |y_pos|
# Print empty space before chart (for angles)
if @universe_map[y_pos].nil?
next
end
if @universe_map[y_pos] == []
puts '_' * (4 * @universe_size * 2)
next
end
nextline = '|'
if y_pos < @universe_size
nextline += ' ' * ((@universe_size - y_pos).abs - 1)
else
nextline += ' ' * ((@universe_size - y_pos).abs)
end
if @universe_size.odd?
nextline += ' '
end
# Print more empty space for the odd places (provides offset)
if y_pos.odd?
print '| '
else
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?
position = Position.new(x_pos, y_pos)
# Print empty space if we are outside the universe (For items)
if distance(@center, position) >= @universe_size
print ' '
next
end
# Print what we are:
value = @universe_map[position]
nextline += ' '
if value.is_a?(Star)
print 'O'
elsif value.is_a?(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, Position.new(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
# Returns all of the planets contained in the universe:
#
# @return [Array<Planet>]
def planets
result = []
@universe_map.flatten.each do |item|
result.push(item) if item.is_a? Planet
end
return result
end
# Runs a tick of the game. Passes it on to all of the sub objects
def tick
# Incriment planets
@planet_list.each do |planet|
planet.tick
end
# Incriment ships
@ship_list.each do |ship|
ship.tick
end
end
################################################################################
################################################################################
################################################################################
################################################################################
################################################################################
################################################################################
##################################################
################### 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
# @return [Planet, nil] Returns the thing at that point.
def get_position(player, position)
return @universe_map[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)
return false unless validate_player(player)
end
alias_method :add_ship, :create_ship
# 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)
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
##################################################
##################### 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
# Change the ownership of a planet
#
# @param planet [Planet] The planet to be modified
# @param player [Player] The player who should now own the planet
# @param position [Position] The position of the planet
##################################################
##################### 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)
# Give the player a planet
free_planets = []
planets.each do |planet|
free_planets.push(planet) if planet.owner.nil?
end
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
#
# @param player [Player] The player that should be removed from the player list.
def remove_player(player)
if @player_list.delete(player).nil?
@@logger.debug 'Failed to remove player! Does not exist in this universe! Returning true anyway though!'
end
@@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<Ship>]
def get_player_ships(player)
return [] unless validate_player(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)
return [] unless validate_player(player)
result = []
@planet_list.each do |planet|
if planet.owner == player
result.push(planet)
end
end
return result
end
end # Class Universe
-47
View File
@@ -1,47 +0,0 @@
# 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
@@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]
else
super
end
end
end
+3
View File
@@ -0,0 +1,3 @@
# Designing UI
I have decided to use godot to design the UI. (Hopefully it works...)