pushing befor major changes
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
gem sinatra
|
gem sinatra
|
||||||
gem sinatra-cross_origin
|
gem sinatra-cross_origin
|
||||||
|
gem yard-sinatra
|
||||||
|
|||||||
+2
-2
@@ -23,8 +23,8 @@ end
|
|||||||
namespace :doc do
|
namespace :doc do
|
||||||
desc 'Create the YARD documentation for the project'
|
desc 'Create the YARD documentation for the project'
|
||||||
task :yard do
|
task :yard do
|
||||||
res = sh 'yard doc *.rb'
|
res = sh 'yard doc --plugin yard-sinatra *.rb'
|
||||||
sh 'yard stats *.rb --list-undoc'
|
sh 'yard stats --plugin yard-sinatra *.rb --list-undoc'
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Create code coverage'
|
desc 'Create code coverage'
|
||||||
|
|||||||
@@ -62,6 +62,18 @@ class Game
|
|||||||
@players.delete_at(idx)
|
@players.delete_at(idx)
|
||||||
end
|
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
|
# Checks if the given id matches a player in the game. Returns the player if
|
||||||
# so. otherwise raises an error.
|
# so. otherwise raises an error.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -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
|
||||||
+169
-49
@@ -1,55 +1,175 @@
|
|||||||
# Nathan Hinton 19 Dec 2025
|
# Nathan Hinton 20 Dec 2025
|
||||||
# Runs the server for the game
|
# The main game server. This will handle the clients and the game in one go
|
||||||
|
|
||||||
require 'sinatra'
|
require 'async'
|
||||||
require 'sinatra/cross_origin'
|
require 'async/websocket'
|
||||||
require 'json'
|
require 'json'
|
||||||
|
require 'yaml'
|
||||||
|
|
||||||
require './game'
|
require './game'
|
||||||
|
|
||||||
configure do
|
class GameServer
|
||||||
enable :cross_origin
|
def initialize(conf_path)
|
||||||
end
|
@clients = {}
|
||||||
|
File.open(conf_path, 'r') do |fi|
|
||||||
# CORS headers for all routes
|
world_config = YAML.safe_load(fi.read)
|
||||||
before do
|
end
|
||||||
headers 'Access-Control-Allow-Origin' => '*', # Allow all domains (you can change '*' to a specific domain for security)
|
puts world_config
|
||||||
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS', # Allow only specific methods
|
@world = World.new(
|
||||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
|
width: world_config['width'],
|
||||||
end
|
height: world_config['height'],
|
||||||
|
planets: world_config['planets'],
|
||||||
|
game_id: world_config['game_id'],
|
||||||
games = {}
|
name: world_config['name']
|
||||||
|
)
|
||||||
|
end
|
||||||
# Get the server status
|
|
||||||
get '/status' do
|
def start(endpoint)
|
||||||
return 'UP'
|
Async do |task|
|
||||||
end
|
Async::WebSocket::Server.open(endpoint) do |connection|
|
||||||
|
task.async do
|
||||||
# Creates a new game
|
handle_connection(connection)
|
||||||
#
|
end
|
||||||
# @param name [String] Required. The name of the game
|
end
|
||||||
# @param width [Integer] The width of the game space
|
task.async {simulation_loop}
|
||||||
# @param height [Integer] The height of the game space
|
end
|
||||||
# @param planets [Integer] The number of planets to have in the game
|
end
|
||||||
get '/create_game' do
|
|
||||||
puts "Creating game named #{params['name']}"
|
def handle_connection(ws)
|
||||||
name = params['name']
|
player = nil
|
||||||
width = params['width'] || 10
|
while message == ws.read
|
||||||
width = width.to_i
|
data = JSON.parse(message)
|
||||||
height = params['height'] || 10
|
response = nil
|
||||||
height = height.to_i
|
|
||||||
planets = params['planets'] || 20
|
case data['type']
|
||||||
planets = planets.to_i
|
when 'hello'
|
||||||
games.update({name => Game.new(width: width, height: height, planets: planets, name: name)})
|
player = handle_hello(ws, data)
|
||||||
return 'OK'
|
when 'querey'
|
||||||
end
|
response = handle_query(ws, data)
|
||||||
|
when 'command'
|
||||||
|
response = handle_command(ws, data)
|
||||||
# Gets games available
|
else
|
||||||
#
|
response = send_error('UNKNOWN_TYPE', 'Unknown message type')
|
||||||
# @return [Array] list of game names
|
end
|
||||||
get '/list_games' do
|
|
||||||
return games.keys.to_json
|
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
|
end
|
||||||
|
|||||||
+33
-31
@@ -1,34 +1,36 @@
|
|||||||
example_id | status | run_time |
|
example_id | status | run_time |
|
||||||
---------------------------------- | ------ | --------------- |
|
---------------------------------- | ------ | --------------- |
|
||||||
./spec/building_spec.rb[1:1:1] | passed | 0.00024 seconds |
|
./spec/building_spec.rb[1:1:1] | passed | 0.00009 seconds |
|
||||||
./spec/building_spec.rb[1:1:2] | passed | 0.00044 seconds |
|
./spec/building_spec.rb[1:1:2] | passed | 0.00008 seconds |
|
||||||
./spec/building_spec.rb[1:1:3] | passed | 0.00059 seconds |
|
./spec/building_spec.rb[1:1:3] | passed | 0.00015 seconds |
|
||||||
./spec/building_spec.rb[1:2:1] | passed | 0.00024 seconds |
|
./spec/building_spec.rb[1:2:1] | passed | 0.00012 seconds |
|
||||||
./spec/building_spec.rb[1:2:2] | passed | 0.00208 seconds |
|
./spec/building_spec.rb[1:2:2] | passed | 0.00013 seconds |
|
||||||
./spec/game_spec.rb[1:1:1] | passed | 0.00011 seconds |
|
./spec/game_spec.rb[1:1:1] | passed | 0.00006 seconds |
|
||||||
./spec/game_spec.rb[1:1:2] | passed | 0.00011 seconds |
|
./spec/game_spec.rb[1:1:2] | passed | 0.00006 seconds |
|
||||||
./spec/game_spec.rb[1:1:3] | passed | 0.00011 seconds |
|
./spec/game_spec.rb[1:1:3] | passed | 0.00006 seconds |
|
||||||
./spec/game_spec.rb[1:1:4] | passed | 0.00014 seconds |
|
./spec/game_spec.rb[1:1:4] | passed | 0.00008 seconds |
|
||||||
./spec/game_spec.rb[1:2:1] | passed | 0.00014 seconds |
|
./spec/game_spec.rb[1:2:1] | passed | 0.00022 seconds |
|
||||||
./spec/game_spec.rb[1:2:2] | passed | 0.00016 seconds |
|
./spec/game_spec.rb[1:2:2] | passed | 0.00012 seconds |
|
||||||
./spec/game_spec.rb[1:3:1] | passed | 0.00019 seconds |
|
./spec/game_spec.rb[1:3:1] | passed | 0.00008 seconds |
|
||||||
./spec/game_spec.rb[1:3:2] | passed | 0.00026 seconds |
|
./spec/game_spec.rb[1:3:2] | passed | 0.00011 seconds |
|
||||||
./spec/game_spec.rb[1:4:1] | passed | 0.00013 seconds |
|
./spec/game_spec.rb[1:4:1] | passed | 0.00007 seconds |
|
||||||
./spec/game_spec.rb[1:4:2] | passed | 0.00016 seconds |
|
./spec/game_spec.rb[1:4:2] | passed | 0.00009 seconds |
|
||||||
./spec/game_spec.rb[1:5:1] | passed | 0.00016 seconds |
|
./spec/game_spec.rb[1:5:1] | passed | 0.00008 seconds |
|
||||||
./spec/planet_spec.rb[1:1:1] | passed | 0.00018 seconds |
|
./spec/game_spec.rb[1:6:1] | passed | 0.00006 seconds |
|
||||||
./spec/planet_spec.rb[1:1:2] | passed | 0.00013 seconds |
|
./spec/game_spec.rb[1:6:2] | passed | 0.0001 seconds |
|
||||||
./spec/planet_spec.rb[1:3:1] | passed | 0.00015 seconds |
|
./spec/planet_spec.rb[1:1:1] | passed | 0.00005 seconds |
|
||||||
./spec/planet_spec.rb[1:4:1] | passed | 0.00018 seconds |
|
./spec/planet_spec.rb[1:1:2] | passed | 0.00006 seconds |
|
||||||
./spec/planet_spec.rb[1:4:2] | passed | 0.00021 seconds |
|
./spec/planet_spec.rb[1:3:1] | passed | 0.00014 seconds |
|
||||||
./spec/planet_spec.rb[1:5:1] | passed | 0.00168 seconds |
|
./spec/planet_spec.rb[1:4:1] | passed | 0.00009 seconds |
|
||||||
./spec/planet_spec.rb[1:5:2] | passed | 0.00039 seconds |
|
./spec/planet_spec.rb[1:4:2] | passed | 0.00082 seconds |
|
||||||
./spec/planet_spec.rb[1:6:1] | passed | 0.00873 seconds |
|
./spec/planet_spec.rb[1:5:1] | passed | 0.00007 seconds |
|
||||||
./spec/player_spec.rb[1:1:1] | passed | 0.00035 seconds |
|
./spec/planet_spec.rb[1:5:2] | passed | 0.00036 seconds |
|
||||||
./spec/player_spec.rb[1:2:1] | passed | 0.00052 seconds |
|
./spec/planet_spec.rb[1:6:1] | passed | 0.00446 seconds |
|
||||||
./spec/player_spec.rb[1:2:2] | passed | 0.00044 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/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:1:1] | passed | 0.00006 seconds |
|
||||||
./spec/space_object_spec.rb[1:3:1] | passed | 0.00023 seconds |
|
./spec/space_object_spec.rb[1:3:1] | passed | 0.00008 seconds |
|
||||||
./spec/space_object_spec.rb[1:4:1] | passed | 0.00013 seconds |
|
./spec/space_object_spec.rb[1:4:1] | passed | 0.00006 seconds |
|
||||||
./spec/space_object_spec.rb[1:6:1] | passed | 0.00012 seconds |
|
./spec/space_object_spec.rb[1:6:1] | passed | 0.00007 seconds |
|
||||||
|
|||||||
@@ -84,4 +84,17 @@ RSpec.describe Game do
|
|||||||
expect(game.get_captured_planets(player.get_id)).to eq game.instance_variable_get(:@planets)
|
expect(game.get_captured_planets(player.get_id)).to eq game.instance_variable_get(:@planets)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
rerun 'ruby server.rb'
|
||||||
@@ -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 +0,0 @@
|
|||||||
--require spec_helper
|
|
||||||
@@ -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
|
|
||||||
@@ -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
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user