pushing befor major changes
This commit is contained in:
+2
-2
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -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
|
||||
# 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
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
rerun 'ruby server.rb'
|
||||
Reference in New Issue
Block a user