Working on major restructuring
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
gem sinatra
|
||||
gem sinatra-cross_origin
|
||||
gem yard-sinatra
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "async"
|
||||
gem "async-websocket"
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env ruby
|
||||
require_relative "../config/environment"
|
||||
|
||||
GameServer::App.start
|
||||
@@ -0,0 +1,7 @@
|
||||
require "bundler/setup"
|
||||
Bundler.require
|
||||
|
||||
require_relative "settings"
|
||||
require_relative "logging"
|
||||
|
||||
Dir[File.join(__dir__, "../lib/**/*.rb")].sort.each { |f| require f }
|
||||
@@ -0,0 +1,3 @@
|
||||
require_relative "../lib/logging/logger"
|
||||
|
||||
GameServer::Logging.setup
|
||||
@@ -0,0 +1,5 @@
|
||||
module Settings
|
||||
PORT = 8080
|
||||
GAME_SPEED = 1.0
|
||||
SIMULATION_STEP = 0.05
|
||||
end
|
||||
@@ -1 +0,0 @@
|
||||
--require spec_helper
|
||||
@@ -1,34 +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)
|
||||
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 --plugin yard-sinatra *.rb'
|
||||
sh 'yard stats --plugin yard-sinatra *.rb --list-undoc'
|
||||
end
|
||||
|
||||
desc 'Create code coverage'
|
||||
task :coverage do
|
||||
Rake::Task[:test].invoke
|
||||
end
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
|
||||
|
||||
require './space_object'
|
||||
|
||||
|
||||
# This class is for buildings that are built on a planet. Currently it
|
||||
# inherrits the SpaceObject class however it should probably inherrit a class
|
||||
# that is for planets.
|
||||
class Building<SpaceObject
|
||||
attr_reader :name, :type
|
||||
|
||||
# Create a new building object. You can pass in a name and a type.
|
||||
#
|
||||
# @param player [Player] The player who owns this building
|
||||
# @param name [String] The name of the building
|
||||
# @param type [String] The type of the building
|
||||
def initialize(player, planet, name: 'default name', type: 'Central Post')
|
||||
super()
|
||||
# Valiate type:
|
||||
if not get_config['buildings'].include? type
|
||||
raise BuildingError, "Invalid building type"
|
||||
end
|
||||
@player = player
|
||||
@planet = planet
|
||||
@name = name
|
||||
@type = type
|
||||
end
|
||||
|
||||
# Override the superclass tick method. This will add the resources that ar
|
||||
# produced to the player and the planet that the building belongs to.
|
||||
def tick
|
||||
get_config['buildings'][@type]['produces'].each do |resource, value|
|
||||
# Send some to the player (Global resoures)
|
||||
if ['metal', 'credits', 'crystals'].include? resource
|
||||
@player.resources[resource] += value
|
||||
else
|
||||
@planet.resources[resource] += value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Error class for the buildings
|
||||
class BuildingError < StandardError
|
||||
end
|
||||
@@ -1,183 +0,0 @@
|
||||
# Configuration file for the game:
|
||||
|
||||
planets:
|
||||
production:
|
||||
metal: 10
|
||||
credits: 10
|
||||
crystals: 10
|
||||
|
||||
buildings:
|
||||
Central Post:
|
||||
description:
|
||||
"The Central Post allows you to build other buildings on the planet"
|
||||
cost:
|
||||
metal: 100
|
||||
credits: 100
|
||||
crystals: 100
|
||||
produces:
|
||||
metal: 10
|
||||
credits: 10
|
||||
crystals: 10
|
||||
|
||||
##### ENERGY PRODUCTION #####
|
||||
|
||||
Nuclear Plant:
|
||||
description:
|
||||
"The Nuclear Plant is expensive however it will produce a constant and large amount of energy"
|
||||
cost:
|
||||
metal: 500
|
||||
credits: 500
|
||||
crystals: 500
|
||||
produces:
|
||||
energy: 1000
|
||||
|
||||
Wind Farm:
|
||||
description:
|
||||
"A Wind Farm will produce a variable amount of energy depending on the surface of the planet."
|
||||
cost:
|
||||
metal: 200
|
||||
credits: 200
|
||||
crystals: 200
|
||||
produces:
|
||||
energy: 200
|
||||
|
||||
Solar Farm:
|
||||
description:
|
||||
"A Solar Farm will produce a variable amount of energy depending on the planet environment"
|
||||
cost:
|
||||
metal: 200
|
||||
credits: 200
|
||||
crystals: 200
|
||||
produces:
|
||||
energy: 200
|
||||
|
||||
##### RESEARCH BUILDINGS #####
|
||||
|
||||
Civil Research:
|
||||
description:
|
||||
"Proviced access to research economic and morale upgrades"
|
||||
cost:
|
||||
metal: 1000
|
||||
credits: 1000
|
||||
crystals: 1000
|
||||
produces:
|
||||
civ research: 1000 # This is a *global* resource
|
||||
|
||||
Space Research:
|
||||
description:
|
||||
"Provides access to sensor/module and engine upgrades"
|
||||
cost:
|
||||
metal: 1000
|
||||
credits: 1000
|
||||
crystals: 1000
|
||||
produces:
|
||||
spa research: 1000 # This is a *global* resource
|
||||
|
||||
Military Research:
|
||||
description:
|
||||
"Provides access to weapon and shield upgrades"
|
||||
cost:
|
||||
metal: 1000
|
||||
credits: 1000
|
||||
crystals: 1000
|
||||
produces:
|
||||
mil research: 1000 # This is a *global* resource
|
||||
|
||||
Shipping Research:
|
||||
description:
|
||||
"Provides access to ship and efficiency upgrades"
|
||||
cost:
|
||||
metal: 1000
|
||||
credits: 1000
|
||||
crystals: 1000
|
||||
produces:
|
||||
shi research: 1000 # This is a *global* resource
|
||||
|
||||
##### RESOURCE PRODUCTION BUILDINGS #####
|
||||
|
||||
Refinery:
|
||||
description:
|
||||
"Extracts fuel from the ground. Most effective on liquid type planets."
|
||||
cost:
|
||||
metal: 150
|
||||
credits: 150
|
||||
crystals: 150
|
||||
energy: 100 # This is energy per tick
|
||||
produces:
|
||||
fuel: 20 # This is a *global* resource
|
||||
|
||||
Mine:
|
||||
description:
|
||||
"Extracts metal from the ground. Most effective on"
|
||||
cost:
|
||||
metal: 150
|
||||
credits: 150
|
||||
crystals: 150
|
||||
energy: 100 # This is energy per tick
|
||||
produces:
|
||||
metal: 20 # This is a *global* resource
|
||||
|
||||
Harvester:
|
||||
description:
|
||||
"Extracts crystas from the surface. Most effective on"
|
||||
cost:
|
||||
metal: 150
|
||||
credits: 150
|
||||
crystals: 150
|
||||
energy: 100 # This is energy per tick
|
||||
produces:
|
||||
crystals: 20 # This is a *global* resource
|
||||
|
||||
Trade Center:
|
||||
description:
|
||||
"Increases the number of credits earned on this planet. Most effective on"
|
||||
cost:
|
||||
metal: 150
|
||||
credits: 150
|
||||
crystals: 150
|
||||
energy: 100 # This is energy per tick
|
||||
produces:
|
||||
credits: 20 # This is a *global* resource
|
||||
|
||||
Farm:
|
||||
description:
|
||||
"Produces food on this planet. Required to grow the population"
|
||||
cost:
|
||||
metal: 150
|
||||
credits: 150
|
||||
crystals: 150
|
||||
energy: 100 # This is energy per tick
|
||||
produces:
|
||||
food: 1000
|
||||
|
||||
##### STUFF PRODUCTION #####
|
||||
|
||||
Space Port:
|
||||
description:
|
||||
"Produces space ships"
|
||||
cost:
|
||||
metal: 500
|
||||
credits: 500
|
||||
crystals: 500
|
||||
produces:
|
||||
ship building power: 1000
|
||||
|
||||
Manufacturing Plant:
|
||||
description:
|
||||
"Produces modules and engines for ships"
|
||||
cost:
|
||||
metal: 500
|
||||
credits: 500
|
||||
crystals: 500
|
||||
produces:
|
||||
manufacturing power: 1000
|
||||
|
||||
Military Production:
|
||||
description:
|
||||
"Produces weapons and shields"
|
||||
cost:
|
||||
metal: 500
|
||||
credits: 500
|
||||
crystals: 500
|
||||
produces:
|
||||
military power: 1000
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
|
||||
|
||||
require 'securerandom'
|
||||
|
||||
require './planet'
|
||||
|
||||
# This class holds a game and should be the top level object.
|
||||
class Game
|
||||
attr_reader :players, :width, :height, :planets, :game_id
|
||||
# Create the game
|
||||
def initialize(width: 0, height: 0, planets: 0, game_id: nil, name: 'default name')
|
||||
@game_id = game_id || SecureRandom.hex
|
||||
@game_name = name
|
||||
@players = []
|
||||
@planets = []
|
||||
planets.times do |idx|
|
||||
planet = Planet.new()
|
||||
# Move to a random position
|
||||
position = [rand(width), rand(height), 0]
|
||||
# Ensure that the planets position is random
|
||||
while @planets.map{ |pl| pl.position}.include? position
|
||||
position = [rand(width), rand(height), 0]
|
||||
end
|
||||
planet.move(position)
|
||||
# Add to the list:
|
||||
@planets.append(planet)
|
||||
end
|
||||
@width = width
|
||||
@height = height
|
||||
end
|
||||
|
||||
# Add a player to the game
|
||||
#
|
||||
# @param player [Player] The player to add to the game
|
||||
def add_player(player)
|
||||
# Check if there is a planet free for the player:
|
||||
avail_planets = []
|
||||
@planets.each do |planet|
|
||||
avail_planets.append(planet) if planet.owner.nil?
|
||||
end
|
||||
if avail_planets == []
|
||||
raise GameError, "No available planets to add player to"
|
||||
end
|
||||
planet = avail_planets.sample
|
||||
|
||||
# Add the player to the game
|
||||
@players.append(player)
|
||||
planet.capture(player)
|
||||
player.home_planet = planet
|
||||
end
|
||||
|
||||
# Remove a player from the game
|
||||
#
|
||||
# @param player [Player] The player to be removed
|
||||
def remove_player(player)
|
||||
idx = @players.index(player)
|
||||
if idx.nil?
|
||||
# Raise an error that the player could not be found
|
||||
raise GameError, 'Player not found to remove!'
|
||||
end
|
||||
@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.
|
||||
#
|
||||
# @param id [String] The id of a player
|
||||
# @return Player The player matching the id
|
||||
def check_id(id)
|
||||
@players.each do |player|
|
||||
if player.id == id
|
||||
return player
|
||||
end
|
||||
end
|
||||
raise GameError, 'Invalid player id!'
|
||||
end
|
||||
|
||||
# Gets the planets for a player
|
||||
#
|
||||
# @param id [String] Players id
|
||||
# @return [Array<Planet>] List of planets captured by that player
|
||||
def get_captured_planets(id)
|
||||
player = check_id(id)
|
||||
result = []
|
||||
@planets.each do |planet|
|
||||
result.append(planet) if planet.owner == player
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
# Returns a string representation of the map
|
||||
# Begin no coverage
|
||||
# :nocov:
|
||||
def to_s()
|
||||
# Create char map:
|
||||
game_map = []
|
||||
@height.times do
|
||||
tmp = []
|
||||
@width.times do
|
||||
tmp.append('')
|
||||
end
|
||||
game_map.append(tmp)
|
||||
end
|
||||
|
||||
# Add planets to the map:
|
||||
@planets.each do |planet|
|
||||
pos_x, pos_y, pos_z = planet.position
|
||||
puts("x: #{pos_x}, y: #{pos_y}")
|
||||
game_map[pos_y][pos_x] += 'p'
|
||||
end
|
||||
|
||||
require 'pp'
|
||||
pp(game_map)
|
||||
game_map.each do |row|
|
||||
row.each do |col|
|
||||
if col == ''
|
||||
print(' ')
|
||||
else
|
||||
print(col)
|
||||
end
|
||||
end
|
||||
print("\n")
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
# End no coverage
|
||||
|
||||
end
|
||||
|
||||
# General game errors
|
||||
class GameError<StandardError
|
||||
end
|
||||
@@ -1,119 +0,0 @@
|
||||
# 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
|
||||
@@ -1,63 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
# Holds a planet and planet information
|
||||
|
||||
require './space_object'
|
||||
|
||||
# Class for planets.
|
||||
class Planet < SpaceObject
|
||||
attr_reader :owner, :resources
|
||||
|
||||
# Create a planet in the game
|
||||
def initialize()
|
||||
super()
|
||||
@buildings = []
|
||||
@owner = nil
|
||||
@resources = {
|
||||
'food' => 0,
|
||||
'energy' => 0,
|
||||
'people' => 10
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
# Any clean up that the planet will need
|
||||
def delete()
|
||||
end
|
||||
|
||||
# Build a building on the planet
|
||||
#
|
||||
# @param building [Building] The building to be built
|
||||
def building_build(building)
|
||||
@buildings.append(building)
|
||||
end
|
||||
|
||||
# Remove building from the planet
|
||||
#
|
||||
# @param building [Building] The building to be destroyed
|
||||
def building_destroy(building)
|
||||
index = @buildings.index(building)
|
||||
if index == nil
|
||||
raise PlanetError, 'Building not found to delete'
|
||||
end
|
||||
@buildings.delete_at(index)
|
||||
end
|
||||
|
||||
# Capture a planet for a player
|
||||
#
|
||||
# @param player [Player] The player who is capturing the planet
|
||||
def capture(player)
|
||||
@buildings = [] # Destroy all the buildings on the planet
|
||||
@owner = player
|
||||
end
|
||||
|
||||
# Tick for the planet
|
||||
def tick()
|
||||
@buildings.each do |building|
|
||||
building.tick
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Generic error for plannets
|
||||
class PlanetError < StandardError
|
||||
end
|
||||
@@ -1,43 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
|
||||
|
||||
require 'securerandom'
|
||||
|
||||
# This defines the player class
|
||||
class Player
|
||||
attr_reader :name, :resources, :id
|
||||
|
||||
attr_accessor :home_planet
|
||||
|
||||
# initialize a player.
|
||||
#
|
||||
# @param name [String] The name of the player
|
||||
def initialize(name: 'test player')
|
||||
@name = name
|
||||
@planets = []
|
||||
@fleets = []
|
||||
@tech = []
|
||||
@resources = {
|
||||
'metal' => 0,
|
||||
'crystals' => 0,
|
||||
'credits' => 0
|
||||
}
|
||||
# This id will be used to validate the player in the game
|
||||
@id = SecureRandom.hex()
|
||||
@id_retrieved = false
|
||||
|
||||
@home_planet = nil
|
||||
end
|
||||
|
||||
# Returns the player ID. This *should* only be called once. When called again
|
||||
# it should print a warning to the log.
|
||||
#
|
||||
# @return [String] The ID of the player
|
||||
def get_id
|
||||
if @id_retrieved
|
||||
# Print a warning message!
|
||||
end
|
||||
@id_retrieved = true
|
||||
return @id
|
||||
end
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
# Core of game
|
||||
|
||||
This will have the classes and a base loop however it will not do anything more
|
||||
than just store stuff I think.
|
||||
|
||||
For now only the x and y cords are used.
|
||||
-175
@@ -1,175 +0,0 @@
|
||||
# Nathan Hinton 20 Dec 2025
|
||||
# The main game server. This will handle the clients and the game in one go
|
||||
|
||||
require 'async'
|
||||
require 'async/websocket'
|
||||
require 'json'
|
||||
require 'yaml'
|
||||
|
||||
require './game'
|
||||
|
||||
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
|
||||
@@ -1,47 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
|
||||
|
||||
require 'yaml'
|
||||
|
||||
# Base class for any object in space. This can be a planet, asteroid, ship etc.
|
||||
class SpaceObject
|
||||
attr_reader :position
|
||||
|
||||
File.open('./config.yaml', 'r') do |fi|
|
||||
@@config = YAML.safe_load(fi.read())
|
||||
end
|
||||
|
||||
# Create the object
|
||||
#
|
||||
def initialize()
|
||||
@position = [0, 0, 0]
|
||||
end
|
||||
|
||||
# Delete the object. This should be overwritten by the sub objects. For
|
||||
# example when an asteroid is destroyed it might want to explode and damage
|
||||
# anything nearby first before being removed from the game. This is the path
|
||||
# that will be used for that.
|
||||
def delete()
|
||||
end
|
||||
|
||||
# Move the object to a new position. Not everything should be movable so this
|
||||
# can be disallowed by sub classes.
|
||||
#
|
||||
# @param position [Array<Integer>] The position that the object should be
|
||||
# moved to.
|
||||
def move(position)
|
||||
@position = position
|
||||
end
|
||||
|
||||
# This *should* be overridden by any object that wants to do anyting. For
|
||||
# example a ship probaly would like to move when the game tick happens.
|
||||
def tick()
|
||||
end
|
||||
|
||||
# Define a method to get the config
|
||||
#
|
||||
# @return [Hash] A hash containing the configuration
|
||||
def get_config()
|
||||
return @@config
|
||||
end
|
||||
end
|
||||
@@ -1,74 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
|
||||
# Defines tests for the buildings
|
||||
|
||||
|
||||
require './building'
|
||||
require './player'
|
||||
require './planet'
|
||||
require './space_object'
|
||||
|
||||
RSpec.describe Building do
|
||||
describe '#initialize' do
|
||||
it 'Initializes to defaults' do
|
||||
building = Building.new(Player.new(), Planet.new())
|
||||
expect(building.name).to eq 'default name'
|
||||
expect(building.type).to eq 'Central Post'
|
||||
end
|
||||
|
||||
it 'initializes with custom values' do
|
||||
building = Building.new(Player.new(), Planet.new(), name: 'nathan house', type: 'Solar Farm')
|
||||
expect(building.name).to eq 'nathan house'
|
||||
expect(building.type).to eq 'Solar Farm'
|
||||
end
|
||||
|
||||
it 'will raise error when invalid building type' do
|
||||
expect{Building.new(Player.new(), Planet.new(), name: 'error', type: 'error')}.to raise_error BuildingError, "Invalid building type"
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tick' do
|
||||
it 'Adds resources to the player' do
|
||||
player = Player.new()
|
||||
planet = Planet.new()
|
||||
type = 'Central Post'
|
||||
building = Building.new(player, planet, type: type)
|
||||
config = SpaceObject.new().get_config
|
||||
metal = config['buildings'][type]['produces']['metal']
|
||||
credits = config['buildings'][type]['produces']['credits']
|
||||
crystals = config['buildings'][type]['produces']['crystals']
|
||||
expect(player.resources).to eq({'metal' => 0, 'crystals' => 0, 'credits' => 0})
|
||||
expect{building.tick}.to_not raise_error
|
||||
expect(player.resources).to eq({
|
||||
'metal' => metal,
|
||||
'crystals' => crystals,
|
||||
'credits' => credits
|
||||
})
|
||||
expect(planet.resources).to eq({
|
||||
'food' => 0,
|
||||
'energy' => 0,
|
||||
'people' => 10
|
||||
})
|
||||
end
|
||||
|
||||
it 'Adds resources to the planet' do
|
||||
player = Player.new()
|
||||
planet = Planet.new()
|
||||
type = 'Nuclear Plant'
|
||||
building = Building.new(player, planet, type: type)
|
||||
config = SpaceObject.new().get_config
|
||||
expect(player.resources).to eq({'metal' => 0, 'crystals' => 0, 'credits' => 0})
|
||||
expect{building.tick}.to_not raise_error
|
||||
expect(player.resources).to eq({
|
||||
'metal' => 0,
|
||||
'crystals' => 0,
|
||||
'credits' => 0
|
||||
})
|
||||
expect(planet.resources).to eq({
|
||||
'food' => 0,
|
||||
'energy' => 1000,
|
||||
'people' => 10
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,36 +0,0 @@
|
||||
example_id | status | run_time |
|
||||
---------------------------------- | ------ | --------------- |
|
||||
./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.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 |
|
||||
@@ -1,100 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
|
||||
|
||||
require './game'
|
||||
require './player'
|
||||
|
||||
RSpec.describe Game do
|
||||
describe '#initialize' do
|
||||
it 'Creates an empty list of players' do
|
||||
expect(Game.new().players).to eq []
|
||||
end
|
||||
|
||||
it 'sets the width when passed in' do
|
||||
expect(Game.new(width: 10).width).to eq 10
|
||||
end
|
||||
|
||||
it 'sets the height when passed in' do
|
||||
expect(Game.new(height: 11).height).to eq 11
|
||||
end
|
||||
|
||||
it 'creates the specified number of planets' do
|
||||
expect(Game.new(planets: 5).planets.length).to eq 5
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#add_player' do
|
||||
it 'adds a player to the game' do
|
||||
game = Game.new(planets: 1)
|
||||
player = Player.new()
|
||||
game.add_player(player)
|
||||
expect(game.players).to eq [player]
|
||||
end
|
||||
|
||||
it 'fails when there are no planets available for the player' do
|
||||
game = Game.new(planets: 0)
|
||||
player = Player.new()
|
||||
expect{game.add_player(player)}.to raise_error GameError, "No available planets to add player to"
|
||||
expect(game.players).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#remove_player' do
|
||||
it 'removes a player from the game' do
|
||||
game = Game.new(planets: 1)
|
||||
player = Player.new()
|
||||
game.add_player(player)
|
||||
expect(game.players).to eq [player]
|
||||
expect(game.remove_player(player)).to eq player
|
||||
expect(game.players).to eq []
|
||||
end
|
||||
|
||||
it 'returns an error when the player is not in the game' do
|
||||
game = Game.new(planets: 1)
|
||||
player = Player.new()
|
||||
game.add_player(player)
|
||||
player2 = Player.new()
|
||||
expect{game.remove_player(player2)}.to raise_error GameError, 'Player not found to remove!'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#check_id' do
|
||||
it 'returns true when the player id is valid for a player' do
|
||||
game = Game.new(planets: 1)
|
||||
player = Player.new()
|
||||
game.add_player(player)
|
||||
expect(game.check_id(player.get_id)).to eq player
|
||||
end
|
||||
|
||||
it 'returns false when the player id does not match a player' do
|
||||
game = Game.new(planets: 1)
|
||||
player = Player.new()
|
||||
game.add_player(player)
|
||||
expect{game.check_id('a')}.to raise_error GameError, 'Invalid player id!'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_captured_planets' do
|
||||
it 'returns the captured planets of the player' do
|
||||
game = Game.new(planets: 1)
|
||||
player = Player.new()
|
||||
game.add_player(player)
|
||||
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
|
||||
@@ -1,95 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
#
|
||||
# Defines tests for the planets
|
||||
|
||||
|
||||
require './planet'
|
||||
require './building'
|
||||
|
||||
RSpec.describe Planet do
|
||||
describe '#initialize' do
|
||||
it 'Starts in a position' do
|
||||
expect(Planet.new().instance_variable_get(:@position)).to eq [0, 0, 0]
|
||||
end
|
||||
|
||||
it 'Creates a buildings list' do
|
||||
expect(Planet.new().instance_variable_get(:@buildings)).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#delete' do
|
||||
end
|
||||
|
||||
|
||||
describe '#building_build' do
|
||||
it 'Adds a building to the planet building list' do
|
||||
planet = Planet.new()
|
||||
test_building = Building.new(Player.new(), planet)
|
||||
expect{planet.building_build(test_building)}.to_not raise_error
|
||||
expect(planet.instance_variable_get(:@buildings)).to eq [test_building]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#building_destroy' do
|
||||
it 'removes a building from the list' do
|
||||
planet = Planet.new()
|
||||
test_building = Building.new(Player.new(), planet)
|
||||
planet.building_build(test_building)
|
||||
expect(planet.instance_variable_get(:@buildings)).to eq [test_building]
|
||||
expect(planet.building_destroy(test_building)).to eq test_building
|
||||
expect(planet.instance_variable_get(:@buildings)).to eq []
|
||||
end
|
||||
|
||||
it 'raises an error if the building can not be removed' do
|
||||
planet = Planet.new()
|
||||
test_building = Building.new(Player.new(), planet)
|
||||
planet.building_build(test_building)
|
||||
test_building2 = Building.new(Player.new(), planet)
|
||||
expect{planet.building_destroy(test_building2)}.to raise_error PlanetError, 'Building not found to delete'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#capture' do
|
||||
it 'sets the planet owner' do
|
||||
planet = Planet.new()
|
||||
player = Player.new(name: 'test0')
|
||||
expect(planet.capture(player)).to be player
|
||||
expect(planet.instance_variable_get(:@owner)).to be player
|
||||
end
|
||||
|
||||
it 'removes all buildings from the planet' do
|
||||
planet = Planet.new()
|
||||
player = Player.new(name: 'test0')
|
||||
planet.capture(player)
|
||||
test_building = Building.new(player, planet)
|
||||
planet.building_build(test_building)
|
||||
|
||||
player1 = Player.new(name: 'test1')
|
||||
expect(planet.instance_variable_get(:@buildings)).to eq [test_building]
|
||||
expect(planet.capture(player1)).to be player1
|
||||
expect(planet.instance_variable_get(:@owner)).to be player1
|
||||
expect(planet.instance_variable_get(:@buildings)).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#tick' do
|
||||
it 'Sends the tick event to all buildings on the planet' do
|
||||
planet = Planet.new()
|
||||
test_building1 = Building.new(Player.new(), planet)
|
||||
test_building2 = Building.new(Player.new(), planet)
|
||||
test_building3 = Building.new(Player.new(), planet)
|
||||
planet.building_build(test_building1)
|
||||
planet.building_build(test_building2)
|
||||
planet.building_build(test_building3)
|
||||
|
||||
expect(test_building1).to receive(:tick)
|
||||
expect(test_building2).to receive(:tick)
|
||||
expect(test_building3).to receive(:tick)
|
||||
expect(planet.tick).to eq planet.instance_variable_get(:@buildings)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
# Test file for the player class.
|
||||
|
||||
|
||||
require 'securerandom'
|
||||
|
||||
require './player'
|
||||
|
||||
RSpec.describe Player do
|
||||
# Set the testing id
|
||||
let(:id) { 'cfeab1a60864fd529cb9ce993e9c3af4' }
|
||||
|
||||
before(:each) do
|
||||
# Make sure that we get the same ID every time.
|
||||
allow(SecureRandom).to receive(:hex).and_return(id)
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
it 'initializes with name' do
|
||||
expect(Player.new(name: 'Nathan').name).to eq 'Nathan'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_id' do
|
||||
it 'returns the id' do
|
||||
player = Player.new()
|
||||
expect(player.instance_variable_get(:@id_retrieved)).to be false
|
||||
expect(player.get_id).to eq id
|
||||
expect(player.instance_variable_get(:@id_retrieved)).to be true
|
||||
end
|
||||
|
||||
it 'prints a warning when the id is gotten more than once' do
|
||||
player = Player.new()
|
||||
player.get_id
|
||||
expect(player.instance_variable_get(:@id_retrieved)).to be true
|
||||
expect(player.get_id).to eq id
|
||||
expect(player.instance_variable_get(:@id_retrieved)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#planets' do
|
||||
end
|
||||
|
||||
|
||||
describe '#fleets', pending: 'Ships not yet implimented' do
|
||||
end
|
||||
|
||||
|
||||
describe '#tech', pending: 'Tech not yet implimented' do
|
||||
end
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
|
||||
|
||||
require './player'
|
||||
require './game'
|
||||
|
||||
|
||||
# This tests a simple game.
|
||||
RSpec.describe 'Simple game', :sim => true do
|
||||
it 'Initalize a new game and add a player' do
|
||||
# Setup the player
|
||||
player = Player.new()
|
||||
id = player.get_id
|
||||
expect(player.resources).to eq({'metal' => 0, 'crystals' => 0, 'galaxy credits' => 0})
|
||||
|
||||
# Setup the start planet
|
||||
game = Game.new(width: 5, height: 5, planets: 5)
|
||||
expect(game.planets.length).to eq 5
|
||||
game.add_player(player)
|
||||
expect(game.get_captured_planets(id).length).to eq 1
|
||||
puts(game)
|
||||
end
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
# Nathan Hinton 19 Dec 2025
|
||||
#
|
||||
# Defines tests for the space object class
|
||||
|
||||
|
||||
require './space_object'
|
||||
|
||||
RSpec.describe SpaceObject do
|
||||
describe '#initialize' do
|
||||
it 'works with no args' do
|
||||
expect(SpaceObject.new().instance_variable_get(:@position)).to eq [0, 0, 0]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#delete' do
|
||||
end
|
||||
|
||||
|
||||
describe '#move' do
|
||||
let(:so) { SpaceObject.new() }
|
||||
it 'changes the objects position and returns the new position' do
|
||||
expect(so.instance_variable_get(:@position)).to eq [0, 0, 0]
|
||||
expect(so.move([2, 3, 4])).to eq [2, 3, 4]
|
||||
expect(so.instance_variable_get(:@position)).to eq [2, 3, 4]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#position' do
|
||||
let(:so) { SpaceObject.new() }
|
||||
it 'returns the current position' do
|
||||
so.move([1, 2, 3])
|
||||
expect(so.position).to eq [1, 2, 3]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#tick' do
|
||||
end
|
||||
|
||||
describe '#get_config' do
|
||||
it 'returns the correct keys' do
|
||||
expect(SpaceObject.new().get_config.keys).to eq ['planets', 'buildings']
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,109 +0,0 @@
|
||||
require 'simplecov'
|
||||
|
||||
SimpleCov.configure do
|
||||
coverage_dir 'coverage'
|
||||
|
||||
# Start SimpleCov
|
||||
SimpleCov.start
|
||||
end
|
||||
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# Exclude by default the simulate tests.
|
||||
config.filter_run_excluding :sim => true
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
rerun 'ruby server.rb'
|
||||
@@ -0,0 +1,11 @@
|
||||
module GameServer
|
||||
module Domain
|
||||
class Player
|
||||
attr_reader :money
|
||||
|
||||
def initialize
|
||||
@money = 1000
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
module GameServer
|
||||
class App
|
||||
def self.start
|
||||
world = Simulation::World.new
|
||||
Server::WebSocketServer.new(world).start
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
module GameServer
|
||||
module Logging
|
||||
module Context
|
||||
def self.with(data)
|
||||
old = Thread.current[:log_ctx]
|
||||
Thread.current[:log_ctx] = data
|
||||
yield
|
||||
ensure
|
||||
Thread.current[:log_ctx] = old
|
||||
end
|
||||
|
||||
def self.current
|
||||
ctx = Thread.current[:log_ctx]
|
||||
ctx ? ctx.map { |k,v| "#{k}=#{v} " }.join : ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
require "time"
|
||||
|
||||
module GameServer
|
||||
module Logging
|
||||
class Formatter
|
||||
def call(severity, time, _, msg)
|
||||
"[#{time.utc.iso8601}] #{severity} #{Context.current}#{msg}\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
require "logger"
|
||||
|
||||
module GameServer
|
||||
module Logging
|
||||
def self.setup
|
||||
FileUtils.mkdir_p("log")
|
||||
file = File.open("log/development.log", "a")
|
||||
file.sync = true
|
||||
|
||||
@logger = Logger.new(file)
|
||||
@logger.level = Logger::DEBUG
|
||||
@logger.formatter = Formatter.new
|
||||
end
|
||||
|
||||
def self.logger
|
||||
@logger
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
require "json"
|
||||
|
||||
module GameServer
|
||||
module Server
|
||||
class Connection
|
||||
def initialize(ws, world)
|
||||
@ws = ws
|
||||
@world = world
|
||||
@router = MessageRouter.new(world)
|
||||
end
|
||||
|
||||
def run
|
||||
Logging.logger.info "connection opened"
|
||||
|
||||
while msg = @ws.read
|
||||
data = JSON.parse(msg)
|
||||
response = @router.handle(data)
|
||||
@ws.write(JSON.dump(response)) if response
|
||||
end
|
||||
rescue => e
|
||||
Logging.logger.error e.message
|
||||
ensure
|
||||
Logging.logger.info "connection closed"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
module GameServer
|
||||
module Server
|
||||
class MessageRouter
|
||||
def initialize(world)
|
||||
@world = world
|
||||
end
|
||||
|
||||
def handle(data)
|
||||
case data["type"]
|
||||
when "hello"
|
||||
hello
|
||||
when "query"
|
||||
query(data)
|
||||
when "command"
|
||||
command(data)
|
||||
else
|
||||
error("UNKNOWN_TYPE")
|
||||
end
|
||||
end
|
||||
|
||||
def hello
|
||||
{
|
||||
type: "update",
|
||||
domain: "world",
|
||||
payload: {
|
||||
time: @world.time,
|
||||
game_speed: Settings::GAME_SPEED
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def query(data)
|
||||
if data["domain"] == "player"
|
||||
{
|
||||
type: "update",
|
||||
domain: "player",
|
||||
payload: { money: 1000 }
|
||||
}
|
||||
else
|
||||
error("UNKNOWN_DOMAIN")
|
||||
end
|
||||
end
|
||||
|
||||
def command(data)
|
||||
{
|
||||
type: "update",
|
||||
domain: "command",
|
||||
payload: { status: "accepted" }
|
||||
}
|
||||
end
|
||||
|
||||
def error(code)
|
||||
{
|
||||
type: "error",
|
||||
domain: "system",
|
||||
payload: { code: code }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
require "async"
|
||||
require "async/websocket"
|
||||
|
||||
module GameServer
|
||||
module Server
|
||||
class WebSocketServer
|
||||
def initialize(world)
|
||||
@world = world
|
||||
end
|
||||
|
||||
def start
|
||||
Async do |task|
|
||||
task.async { simulation_loop }
|
||||
|
||||
Async::WebSocket::Server.open("0.0.0.0", Settings::PORT) do |ws|
|
||||
task.async { Connection.new(ws, @world).run }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def simulation_loop
|
||||
last = Time.now
|
||||
loop do
|
||||
now = Time.now
|
||||
delta = (now - last) * Settings::GAME_SPEED
|
||||
@world.advance(delta)
|
||||
last = now
|
||||
sleep Settings::SIMULATION_STEP
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
module GameServer
|
||||
module Simulation
|
||||
class World
|
||||
attr_reader :time
|
||||
|
||||
def initialize
|
||||
@time = 0.0
|
||||
end
|
||||
|
||||
def advance(delta)
|
||||
@time += delta
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user