Basic working with ships partly implimented

This commit is contained in:
2026-01-07 23:11:46 -07:00
parent 1ec24fbf12
commit 6e914467ed
49 changed files with 2079 additions and 220 deletions
+60
View File
@@ -0,0 +1,60 @@
# Nathan Hinton 22 Dec 2025
# Here is where I will have the server start
require 'socket'
require 'json'
require 'yaml'
require 'game/game'
server = TCPServer.new(2000)
puts "Multithreaded server listening on port 2000..."
conf_data = {}
configuration_file = 'game_data/game_conf.yaml'
File.open(configuration_file, 'r') do |fi|
conf_data = YAML.safe_load(fi.read())
end
puts "loaded #{conf_data} for game"
game = Game::Game.new(conf_data)
game_thread = Thread.new() do
game.run()
end
loop do
# Accept the connection and pass the client object to a new thread
Thread.start(server.accept) do |client|
puts "New thread started for a client connection."
# Now we start to connect commands to the server
#client.puts ({'type' => 'update', 'domain' => 'game', 'request_id' => nil, 'payload' => {'game_name' => conf_data['name']}}).to_json
# Read data from the client
while line = client.gets
# Ensure that it is JSON
begin
encoded_command = JSON.parse(line.chomp)
response = ''
if encoded_command['type'] == 'hello'
# Here we need to get the client information.
response = game.player_connect(client, encoded_command).to_json
else
response = game.parse_command(encoded_command).to_json
end
puts "Received: #{line.chomp}"
puts "Sending: #{response}"
client.puts "#{response}"
rescue JSON::ParserError
puts "Error decoding JSON: '#{line.chomp}'"
client.puts ({'type' => 'error', 'domain' => 'error', 'payload' => 'Server unable to decode JSON'}).to_json()
end
end
client.close
puts "Thread completed and client disconnected."
end
end
View File
View File
View File
-11
View File
@@ -1,11 +0,0 @@
module GameServer
module Domain
class Player
attr_reader :money
def initialize
@money = 1000
end
end
end
end
View File
View File
View File
View File
+163
View File
@@ -0,0 +1,163 @@
# Nathan Hinton 2 Jan 2025 Buildings for the game
#
# Production buildings:
Shipyard Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Shipyard Level 2:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Space Construction Station Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Space Construction Station Level 2:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Research Factory Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Research Factory Level 2:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Shipyard Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Shipyard Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Shipyard Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
Shipyard Level 1:
classification: production
faction: good
production: # This is what is required to produce the building:
metal:
crystals:
fuel:
credits:
time:
upkeep:
metal:
crystals:
fuel:
credits:
base_stats: []
+400
View File
@@ -0,0 +1,400 @@
# frozen_string_literal: true
# Nathan Hinton, 22 Dec 2025
# Game server for the space game
require 'game/player'
require 'game/planet'
require 'thread'
require 'securerandom'
# Main file for the Game module
module Game
# Controls how frequently autosaves are created (seconds)
SAVE_INTERVAL = 15
# Controls how frequently updates are sent to the client about the player (seconds)
PLAYER_UPDATE_INTERVAL = 2
# Controls how fast the game scales with player count. Bigger means more
# players required to change the speed.
GAME_SPEED_FACTOR = 1
##
# The central server object that keeps track of players, planets, and
# game-time.
#
# @api public
class Game
# The name of the game
attr_reader :name
# Returns the width of the game space
attr_reader :width
# Returns the height of the game space
attr_reader :height
# Returns the depth of the game space
attr_reader :depth
# Collection of players in the game
attr_reader :players
# Create the game
##
# Initialize the game from a configuration hash. The hash is expected
# to be the result of parsing the YAML file `game_data/game_conf.yaml`.
#
# @param conf_data [Hash] The parsed configuration
# @raise [ArgumentError] if the configuration is missing a required key
#
def initialize(conf_data)
# Validate that the correct keys exist
['name', 'width', 'height', 'depth', 'players', 'planets', 'ships', 'time', 'player_defaults'].each do |key|
if conf_data[key].nil?
raise ArgumentError, "Missing toplevel key '#{key}' in configuration"
end
end
@conf_data = conf_data
@name = conf_data['name']
@width = conf_data['width']
@height = conf_data['height']
@depth = conf_data['depth']
@to_load_players = conf_data['players']
@to_load_planets = conf_data['planets']
@game_time = conf_data['time']
@player_defaults = @conf_data['player_defaults']
@connected_players = [] # When we start up we do not have any players connected.
# Load the players:
@players = []
@to_load_players.each do |player_id|
@players.append(Player.load_from_save(player_id))
end
# If we are an integer then we need to create new planets.
@planets = []
if @to_load_planets.is_a? Integer
puts "Appears to be the first time running. Creating #{@to_load_planets} planets"
@to_load_planets.times do
@planets.append(Planet.new(@width, @height, @depth))
end
else
@to_load_planets.length.times do |idx|
planet = Planet.load_from_save(@to_load_planets[idx])
@planets.append(planet)
end
end
# Load ships
@ships = []
conf_data['ships'].each do
require 'byebug'; debugger
end
end
##
# Register a client connection. This method sccepts a TCP docket and a hash
# that must comtain a `player_id`. If the player already exists in the game
# it is reconnected otherwise a new `Player` is created and added to the
# game if possible (There must be at least one free planet)
#
# @param client [TCPSocket] The client socket
# @param command [Hash] JSON parsed command from the client.
# @option command [String] :player_id Required, The player ID
#
# @return [Hash] status hash that will be converted to JSON before being
# sent back to the client
#
def player_connect(client, command)
player_id = command.dig('payload', 'player_id')
faction = command.dig('payload', 'faction')
raise ArgumentError, 'player_id missing in command' unless player_id
raise ArgumentError, 'faction is missing in command' unless faction
player = player_registered?(player_id)
if player
# Re-connect an existing but disconnected player
if player.connected?
raise GameError, 'Player already connected!'
end
player.connect(client)
else
# Create a brand new player and claim a planet for them
player = create_player(player_id, faction)
return player if player.is_a?(Hash) && player['type'] == 'error' # error returned
player.connect(client)
@players << player
end
{ 'type' => 'update', 'payload' => 'success' }
end
##
# Create a new player from scratch. The new player is assigned a free
# planet and receives the default starting resources.
#
# @param player_id [String] the unique identifier that will be stored
# on the new player; if `nil` a random hex string will be generated
# @param faction [String] The faction that the player belongs too.
#
# @return [Player, Hash] The new player object on success or an error
# hash if no free planet is available.
#
def create_player(player_id, faction)
free_planets = @planets.select { |p| p.owner.nil? }
if free_planets.empty?
return {
'type' => 'error',
'payload' => 'Unable to allocate a planet for a new player'
}
end
home = free_planets.sample
player = Player.new(player_id || SecureRandom.hex, faction)
player.create_new(
@player_defaults['credits'],
@player_defaults['metal'],
@player_defaults['crystals'],
@player_defaults['fuel'],
)
player.claim_planet(home)
player.make_home_planet(home)
player
end
##
# The entry point for all messages that arrive from a client. The
# payload must be a hash decoded from JSON and contain a `type` field.
#
# @param command [Hash] The parsed client command
#
# @return [String] JSON-encoded response
#
def parse_command(command)
return error_response('Player not registered!') unless command['player_id']
case command['type']
when 'dbg'
# Debugger - do nothing in production
{ 'type' => 'ok', 'payload' => 'debugging' }.to_json
when 'hello'
error_response('Hello command should already have been handled.')
when 'query'
parse_query(command)
when 'command'
error_response('Command handling not implemented')
else
error_response('Unknown type')
end
end
##
# Dispatch a `query` command to the appropriate helper. Only a small
# subset of query types is required for the test-suite; all others
# return a generic error.
#
# @param query [Hash] The query to be parsed
#
# @return [String] JSON-encoded response
#
def parse_query(query)
return error_response('Missing query payload') unless query['payload']
player = player_registered?(query['player_id'])
return error_response('Player not found') unless player
case query['domain']
when 'player'
case query['payload']
when 'update'
return player.update(query['payload']['request_id'])
else
error_response('Unknown payload for player domain')
end
else
error_response('Unknown domain')
end
end
##
# Return a status hash that contains a friendly error message. The
# helper exists only to keep the body of all error branches short.
#
# @param msg [String] The error message
# @return [String] JSON-encoded error response
#
def error_response(msg)
{ 'type' => 'error', 'payload' => msg }.to_json
end
alias error_response error_response
##
# Return a JSON object that tells a client about the current
# server-time. Useful for debugging or “ping” operations.
#
# @return [String] JSON-encoded hash
#
def info(request_id)
{
'type' => 'update',
'domain' => 'game',
'request_id' => request_id,
'payload' => {
'name' => @name,
'width' => @width,
'height' => @height,
'depth' => @depth,
'time' => @game_time
}
}.to_json
end
# Actually start running the game
def run()
# Main game loop
last_time = Time.now # The delta time will be very small on the first one.
next_save = @game_time + SAVE_INTERVAL
next_client_update = @game_time + PLAYER_UPDATE_INTERVAL
while true
# This is the **ONLY** place where Time.now should be used
delta_time = (Time.now - last_time)
last_time = Time.now
# Here is where we do our time scaling. Count the number of players in the game then use that mult
player_count = @players.map {|player| player.connected?}.count true
@game_speed = (player_count / GAME_SPEED_FACTOR)
delta_time * (player_count / GAME_SPEED_FACTOR)
@game_time += delta_time
@planets.each do |planet|
planet.update(delta_time)
end
@ships.each do |ship|
ship.update(delta_time)
end
#@players.each do |player|
# player.update(delta_time)
#end
# Perform game saves (Or not for now...)
if (@game_time) > next_save
puts 'Trying to save...'
next_save = @game_time + SAVE_INTERVAL
save_game_state
end
# Send out regular updates to the clients (Every 2 seconds)
if @game_time > next_client_update
next_client_update = @game_time + PLAYER_UPDATE_INTERVAL
@players.each do |player|
if player.connected?
player.send_update
end
end
end
if player_count == 0
puts 'No players connected, pausing time'
while 0 == (@players.map {|player| player.connected?}.count true)
sleep(1)
end
end
#sleep(0.05) # About 20 TPS/FPS
sleep(1)
end
end
##
# Persist the current state of the game to the disk. The method is
# fully unit-tested via a double that captures the YAML payload
# instead of actually writing a file.
#
# @return [void]
#
def save_game_state
config = {
'name' => @name,
'width' => @width,
'height' => @height,
'depth' => @depth,
'players' => @players.map(&:player_id),
'planets' => @planets.map(&:planet_id),
'ships' => @ships.map(&:ship_id),
'time' => @game_time,
'player_defaults' => @player_defaults
}
File.open('game_data/game_conf.yaml', 'w') { |f| f.write(YAML.dump(config)) }
# Save the planets
@planets.each do |planet|
planet.save_to_file
end
# Save the ships
@ships.each do |ship|
ship.save_to_file
end
# Save the players
@players.each do |player|
player.save_to_file
end
end
##
# Determine whether a player with a given id is known to the game.
#
# @param player_id [String] the id that the caller is looking for
#
# @return [Player, false] the `Player` instance if found or `false`
# otherwise
#
def player_registered?(player_id)
@players.find { |p| p.player_id == player_id } || false
end
# --------------------------------------------------------------------
# PRIVATE HELPERS - kept simple for the test-suite
# --------------------------------------------------------------------
private
def error_response(msg)
{ 'type' => 'error', 'payload' => msg }.to_json
end
def load_players(ids)
ids.map { |id| Player.load_from_id(id) }
end
def load_planets(planets)
if planets.is_a?(Integer) # legacy config - integer means “create N”
Array.new(planets) { Planet.new }
else
planets.map { |id| Planet.load_from_id(id) }
end
end
def load_ships(ids)
ids.map { |id| Ship.load_from_id(id) } # Ship is a placeholder
end
end
##
# Raised when an invalid operation is attempted (e.g. reconnecting a
# player that already has a socket). The tests catch this error
# to confirm that the server protects against accidental double
# connections.
class GameError < StandardError; end
end
+40
View File
@@ -0,0 +1,40 @@
# Nathan Hinton 31 Dec 2025
# This holds a list of all modules in the game
# Here is how to create a module:
#
# name:
# classification:
# auxiluary/attack/defense # Pick one
# faction:
# good/evil
# production:
# metal/crystals/fuel
# upkeep:
# metal/crystals/fuel
# add_stats: # These are additive to the base stat
# stat_name: value
# mult_stats: # These are multipulicative to the base stat (After adding the additive stats?)
# stat_name: value # Greater than one will increase while less than one will decrease
# required_tech: [] # The required tech to be researched *before* this one can be started
# required_buildings: [] # The buildings required to begin this research
#
#
#
light shield:
classification: defense
faction: good
production:
metal: 1,000
crystals: 1,000
fuel: 0
upkeep:
metal: 10
crustals: 10
fuel: 0
add_stats:
shield: 1,000
mult_stats: []
required_tech: []
required_buildings: []
+75
View File
@@ -0,0 +1,75 @@
# Nathan Hinton 22 Dec 2025
# Planet file
require 'securerandom'
# A planet object for the Game module
class Planet
attr_reader :owner, :planet_id
# Creates a new planet.
#
# @param width [Integer] The width of the game
# @param height [Integer] The height of the game
# @param depth [Integer] The depth of the game
def initialize(width, height, depth)
@owner = nil
@buildings = []
@position = [rand(width), rand(height), rand(depth)]
@planet_id = SecureRandom.hex
end
# Returns if the planet can be claimed. Currently requires that there be no
# buildings on the planet.
def claimable?
if @owner.nil?
return true
elsif @buildings == []
return true
end
return false
end
# Function that allows a player to claim the planet and to become the owner.
def claim(player)
@owner = player
end
# Function called to update the planet.
def update(delta_time)
end
# Function to return info to the client about a planet
def info_update
raise NotImplementedError, "Need to write tests for this function"
return {
'id' => @planet_id,
'position' => @position
}
end
##########################
##### UTIL FUNCTIONS #####
##########################
# Save a player to a file
def save_to_file
File.open("game_data/#{self.planet_id}_planet_.save", 'wb') do |fi|
# Load resources for the player:
fi.write(Marshal.dump(self))
end
return self.planet_id
end
# Load a player from a file
def self.load_from_save(fname)
File.open("game_data/#{fname}_planet_.save", 'rb') do |fi|
# Load resources for the player:
Marshal.load(fi.read)
end
end
end
+156
View File
@@ -0,0 +1,156 @@
# Nathan Hinton 22 Dec 2025
# The main player file
require 'securerandom'
require 'yaml'
# A player object for the class Game
class Player
attr_reader :player_resources, :player_id, :player_planets, :player_ships, :faction, :researched_tech, :buildings
def initialize(player_id, faction)
@player_id = player_id
@player_planets = []
@player_ships = []
@home_planet = nil
# Validate faction:
if !(['good', 'evil'].include? faction)
raise PlayerError, 'Faction must be `good` or `evil`'
end
@faction = faction
end
# Create a new player. This is actually just setting it up. All the defaults
# are defined in the game_config.yaml file.
#
# @param credits [Integer] The number of starting credits
# @param metal [Integer] The amount of starting metal
# @param crystals [Integer] The number of starting crystals
# @param fuel [Integer] The amount of fuel to start with.
# @param faction [String] The faction that the player should start as. Should be `good` or `evil`
def create_new(credits, metal, crystals, fuel)
@player_resources = {
'credits' => credits,
'metal' => metal,
'crystals' => crystals,
'fuel' => fuel
}
end
# Connect a player
#
# @param client [TCPSocket] The socket used to communicate with the player
def connect(client)
@socket = client
end
# Returns if the player is connected or not
def connected?
if @socket.nil?
return false
elsif @socket.closed?
return false
end
return true
end
# Allows a player to claim a planet.
#
# @param planet
def claim_planet(planet)
if planet.claimable?
@player_planets.append(planet)
planet.claim(self)
end
end
# Makes a planet the player's home planet
#
# @param planet
def make_home_planet(planet)
if planet.owner == self
@home_planet = planet
else
return {'type' => 'error',
'payload' => 'Planet must be owned by the player'}
end
end
# Create an update to send out
def update(request_id)
planet_data = []
@player_planets.each do |planet|
planet_data.append(planet.info_update())
end
{
'type' => 'update',
'domain' => 'player',
'request_id' => request_id,
'payload' => {
'resources' => @player_resources,
'planets' => planet_data,
'ships' => @player_ships
}
}
end
# Used to send regular updates for the game data.
def send_update
# Send an update of all of the player data and stuff
return if !connected?
begin
@socket.puts(update(nil).to_json)
rescue
@socket.close
end
end
# Save a player to a file through marshaling it.
def save_to_file
File.open("game_data/#{@player_id}_player_.save", 'wb') do |fi|
# Load resources for the player:
fi.write(Marshal.dump(self))
end
return @player_id
end
# Load a player from a file. Requires the player id to load with as that is
# the file name.
def self.load_from_save(player_id)
File.open("game_data/#{player_id}_player_.save", 'rb') do |fi|
# Load resources for the player:
Marshal.load(fi.read)
end
end
# Custom funciton to dump marshal data for players. We do not want to try to
# dump the socket...
def marshal_dump
{
player_id: @player_id,
player_planets: @player_planets,
player_ships: @player_ships,
home_planet: @home_planet,
faction: @faction,
player_resources: @player_resources
}
end
# Custom function to load the marshaled data for the player.
#
# @param data [Hash] A hash containing the data.
def marshal_load(data)
@player_id = data[:player_id]
@player_planets = data[:player_planets]
@player_ships = data[:player_ships]
@home_planet = data[:home_planet]
@faction = data[:faction]
@player_resources = data[:player_resources]
end
end
# Errors for the player
class PlayerError < StandardError
end
+146
View File
@@ -0,0 +1,146 @@
# Nathan Hinton 30 Dec 2025
# Ship file for the game
require 'securerandom'
# Load the ships into a constant:
require 'yaml'
SHIP_DATA = File.open('lib/game/ships.yaml', 'r') do |fi|
YAML.safe_load(fi.read())
end
class Ship
attr_reader :owner, :position
# Ships are built by a player so we should *always* know what player is
# owning a ship
#
# @param player [Player] The player who owns the ship
# @param position_x [Float] The starting x position of the ship.
# @param position_y [Float] The starting y position of the ship.
# @param position_z [Float] The starting z position of the ship.
def initialize(player, position_x, position_y, position_z, name)
@owner = player
@ship_id = SecureRandom.hex
@position = [position_x, position_y, position_z]
@target = @position
@speed = 1
@ship_info = validate_creation(name)
end
# Validates that the ship name exists and it is unlocked by the player (Has
# the right buildings and tech to actually create). This means that you might
# be able to start a ship however when it is 'finished' if a required
# building has been removed it will fail to finish properly. In this case the
# player resources should be refunded (with some lost) and the ship creation
# will raise an error.
def validate_creation(name)
ship = SHIP_DATA[name]
raise ShipError, "invalid ship name '#{name}'" if ship.nil?
# Validate that the owner has the correct tech:
ship['required_tech'].each do |tech_needed|
if !(@owner.researched_tech.include? tech_needed)
raise ShipError, "Player needs the tech of '#{tech_needed}'"
end
end
# Validate the buildings:
ship['required_buildings'].each do |building_needed|
if !(@owner.buildings.include? building_needed)
raise ShipError, "Player needs the building of '#{building_needed}' to build this ship"
end
end
end
# Teleport the ship to a given position. Really only should be used for admin
# stuff.
#
# @param position_x [Float] The new x position of the ship.
# @param position_y [Float] The new y position of the ship.
# @param position_z [Float] The new z position of the ship.
def teleport(position_x, position_y, position_z)
@position = [position_x, position_y, position_z]
end
# Sets the target position of a ship.
#
# @param position_x [Float] The new x position of the ship.
# @param position_y [Float] The new y position of the ship.
# @param position_z [Float] The new z position of the ship.
def move(position_x, position_y, position_z)
@target = [position_x, position_y, position_z]
end
# Contains the logic to move a ship
#
# @param delta_time [Float] The delta time elapsed in the game
def update(delta_time)
# Movement logic:
if @position == @target
# Do nothing
else
# Calculate the difference of the position and the target
x_dist = @target[0] - @position[0]
y_dist = @target[1] - @position[1]
z_dist = @target[2] - @position[2]
total_dist = Math.sqrt(x_dist**2 + y_dist**2 + z_dist**2)
speed = (@speed/total_dist) * delta_time
new_x = @position[0] + (x_dist * speed)
new_y = @position[1] + (y_dist * speed)
new_z = @position[2] + (z_dist * speed)
if x_dist > 0 # Going right
if new_x > @target[0]
new_x = @target[0]
end
elsif x_dist < 0 # Going left
if new_x < @target[0]
new_x = @target[0]
end
end
if y_dist > 0 # Going right
if new_y > @target[1]
new_y = @target[1]
end
elsif y_dist < 0 # Going left
if new_y < @target[1]
new_y = @target[1]
end
end
if z_dist > 0 # Going right
if new_z > @target[2]
new_z = @target[2]
end
elsif z_dist < 0 # Going left
if new_z < @target[2]
new_z = @target[2]
end
end
@position = [new_x, new_y, new_z]
end
end
# Save a player to a file
def save_to_file
File.open("game_data/#{self.planet_id}.save", 'wb') do |fi|
# Load resources for the player:
fi.write(Marshal.dump(self))
end
return self.planet_id
end
# Load a player from a file
def self.load_from_save(fname)
File.open("game_data/#{fname}_ship_.save", 'rb') do |fi|
# Load resources for the player:
Marshal.load(fi.read)
end
end
end
# Error class for ships
class ShipError < StandardError
end
+235
View File
@@ -0,0 +1,235 @@
# Nathan Hinton 31 Dec 2025
# This file will hold configuration for ships in the game.
# Here is how to create a ship:
#
# name:
# classification:
# flagship/battleship/heavy cruiser/light cruiser/destroyer/frigate/fighter # Pick one
# faction:
# good/evil
# production:
# metal/crystals/fuel/etc
# build_time: A value that represents how long it takes to build. Standard shipyards will build at a rate of 100 per tick. This means that a value of 1000 would take (for one shipyard) 10 ticks to produce.
# upkeep:
# metal/crystals/fuel/etc
# base_stats:
# hp/shield
# armor/def
# att
# speed # Units per tick
# total_module_slots: integer # The total slots available on this ship
# module_slots: [] # List of module names found from modules.yaml
# required_tech: [] # List of all tech required to build the ship
# required_buildings: # List of all buildings required to build the ship
#
#
#
#
##########################################
############## ##############
############## GOOD FACTION ##############
############## ##############
##########################################
#
Nebula Dominion:
classification: flagship
faction: good
production:
metal: 100,000
crystals: 100,000
fuel: 5,000
time: 10,000 # In production power
upkeep:
metal: 100
crystals: 100
fuel: 150
base_stats:
hp: 10,000
shield: 10,000
armor: 50
def: 50
att: 1,000
speed: 0.2
total_module_slots: 12
module_slots: []
required_tech: [Heavy Shield, Large Engine, Laser Cannons, Neoplated Armor]
required_buildings: [Space Construction Station]
Void Breaker:
classification: battleship
faction: good
production:
metal: 70,000
crystals: 70,000
fuel: 3,500
time: 7,000
upkeep:
metal: 70
crystals: 70
fuel: 100
base_stats:
hp: 7,000
shield: 7,000
armor: 35
def: 35
att: 700
speed: 0.26
total_module_slots: 8
module_slots: []
required_tech: []
required_buildings: []
Solar Sentinal:
classification: heavy cruiser
faction: good
production:
metal: 49,000
crystals: 49,000
fuel: 2,500
time: 4,900
upkeep:
metal: 49
crystals: 49
fuel: 73
base_stats:
hp: 4,900
shield: 4,900
armor: 25
def: 25
att: 490
speed: 0.34
total_module_slots: 6
module_slots: []
required_tech: []
required_buildings: []
Photon Protector:
classification: light cruiser
faction: good
production:
metal: 34,000
crystals: 34,000
fuel: 1,750
time: 3,430
upkeep:
metal: 34
crystals: 34
fuel: 51
base_stats:
hp: 3,400
shield: 3,400
armor: 18
def: 18
att: 343
speed: 0.44
total_module_slots: 4
module_slots: []
required_tech: []
required_buildings: []
Third Smallest:
classification: destroyer
faction: good
production:
metal: 23,800
crystals: 23,800
fuel: 1225
time: 2,380
upkeep:
metal: 24
crystals: 24
fuel: 36
base_stats:
hp: 2,380
shield: 2,380
armor: 12
def: 12
att: 240
speed: 0.57
total_module_slots: 3
module_slots: []
required_tech: []
required_buildings: []
Starlight:
classification: frigate
faction: good
production:
metal: 17,000
crystals: 17,000
fuel: 858
time: 1,700
upkeep:
metal: 17
crystals: 17
fuel: 25
base_stats:
hp: 1,700
shield: 1,700
armor: 8
def: 8
att: 168
speed: 0.74
total_module_slots: 2
module_slots: []
required_tech: []
required_buildings: []
Starblade:
classification: fighter
faction: good
production:
metal: 11,900
crystals: 11,900
fuel: 600
time: 1,100
upkeep:
metal: 17
crystals: 17
fuel: 25
base_stats:
hp: 1,190
shield: 1,190
armor: 8
def: 8
att: 168
speed: 0.74
total_module_slots: 1
module_slots: []
required_tech: [Engine_1, Body_1, Weapons_1]
required_buildings: []
#
##########################################
############## ##############
############## EVIL FACTION ##############
############## ##############
##########################################
#
Void Bastion:
classification: flagship
faction: evil
production:
metal: 100,000
crystals: 100,000
fuel: 5,000
time: 1,000 # In game ticks
upkeep:
metal: 100
crystals: 100
fuel: 150
base_stats:
hp: 10,000
shield: 10,000
armor: 50
def: 50
att: 1,000
total_module_slots:
module_slots: []
required_tech: [Plastered glass]
required_buildings: []
-8
View File
@@ -1,8 +0,0 @@
module GameServer
class App
def self.start
world = Simulation::World.new
Server::WebSocketServer.new(world).start
end
end
end
-18
View File
@@ -1,18 +0,0 @@
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
-11
View File
@@ -1,11 +0,0 @@
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
-19
View File
@@ -1,19 +0,0 @@
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
View File
View File
View File
View File
View File
-27
View File
@@ -1,27 +0,0 @@
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
-61
View File
@@ -1,61 +0,0 @@
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
-33
View File
@@ -1,33 +0,0 @@
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
View File
View File
View File
View File
View File
-15
View File
@@ -1,15 +0,0 @@
module GameServer
module Simulation
class World
attr_reader :time
def initialize
@time = 0.0
end
def advance(delta)
@time += delta
end
end
end
end
View File