From f4c5ba1f1406d8a08c5ea7337240aa63c5d1e247 Mon Sep 17 00:00:00 2001 From: nathan Date: Sat, 20 Dec 2025 15:07:58 -0700 Subject: [PATCH] pushing befor major changes --- Gemfile | 1 + core/Rakefile | 4 +- core/game.rb | 12 + core/http_server.rb | 119 +++++++++ core/server.md | 0 core/server.rb | 218 +++++++++++++---- core/spec/examples.txt | 64 ++--- core/spec/game_spec.rb | 13 + core/test.sh | 3 + dev_notes.md | 83 +++++++ game/.rspec | 1 - game/Rakefile | 36 --- game/game.rb | 14 -- game/log.rb | 17 -- game/planet.rb | 40 --- game/player.rb | 25 -- game/position.rb | 52 ---- game/ship.rb | 63 ----- game/spec/api_spec.rb | 159 ------------ game/spec/planet_spec.rb | 52 ---- game/spec/player_spec.rb | 16 -- game/spec/position_spec.rb | 72 ------ game/spec/ship_spec.rb | 90 ------- game/spec/spec_helper.rb | 102 -------- game/spec/stdout_helper.rb | 12 - game/spec/universe_array_spec.rb | 45 ---- game/spec/universe_spec.rb | 163 ------------- game/star.rb | 8 - game/universe.rb | 406 ------------------------------- game/universe_array.rb | 47 ---- ui.md | 3 + 31 files changed, 438 insertions(+), 1502 deletions(-) create mode 100644 core/http_server.rb create mode 100644 core/server.md create mode 100644 core/test.sh create mode 100644 dev_notes.md delete mode 100644 game/.rspec delete mode 100644 game/Rakefile delete mode 100644 game/game.rb delete mode 100644 game/log.rb delete mode 100644 game/planet.rb delete mode 100644 game/player.rb delete mode 100644 game/position.rb delete mode 100644 game/ship.rb delete mode 100644 game/spec/api_spec.rb delete mode 100644 game/spec/planet_spec.rb delete mode 100644 game/spec/player_spec.rb delete mode 100644 game/spec/position_spec.rb delete mode 100644 game/spec/ship_spec.rb delete mode 100644 game/spec/spec_helper.rb delete mode 100644 game/spec/stdout_helper.rb delete mode 100644 game/spec/universe_array_spec.rb delete mode 100644 game/spec/universe_spec.rb delete mode 100644 game/star.rb delete mode 100644 game/universe.rb delete mode 100644 game/universe_array.rb create mode 100644 ui.md diff --git a/Gemfile b/Gemfile index b3687c2..617453e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,3 @@ gem sinatra gem sinatra-cross_origin +gem yard-sinatra diff --git a/core/Rakefile b/core/Rakefile index 04f0ae6..f972d99 100644 --- a/core/Rakefile +++ b/core/Rakefile @@ -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' diff --git a/core/game.rb b/core/game.rb index 8bc97b9..ec1b76a 100644 --- a/core/game.rb +++ b/core/game.rb @@ -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. # diff --git a/core/http_server.rb b/core/http_server.rb new file mode 100644 index 0000000..6f94439 --- /dev/null +++ b/core/http_server.rb @@ -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 diff --git a/core/server.md b/core/server.md new file mode 100644 index 0000000..e69de29 diff --git a/core/server.rb b/core/server.rb index 5a2366c..4d9a998 100644 --- a/core/server.rb +++ b/core/server.rb @@ -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 diff --git a/core/spec/examples.txt b/core/spec/examples.txt index 2878b6f..dcee7b6 100644 --- a/core/spec/examples.txt +++ b/core/spec/examples.txt @@ -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 | diff --git a/core/spec/game_spec.rb b/core/spec/game_spec.rb index c182da9..288a0c1 100644 --- a/core/spec/game_spec.rb +++ b/core/spec/game_spec.rb @@ -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 diff --git a/core/test.sh b/core/test.sh new file mode 100644 index 0000000..dc659b1 --- /dev/null +++ b/core/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +rerun 'ruby server.rb' diff --git a/dev_notes.md b/dev_notes.md new file mode 100644 index 0000000..a76b4d9 --- /dev/null +++ b/dev_notes.md @@ -0,0 +1,83 @@ +# Chatting with GPT: + +I think that I will modify some of how I am doing things. We will have the game do things differently. We will have a main loop that will process events and then we will use delta time and a `game speed` variable to control things. + +Here is my newly defined API: + + +# API + + +Transport: WebSocket +Encoding: JSON +Direction: Bidirectional + +Every message has: + +``` +{ + "type": "...", + "domain": "...", + "request_id": integer + "payload": {} +} +``` + +## Message types: + + +### `hello` + + - Direction: Client --> Server + - Purpose: initiates the connection and has authentication + - Behavior: + - Must be responded to immediately + - Response must be an `update` type with a full update (domain `all`) + +### `query` + + - Direction: Client --> Server + - Purpose: request read-only data + - Behavior: + - Must be responded to immediately + - Does not mutate server state + - Response must be an `update` type with the requested domain + +### `command` + + - Direction: Client --> Server + - Purpose: Request a state change + - Behavior: + - Server validates the command immediately + - If the command is valid the server applies the command and acknowledges with an `event` with the resulted action. + - If invalid the server responds with an `error` and includes the command and an error message. + +### `update` + + - Direction: Server --> Client + - Purpose: Deliver the game state to the client + - Behavior: + - For a full state update (ie player login) the `all` domain is used. Otherwise it is a subdomain. + - Used as a response to `hello`, `query`. + - Full updates are sent periodically for client synchnorization. + +### `event` + + - Direction: Server --> Client + - Purpose: Notify the client of events + - Behavior: + - Represents something that *happened* + - Not a full state snapshot + - Can not be requested by the client + +### `error` + + - Direction: Server --> Client + - Purpose: Report a failure to process a `query` or `command` + - Behavior: + - Does not mutate state + - Describe why the request failed. + +## Notes + +**The server is authoritative for all game state. Clients may not infer or simulate authoritative outcomes beyond visual interpolation.** diff --git a/game/.rspec b/game/.rspec deleted file mode 100644 index c99d2e7..0000000 --- a/game/.rspec +++ /dev/null @@ -1 +0,0 @@ ---require spec_helper diff --git a/game/Rakefile b/game/Rakefile deleted file mode 100644 index e8f5520..0000000 --- a/game/Rakefile +++ /dev/null @@ -1,36 +0,0 @@ -# Nathan Hinton 2 Oct 2025 -# Rake file - - -desc 'Run RSpec tests' -task :test do - begin - require 'rspec/core/rake_task' - RSpec::Core::RakeTask.new(:spec) - ENV['RUN_ALL_TESTS'] = 'true' - Rake::Task['spec'].invoke - rescue LoadError - puts 'Failed to load RSpec to run tests... Try installing rspec' - end -end - - -desc 'Run the server for clients to connect to' -task :serve do - raise NotImplementedError.new('Server task not yet implimented!') -end - - -namespace :doc do - desc 'Create the YARD documentation for the project' - task :yard do - res = sh 'yard doc *.rb --list-undoc' - sh 'yard stats *.rb --list-undoc' - end - - desc 'Create code coverage' - task :coverage do - ENV['COVERAGE'] = 'true' - Rake::Task[:test].invoke - end -end diff --git a/game/game.rb b/game/game.rb deleted file mode 100644 index bfd4d95..0000000 --- a/game/game.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Nathan Hinton 1 Oct 2025 - -# This file basiacally creates a universe and runs a simulated game in it. - -require './universe' -require './player' - -universe = Universe.new('test universe', universe_size: 3) -universe.create_universe -universe.print_universe() -planets = universe.planets - -player = Player.new('test player') -puts player.name diff --git a/game/log.rb b/game/log.rb deleted file mode 100644 index b400c13..0000000 --- a/game/log.rb +++ /dev/null @@ -1,17 +0,0 @@ -# BROKEN :( # Nathan Hinton 2 Oct 2025 -# BROKEN :( # Does not work sadly... -# BROKEN :( -# BROKEN :( require 'logger' -# BROKEN :( -# BROKEN :( module Log -# BROKEN :( @log = Logger.new('game.log') -# BROKEN :( @log.level = Logger::INFO -# BROKEN :( -# BROKEN :( def self.logger -# BROKEN :( @logger -# BROKEN :( end -# BROKEN :( -# BROKEN :( def logger -# BROKEN :( Log.logger -# BROKEN :( end -# BROKEN :( end diff --git a/game/planet.rb b/game/planet.rb deleted file mode 100644 index 9e58d69..0000000 --- a/game/planet.rb +++ /dev/null @@ -1,40 +0,0 @@ -# Nathan Hinton 1 Oct 2025 - -PLANET_PRODUCTION_RESOURCES = [:ships] - -# This class holds information about a planet. -class Planet - attr_reader :owner - - # Create a planet - # - # @param random [Random] The random generator for the game (ensures seeds can be duplicated) - def initialize(random) - @random = random - @prod_rate = @random.rand - @resource = PLANET_PRODUCTION_RESOURCES.sample(random: @random) - @owner = nil - end - - # Change the ownership of a planet. Should check for things like the planet - # has no defenders left and other fun stuff (later) - # - # @param owner [Player] The owner of the planet - # @return [Boolean] If the operation was successful or not - def change_owner(owner) - # For now only unclaimed planets can be owned - if @owner.nil? - @owner = owner - return true - end - return false - end - - # Incriment the planet. Used to add resources to the Player's global resource - # storage - def tick - if !@owner.nil? - @oener.add_resources({@resource => @prod_rate}) - end - end -end diff --git a/game/player.rb b/game/player.rb deleted file mode 100644 index 5827eaa..0000000 --- a/game/player.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Nathan Hinton 2 Oct 2025 - - -# Class for the player. For now does not do much. -class Player - attr_reader :name, :resources - - # initializes with just the player name. - # - # @param name [String] The name of the player - def initialize(name) - @name = name - @resources = {:ships => 10, :metal => 100} - end - - def add_resources(resources) - resources.keys.each do |resource_key| - if @resources[resource_key].nil? - @resources[resource_key] = resources[resource_key] - else - @resources[resource_key] += resources[resource_key] - end - end - end -end diff --git a/game/position.rb b/game/position.rb deleted file mode 100644 index b078243..0000000 --- a/game/position.rb +++ /dev/null @@ -1,52 +0,0 @@ -# Nathan Hinton 2 Oct 2025 - - -# A helper class for storing and converting universe point formats: -class Position - attr_reader :x_pos, :y_pos, :a, :b, :c - def initialize(*args) - if args.length == 2 - # initialize with x, y position: - if args.sum.odd? - raise ArgumentError.new('A position created in (x, y) must sum to an even number!') - end - @x_pos = args[0] - @y_pos = args[1] - elsif args.length == 3 - # initialize with a, b, c position - raise NotImplementedError.new('(a, b, c) positioning not implimented!') - else - raise ArgumentError.new('Creation of a position requires two or three integer args for a position.') - end - end - - # Returns the corrdinates in the 2d plane in a 3 axis notation. - # - # @return [Array] The (a, b, c) position - def two_d_hexagonal - return [a, b, c] - end - - # Returns the coordinates in the 2d plane - # - # @return [Array] The (x, y) position - def two_d_cartesian - return [x_pos, y_pos] - end - - # Returns the string formatted version - # - # @return [String] Returns a string representing the point. - def to_s - return "(#{@x_pos}, #{y_pos})" - end - - # Custom comparison. We need to check the the point is the same, not the - # identity. - # - # @param other [Positon] The position to compare with - # @return [Boolean] If they are in the same position as each other. - def ==(other) - return other.x_pos == @x_pos && other.y_pos == @y_pos - end -end diff --git a/game/ship.rb b/game/ship.rb deleted file mode 100644 index b122db5..0000000 --- a/game/ship.rb +++ /dev/null @@ -1,63 +0,0 @@ -# Nathan Hinton 2 Oct 2025 - - -# This class will be the container for ships. For now they are all the same -class Ship - attr_reader :owner - - def initialize(x_pos, y_pos, owner) - @position = [x_pos, y_pos] - @speed = 0.1 - @owner = owner - end - - # Called every game tick - def tick - if @moving != [0, 0] - - end - end - - # Move function. This will move a ship. For now you can only move one hop, - # there is no way to move more than one at the moment. This position should - # be only *one* spot away from where we are. - # - # @param x_pos [Integer] The target x position - # @param y_pos [Integer] The target y position - def move(x_pos, y_pos) - #Validate coordinates: - if (x_pos + y_pos).odd? - raise ArgumentError.new('The x, y coordinate for movement must add to be even!') - end - - # Ensure we are not too far: - if ((x_pos + y_pos) - @position.sum).abs != 2 - raise ArgumentError.new('The x, y coordinates for moevement must be to an adjcent space!') - end - - end # Function move - - # Move_direction. Takes a direction and then sets the moving variable equal that direction. - # - # @param direction [Integer] A value between (0, 5) that determines the direction (think unit circle) - def move_direction(direction) - if (direction < 0 || direction > 5) - raise ArgumentError.new('When moving using direction based movement, x must be between (0..5)!') - end - # This means that the direction can range from 0 to 5 (think of unit circle) - case direction - when 0 - @moving = [2, 0] # Right - when 1 - @moving = [1, 1] # Right up - when 2 - @moving = [-1, 1] # Left up - when 3 - @moving = [-2, 0] # Left - when 4 - @moving = [-1, -1] # Left down - when 5 - @moving = [1, -1] # Right down - end - end # Function move_direction -end diff --git a/game/spec/api_spec.rb b/game/spec/api_spec.rb deleted file mode 100644 index 28690f1..0000000 --- a/game/spec/api_spec.rb +++ /dev/null @@ -1,159 +0,0 @@ -# frozen_string_literal: true -# Nathan Hinton 2 Oct 2025 - -# This is for testing the API that the clients can use. - - -# Most of the api is here as it relates to interacting with the universe -require './universe' -require './player' -require './position' -require './ship' - -RSpec.describe 'API' do - - # Universe setup - let(:name) { 'Test Universe' } - let(:universe_size) { 3 } - let(:seed) { 12345 } - - subject(:universe) do - Universe.new( - name, - universe_size: universe_size, - seed: seed, - create_chances: { planet: 1 } # force a planet at every valid spot - ) - end - - before(:each) do - universe.create_universe - end - - - # Player setup - let(:player_1) { Player.new('test player 01') } - let(:player_2) { Player.new('test player 02') } - - # Positions setup - let(:center_position) { Position.new(5, 3) } - describe 'UNIVERSE API' do - describe '#get_position' do - it 'returns a position in the universe' do - expect(universe.get_position(player_1, Position.new(0, 0))).to be nil - expect(universe.get_position(player_1, Position.new(3, 5))).to be_a(Planet) - expect(universe.get_position(player_1, center_position)).to be_a(Star) - end - end # describe Universe#get_position - end # describe 'UNIVERSE API' - - - describe 'SHIPS API' do - describe '@create_ship' do - it 'returns false when the player is invalid' do - expect(universe.create_ship(player_1, Position.new(3, 5))).to eq false - end - end # describe '@create_ship' - - describe '@move_ship' do - it 'returns false when the player is invalid' do - expect(universe.move_ship(player_1, Ship.new(2, 4, player_1), Position.new(3, 5))).to eq false - end - end # describe '@move_ship' - - describe '@remove_ship' do - it 'returns false when the player is invalid' do - expect(universe.remove_ship(player_1, Position.new(3, 5))).to eq false - end - end # describe '@remove_ship' - end # describe 'SHIPS API' - - - - describe 'PLANETS API' do - describe '@remove_planet' do - end # describe '@remove_planet' - end # describe 'PLANETS API' - - - describe 'PLAYER API' do - describe '#add_player' do - it 'adds with free planets' do - expect(universe.add_player(player_1)).to be(true) - expect(universe.instance_variable_get(:@player_list)).to eq [player_1] - end - - it 'fails to add when all planets are occopuied' do - # Make all the planets owned by player_2 - universe.planets.each do |planet| - planet.change_owner(player_2) - end - expect(universe.add_player(player_1)).to be(false) - expect(universe.instance_variable_get(:@player_list)).to eq [] - end - - it 'raises an error if a planet can not be taken' do - # This is an exceptional case. This is actually proper behavior when - # not trying to deal out planets. - end - end # describe 'add_player' - - describe '#remove_player' do - it 'can remove an existing player' do - universe.add_player(player_1) - expect(universe.remove_player(player_1)).to eq true - expect(universe.remove_player(player_2)).to eq true - expect(universe.instance_variable_get(:@player_list)).to eq [] - end - - it 'can remove a non existing player' do - expect(universe.remove_player(player_2)).to eq true - end - end # describe 'remove_player' - - describe '#validate_player' do - it 'returns true when a player is valid' do - universe.add_player(player_1) - expect(universe.validate_player(player_1)).to eq true - end - - it 'returns false when the player is not in the player list' do - expect(universe.validate_player(player_2)).to eq false - end - end # describe 'validate_player' - - - describe '#get_player_ships' do - it 'lists all ships owned by a player' do - universe.add_player(player_1) - universe.add_player(player_2) - expect(universe.get_player_ships(player_1)).to eq [] - ship_1 = Ship.new(2, 4, player_1) - ship_2 = Ship.new(2, 4, player_2) - ship_3 = Ship.new(3, 5, player_1) - universe.instance_variable_set(:@ship_list, [ship_1, ship_2, ship_3]) - expect(universe.get_player_ships(player_1)).to eq [ship_1, ship_3] - expect(universe.get_player_ships(player_2)).to eq [ship_2] - end - - it 'returns an empty list when a player that is not registered gets ships' do - ship_2 = Ship.new(2, 4, player_2) - universe.instance_variable_set(:@ship_list, [ship_2]) - expect(universe.get_player_ships(player_2)).to eq [] - end - end # describe 'get_player_ships' - - describe '#get_player_planets' do - it 'returns the planets owned by a player' do - universe.add_player(player_1) - expect(universe.get_player_planets(player_2)).to eq [] - expect(universe.get_player_planets(player_1).length).to eq 1 - end - - it 'returns an empty list for a unregistered player' do - expect(universe.get_player_planets(player_2)).to eq [] - end - - end # describe 'get_player_planets' - end # describe 'PLAYER API' -end diff --git a/game/spec/planet_spec.rb b/game/spec/planet_spec.rb deleted file mode 100644 index 2e4bfab..0000000 --- a/game/spec/planet_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -# Nathan Hinton 2 Oct 2025 - -require './planet' - -require './player' - -RSpec.describe Planet do - let(:seed) { 12345 } - let(:random) { Random.new(seed) } - let(:planet) { Planet.new(random) } - let(:player_1) { Player.new('tester 01') } - let(:player_2) { Player.new('tester 02') } - - describe '#initialize' do - it 'stores the random off' do - expect(planet.instance_variable_get(:@random)).to eq random - end - - it 'creates a production rate' do - expect(planet.instance_variable_get(:@prod_rate)).to eq Random.new(seed).rand - end - - it 'sets a resource' do - expect(planet.instance_variable_get(:@resource)).to eq PLANET_PRODUCTION_RESOURCES[(Random.new(seed).rand * PLANET_PRODUCTION_RESOURCES.length).floor] - end - - it 'has a nil owner' do - expect(planet.instance_variable_get(:@owner)).to eq nil - end - end # describe #initialize - - describe '#change_owner' do - it 'allows an ownership change when the current owner is nil' do - expect(planet.change_owner(player_1)).to eq true - expect(planet.instance_variable_get(:@owner)).to eq player_1 - end - - it 'does not change when already owned' do - planet.change_owner(player_2) - expect(planet.change_owner(player_1)).to eq false - expect(planet.instance_variable_get(:@owner)).to eq player_2 - end - end - - describe '#tick' do - it 'adds resources to the Players storage' do - planet.change_owner(player_1) - planet.tick - expect(player_1.resources).to eq('') - end - end -end diff --git a/game/spec/player_spec.rb b/game/spec/player_spec.rb deleted file mode 100644 index 8596fbb..0000000 --- a/game/spec/player_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Nahtan Hinton 3 Oct 2025 - -require './player' - -RSpec.describe Player do - let(:player_1) { Player.new('tester 01') } - describe '#initialize' do - it 'sets the player name' do - expect(player_1.instance_variable_get(:@name)).to eq 'tester 01' - end - - it 'gives initial resources' do - expect(player_1.instance_variable_get(:@resources)).to eq ({:ships => 10, :metal => 100}) - end - end -end diff --git a/game/spec/position_spec.rb b/game/spec/position_spec.rb deleted file mode 100644 index a3c9cdb..0000000 --- a/game/spec/position_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require './position' - -RSpec.describe Position do - let(:even_pair) { [4, 6] } # 4+6 = 10 → even - let(:odd_pair) { [3, 4] } # 3+4 = 7 → odd - - describe 'initialisation' do - context 'with two arguments' do - it 'stores the coordinates when the sum is even' do - pos = Position.new(*even_pair) - expect(pos.x_pos).to eq even_pair[0] - expect(pos.y_pos).to eq even_pair[1] - end - - it 'raises ArgumentError when the sum is odd' do - expect { Position.new(*odd_pair) } - .to raise_error(ArgumentError, /must sum to an even number/) - end - end - - context 'with three arguments' do - it 'raises NotImplementedError' do - expect { Position.new(1, 2, 3) } - .to raise_error(NotImplementedError, /positioning not implimented/) - end - end - - context 'with an unsupported number of arguments' do - it 'raises ArgumentError when no arguments are supplied' do - expect { Position.new } - .to raise_error(ArgumentError, /requires two or three integer args/) - end - - it 'raises ArgumentError when too many arguments are supplied' do - expect { Position.new(1, 2, 3, 4) } - .to raise_error(ArgumentError, /requires two or three integer args/) - end - end - end - - describe '#two_d_cartesian' do - it 'returns the original (x, y) pair' do - pos = Position.new(*even_pair) - expect(pos.two_d_cartesian).to eq even_pair - end - end - - describe '#two_d_hexagonal' do - it 'returns an array of nil values for a, b, c' do - pos = Position.new(*even_pair) - expect(pos.two_d_hexagonal).to eq [nil, nil, nil] - end - end - - describe '#to_s' do - it 'puts out the coordinates' do - expect(Position.new(*even_pair).to_s).to eq '(4, 6)' - end - end - - describe '#==' do - it 'returns false when the positions differ' do - expect(Position.new(*even_pair) == Position.new(6, 8)).to eq false - end - - it 'returns true when the positions are the same' do - expect(Position.new(*even_pair) == Position.new(4, 6)).to eq true - end - end -end diff --git a/game/spec/ship_spec.rb b/game/spec/ship_spec.rb deleted file mode 100644 index b3cd1c8..0000000 --- a/game/spec/ship_spec.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -require './ship' - -require './player' - -RSpec.describe Ship do - let(:ship) { Ship.new(0, 0, Player.new('test player 01')) } - - describe '#initialize' do - it 'stores the given position' do - expect(ship.instance_variable_get(:@position)).to eq [0, 0] - end - - it 'sets the speed to 0.1' do - expect(ship.instance_variable_get(:@speed)).to eq 0.1 - end - - it 'does not set @moving until a direction is chosen' do - expect(ship.instance_variable_get(:@moving)).to be_nil - end - end - - describe '#tick' do - it 'does nothing (no exception) when called' do - expect { ship.tick }.not_to raise_error - end - end - - describe '#move' do - context 'when coordinates are valid (even sum, adjacent)' do - it 'accepts positions that satisfy the rules' do - expect { ship.move(1, 1) }.not_to raise_error # sum 2 - expect { ship.move(2, 0) }.not_to raise_error # sum 2 - expect { ship.move(0, 2) }.not_to raise_error # sum 2 - expect { ship.move(-1, -1) }.not_to raise_error # sum -2 - end - end - - context 'when the sum is odd' do - it 'raises an ArgumentError' do - expect { ship.move(0, 1) }.to raise_error( - ArgumentError, - /must add to be even/ - ) - end - end - - context 'when the target is not adjacent' do - it 'raises an ArgumentError' do - expect { ship.move(3, 3) }.to raise_error( - ArgumentError, - /must be to an adjcent space/ - ) - expect { ship.move(0, 0) }.to raise_error( - ArgumentError, - /must be to an adjcent space/ - ) - end - end - end - - describe '#move_direction' do - it 'accepts direction values 0..5 and sets @moving correctly' do - mapping = { - 0 => [2, 0], - 1 => [1, 1], - 2 => [-1, 1], - 3 => [-2, 0], - 4 => [-1, -1], - 5 => [1, -1] - } - mapping.each do |dir, expected| - expect { ship.move_direction(dir) }.not_to raise_error - expect(ship.instance_variable_get(:@moving)).to eq expected - end - end - - it 'raises ArgumentError for invalid directions' do - expect { ship.move_direction(-1) }.to raise_error( - ArgumentError, - /must be between \(0..5\)/ - ) - expect { ship.move_direction(6) }.to raise_error( - ArgumentError, - /must be between \(0..5\)/ - ) - end - end -end diff --git a/game/spec/spec_helper.rb b/game/spec/spec_helper.rb deleted file mode 100644 index 4a1b6f2..0000000 --- a/game/spec/spec_helper.rb +++ /dev/null @@ -1,102 +0,0 @@ -# This file was generated by the `rspec --init` command. Conventionally, all -# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause -# this file to always be loaded, without a need to explicitly require it in any -# files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need -# it. -# -# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration - -if ENV['COVERAGE'] == 'true' - require 'simplecov' - SimpleCov.start -end - -RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. - config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. - config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. - mocks.verify_partial_doubles = true - end - - # This option will default to `:apply_to_host_groups` in RSpec 4 (and will - # have no way to turn it off -- the option exists only for backwards - # compatibility in RSpec 3). It causes shared context metadata to be - # inherited by the metadata hash of host groups and examples, rather than - # triggering implicit auto-inclusion in groups with matching metadata. - config.shared_context_metadata_behavior = :apply_to_host_groups - -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. - # This allows you to limit a spec run to individual examples or groups - # you care about by tagging them with `:focus` metadata. When nothing - # is tagged with `:focus`, all examples get run. RSpec also provides - # aliases for `it`, `describe`, and `context` that include `:focus` - # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - config.filter_run_when_matching :focus - - # Allows RSpec to persist some state between runs in order to support - # the `--only-failures` and `--next-failure` CLI options. We recommend - # you configure your source control system to ignore this file. - config.example_status_persistence_file_path = "spec/examples.txt" - - # Limits the available syntax to the non-monkey patched syntax that is - # recommended. For more details, see: - # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ - config.disable_monkey_patching! - - # This setting enables warnings. It's recommended, but in some cases may - # be too noisy due to issues in dependencies. - config.warnings = true - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = "doc" - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - # config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -end diff --git a/game/spec/stdout_helper.rb b/game/spec/stdout_helper.rb deleted file mode 100644 index 0702e19..0000000 --- a/game/spec/stdout_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# If you are getting an error `uninitialized constant StringIO` make sure that -# your code is in an `it` block. - -# Helper that captures STDOUT. -def capture_stdout - output = StringIO.new - $stdout = output - yield - output.string -ensure - $stdout = STDOUT -end diff --git a/game/spec/universe_array_spec.rb b/game/spec/universe_array_spec.rb deleted file mode 100644 index 0811243..0000000 --- a/game/spec/universe_array_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# Nathan Hinton 2 Oct 2025 - - -require './universe_array' - -RSpec.describe UniverseArray do - describe '#[]' do - it 'works with indexes' do - uni_arr = UniverseArray.new() - uni_arr[0] = [] - uni_arr[0][0] = 'hi' - expect(uni_arr[0][0]).to eq 'hi' - end - - it 'works with positions' do - uni_arr = UniverseArray.new() - uni_arr[0] = [] - uni_arr[0][0] = 'hi' - expect(uni_arr[Position.new(0, 0)]).to eq 'hi' - end - - it 'returns nil when access outside universe' do - uni_arr = UniverseArray.new() - uni_arr[3] = [] - expect(uni_arr[Position.new(3, 51)]).to eq nil - expect(uni_arr[Position.new(0, 0)]).to eq nil - end - end - - describe '#[]=' do - it 'works with indexes' do - uni_arr = UniverseArray.new() - uni_arr[0] = [] - uni_arr[0][0] = 'hi' - expect(uni_arr[0][0]).to eq 'hi' - end - - it 'works with positions' do - uni_arr = UniverseArray.new() - uni_arr[0] = [] - uni_arr[Position.new(0, 0)] = 'hi' - expect(uni_arr[Position.new(0, 0)]).to eq 'hi' - end - end -end diff --git a/game/spec/universe_spec.rb b/game/spec/universe_spec.rb deleted file mode 100644 index 1bb1105..0000000 --- a/game/spec/universe_spec.rb +++ /dev/null @@ -1,163 +0,0 @@ -# frozen_string_literal: true - -require './universe' -require './planet' -require './position' -require './universe_array' - -require './spec/stdout_helper' - -RSpec.describe Universe do - - let(:name) { 'Test Universe' } - let(:universe_size) { 3 } - let(:seed) { 12345 } - - subject(:universe) do - Universe.new( - name, - universe_size: universe_size, - seed: seed, - create_chances: { planet: 1 } # force a planet at every valid spot - ) - end - - describe '#initialize' do - it 'stores all expected instance variables' do - expect(universe.name).to eq name - expect(universe.instance_variable_get(:@universe_size)).to eq universe_size - expect(universe.instance_variable_get(:@seed)).to eq seed - end - - it 'creates a deterministic Random object when a seed is given' do - rng = universe.instance_variable_get(:@random) - expect(rng).to be_a(Random) - expect(rng.rand(100)).to eq Random.new(seed).rand(100) - end - - it 'initializes the random if no seed is passed in' do - uni = Universe.new(name) - rng = uni.instance_variable_get(:@random) - expect(rng).to be_a(Random) - end - end - - describe '#create_universe' do - - end - - describe '#distance' do - it 'is symmetric' do - a = Position.new(0, 0) - b = Position.new(4, 2) - expect(universe.distance(a, b)).to eq universe.distance(b, a) - end - - it 'returns 0 when both points are identical' do - p = Position.new(1, 1) - expect(universe.distance(p, p)).to eq 0 - end - - it 'correctly computes known hex distances' do - p0 = Position.new(0, 0) - p1 = Position.new(2, 0) - p2 = Position.new(4, 2) - p3 = Position.new(2, 2) - # (0,0) to (2,0) → 1 - expect(universe.distance(p0, p1)).to eq 1 - # (0,0) to (4,2) → 3 - expect(universe.distance(p0, p2)).to eq 3 - # (0,0) to (2,2) → 2 - expect(universe.distance(p0, p3)).to eq 2 - end - end # describe '#distance' - - describe '#map generation' do - it 'creates the expected dimensions' do - universe.create_universe - map = universe.instance_variable_get(:@universe_map) - # Each valid (x,y) pair inside the radius contains a Planet - # (our stub) and every other entry is nil. - expect(map).to be_a(UniverseArray) - # Check how may nodes were created: - expect(map.flatten.compact.length).to eq 19 # 1 + 1*6 + 2*6 - end - - it 'contains a planet at every valid point when create_chances is 1' do - universe.create_universe - map = universe.instance_variable_get(:@universe_map) - map.each do |row| - row.each do |idx| - # The indexes are allowed to be nil - if idx.nil? - next # Is allowed to be nil - end - if idx.is_a? Star - next # Is allowed to be star - end - expect(idx).to be_a(Planet) - end - end - end # describe '#map generation' - - it 'is deterministic when the same seed is used' do - first_universe = Universe.new( - name, - universe_size: universe_size, - seed: seed, - create_chances: { planet: 0.1 } - ) - first_map = first_universe.instance_variable_get(:@universe_map) - - second_universe = Universe.new( - name, - universe_size: universe_size, - seed: seed, - create_chances: { planet: 0.1 } - ) - second_map = second_universe.instance_variable_get(:@universe_map) - - expect(first_map).to eq second_map - end - end - - describe '#print_universe' do - it 'prints the center point as “O”' do - out = capture_stdout do - universe.create_universe - universe.print_universe - end - expect(out).to include('O') - end - - it 'can print blank spaces for nothing' do - out = capture_stdout do - uni = Universe.new(name, create_chances: {planet: 0.0}) - uni.create_universe - uni.print_universe - end - expect(out).to include('O') - expect(out).to include('- -') - end - end - - describe '#planets' do - it 'lists all of the generated planets' do - universe.create_universe - expect(universe.planets.length).to eq 18 - end - end - - describe '#tick' do - it 'runs a tick of the game' do - universe.create_universe - player_1 = Player.new('test player 01') - universe.add_player(player_1) - universe.add_ship(player_1, Position.new(3, 5)) - ships = universe.get_player_ships(player_1) - universe.move_ship(player_1, ships[0], Position.new(2, 4)) - # Now we tick the universe and the ship should move a little and the player should gain resources. - expect{universe.tick}.to_not raise_error() - end - end -end diff --git a/game/star.rb b/game/star.rb deleted file mode 100644 index 960298b..0000000 --- a/game/star.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Nathan Hinton Oct 2 2025 - -# Literally just a class to have something else in space... - -class Star - def initialize - end -end diff --git a/game/universe.rb b/game/universe.rb deleted file mode 100644 index b44074e..0000000 --- a/game/universe.rb +++ /dev/null @@ -1,406 +0,0 @@ -# Nathan Hinton 1 Oct 2025 - - -require 'logger' - -require './planet' -require './star' -require './position' -require './universe_array' - - -# Universe class. Holds a universe and provides the api for it. Universes are -# created on a 2d grid. I would like it to be hexagonal movement type so that -# it feels less like a grid and more like a circle. This means that we will -# have some interesting point systems. For now I am thinking that we will have -# three directions, a, b, c which will corespond to moving along one of the -# lines to another system. Here is an ascii art: -# -# -# (0, 0, 1) (0, 1, 0) -# \ / -# \ / -# \ / -# (+c) (+b) -# \ / -# \ / -# \ / -# (-1, 0, 0) <--- (-a) --- (0, 0, 0) --- (+a) ---> (1, 0, 0) -# / \ -# / \ -# / -# (-b) (-c) -# / \ -# / \ -# / \ -# (0, -1, 0) (0, 0, -1) -# -# -# This means that we are adding vector together. I do not think that this will -# work super well though with a system that is other than 2D. I think that will -# be fine. -# -# After thinking some more I will have the universe class hold everything in -# the universe. It will be in charge of holding all of the ships and -# planets. Then the Player class will be connected to the ships and planets -# that they own/control -# -# @param name [String] The name of the game -# @param universe_size [Integer] Controlls the size of the generated universe. This is a radius from the center point. -class Universe - # Setup logger for this class: - @@logger = Logger.new('logs/universe.log') - - attr_reader :name - - def initialize(name, universe_size: 4, create_chances: {planet: 1}, seed: nil) - - @name = name - @universe_size = universe_size - @create_chances = create_chances - - # Setup internal lists: - @player_list = [] - @planet_list = [] - @ship_list = [] - - # initizlize RNG - if seed.nil? - @random = Random.new() - @seed = @random.seed - else - @seed = seed - @random = Random.new(@seed) - end - end # Initialize - - - # Create the universe. This will setup all of the planets and a blank slate - # for the universe. - def create_universe() - # Set center of universe - @center = Position.new(@universe_size + 2, @universe_size) - - @@logger.info "Generating Universe '#{@name}' with seed #{@seed}" - @@logger.info "Center is #{@center}" - @universe_map = UniverseArray.new() - # Generate the universe. First generate the x values: - (0 .. (@universe_size * 2)).each do |y_pos| - @universe_map[y_pos] = [] - (0 .. (@universe_size * 2 + 2)).each do |x_pos| - # Make the grid work right, x is actually skipping every other. - x_pos *= 2 - x_pos += 1 if y_pos.odd? - position = Position.new(x_pos, y_pos) - - # Skip if the node is too far - if distance(@center, position) >= @universe_size - next - end - - # Skip if it is the center - if position == @center - @universe_map[position] = Star.new() - next - end - - # Get what the planet should be: - spot = nil - if @random.rand < @create_chances[:planet] - spot = Planet.new(@random) - @planet_list.push(spot) - #else - # spot = '' - end - @@logger.debug "Creating something at (#{position})" - @universe_map[position] = spot - end # Create y points - end # Create x points - # Remove any empty nodes: - end # Function create_universe - - # Returns the distance between points. This is returned as a number which - # indicates how far you have to go to get to a point. This is actually - # supurisingly hard... - # - # @param position_a [Position] A start point in the universe - # @param position_b [Position] Another point in the universe - # @return [Integer] The distance between points in the universe. - def distance(position_a, position_b) - result = 0 # We can just add the abs diff of each direction. - dist_x = (position_b.x_pos - position_a.x_pos).abs - dist_y = (position_b.y_pos - position_a.y_pos).abs - - res = (dist_x / 2) + dist_y - if dist_x >= dist_y - res = (dist_x + dist_y) / 2 - end - @@logger.debug "Distance: #{position_a} and #{position_b} are #{res} units away" - return res - end # Function distance - - # Prints the universe to the screen. - def print_universe(simple: false) - (0 .. (@universe_map.length)).each do |y_pos| - # Print empty space before chart (for angles) - if @universe_map[y_pos].nil? - next - end - if @universe_map[y_pos] == [] - puts '_' * (4 * @universe_size * 2) - next - end - nextline = '|' - if y_pos < @universe_size - nextline += ' ' * ((@universe_size - y_pos).abs - 1) - else - nextline += ' ' * ((@universe_size - y_pos).abs) - end - - if @universe_size.odd? - nextline += ' ' - end - - # Print more empty space for the odd places (provides offset) - if y_pos.odd? - print '| ' - else - print '|' - end - - #Actually loop through everything - (0 .. (@universe_size * 2)).each do |x_pos| - # Make the grid work right, x is actually skipping every other. - x_pos *= 2 - x_pos += 1 if y_pos.odd? - - position = Position.new(x_pos, y_pos) - # Print empty space if we are outside the universe (For items) - if distance(@center, position) >= @universe_size - print ' ' - next - end - - # Print what we are: - value = @universe_map[position] - nextline += ' ' - if value.is_a?(Star) - print 'O' - elsif value.is_a?(Planet) - print 'P' - else - print ' ' - end - - # Calculate what we should print on the next line and if we should print dashes - if y_pos < @universe_size - nextline += '/ \\' - end - if distance(@center, Position.new(x_pos + 2, y_pos)) < @universe_size - print '---' - if y_pos >= @universe_size - nextline += '\\ /' - end - end - end # For each col in row - puts '' - if (y_pos + 1) / 2 < @universe_size && !simple - puts nextline - end - end # For each row - end # Function print_universe - - # Returns all of the planets contained in the universe: - # - # @return [Array] - def planets - result = [] - @universe_map.flatten.each do |item| - result.push(item) if item.is_a? Planet - end - return result - end - - # Runs a tick of the game. Passes it on to all of the sub objects - def tick - # Incriment planets - @planet_list.each do |planet| - planet.tick - end - - # Incriment ships - @ship_list.each do |ship| - ship.tick - end - end - - - -################################################################################ -################################################################################ -################################################################################ -################################################################################ -################################################################################ -################################################################################ - - - - - - ################################################## - ################### UNIVERSE ##################### - ################################################## - - # This is where API stuff for the universe should go - - # Gets a position from the universe for a player. Eventually this will take - # into account that the player can not actually see the whole universe - # - # @param player [Player] The player who is asking for the position - # @param position [Position] The position that is being requested - # @return [Planet, nil] Returns the thing at that point. - def get_position(player, position) - return @universe_map[position] - end - - # Sets a position as explored for a player. This should be called as ships - # move around and explore the universe. - # - # @param player [Player] The player who should be set - # @param position [Position] The position that is being modified - def explore_position(player, position) - end - - - ################################################## - ##################### SHIPS ###################### - ################################################## - - # Create a ship in the universe. This ship is registered to a specific player - # - # @param player [Player] The player who owns the ship - # @param position [Position] The position that the ship should be created at - def create_ship(player, position) - return false unless validate_player(player) - end - alias_method :add_ship, :create_ship - - # Move a player's ship to a given position - # - # @param player [Player] The player whose ship should be moved - # @param ship [Ship] The ship that should be moved - # @param position [Position] The position the ship should be moved to. - def move_ship(player, ship, position) - return false unless validate_player(player) - # Ensure the ship exists before moving it. - end - - # Remove a ship from the game - # @param player [Player] The player who owns the ship to be removed - # @param ship [Ship] The ship to be removed - def remove_ship(player, ship) - return false unless validate_player(player) - end - - - ################################################## - ##################### PLANETS #################### - ################################################## - - # As all planets are created when the galaxy is generated there are no create - # planet functions. It is possible that we want to remove them though - - # Remove a planet from the universe - # - # @param planet [Planet] The planet that should be removed - # @param position [Position] The position that it should be removed from - def remove_planet(planet, position) - end - - # Change the ownership of a planet - # - # @param planet [Planet] The planet to be modified - # @param player [Player] The player who should now own the planet - # @param position [Position] The position of the planet - - ################################################## - ##################### PLAYER ##################### - ################################################## - - # Add a player to the player list in the universe - # - # @param player [Player] The player who should be added to the universe - def add_player(player) - # Give the player a planet - free_planets = [] - planets.each do |planet| - free_planets.push(planet) if planet.owner.nil? - end - if free_planets == [] - @@logger.debug 'Failed to find an unoccopuied planet for new player!' - return false - end - home_planet = free_planets.sample(random: @random) - # Can;t figure out how to test this... - # if !home_planet.change_owner(player) - # raise Exception.new('Failed to allocate a new player a home planet!') - # end - home_planet.change_owner(player) - @player_list.push(player) - @@logger.debug "Added player '#{player.name}' with home planet #{home_planet}" - return true - end - - # Remove a player from the list in this universe - # - # @param player [Player] The player that should be removed from the player list. - def remove_player(player) - if @player_list.delete(player).nil? - @@logger.debug 'Failed to remove player! Does not exist in this universe! Returning true anyway though!' - end - @@logger.debug "Removed player '#{player.name}' from universe" - return true - end - - # Validates if a player exists and is in a good state in the server - # - # @param player [Player] The player to validate - def validate_player(player) - # check if in player list: - unless @player_list.include?(player) - @@logger.warn "Player '#{player}' Tried to perform an action but is an invalid player!" - return false - end - return true - end - - # Returns a list of ships owned by the player - # - # @param player [Player] The player who wants to get their ships. - # @return [Array] - def get_player_ships(player) - return [] unless validate_player(player) - result = [] - @ship_list.each do |ship| - if ship.owner == player - result.push(ship) - end - end - return result - end # Function get_player_ships - - # Returns a list of planets owned by the player - # - # @param player [Player] The player who wants to get their planets. - def get_player_planets(player) - return [] unless validate_player(player) - result = [] - @planet_list.each do |planet| - if planet.owner == player - result.push(planet) - end - end - return result - end - -end # Class Universe diff --git a/game/universe_array.rb b/game/universe_array.rb deleted file mode 100644 index 4e58f96..0000000 --- a/game/universe_array.rb +++ /dev/null @@ -1,47 +0,0 @@ -# Nathan Hinton 1 Oct 2025 -# Create a custom hash class for the universe. - -require 'logger' - -require './position' - -# This will contain the universe hash inside it. It will allow for array type -# indexing as well as point type indexing -class UniverseArray < Array - - @@logger = Logger.new('logs/universe.log') - - # Custom accessor. This allows us to access either with a position object - # (preferred as it does validation) or direct access. - # - # @param index [Integer, Position] If it is an integer nothing special. If a - # Position it will be intepreted for us into the regular indexint. - # @param rest [Object] Actually, I have no idea why this is here. (copy paste) - # @return Whatever is at that position. Can be anything. - def [](index, *rest) - #require 'byebug';debugger - if index.is_a?(Position) - if self[index.y_pos].nil? - @@logger.debug "attempted access outside of known universe!" - return nil - end - return self[index.y_pos][index.x_pos] - else - super - end - end - - # Custom writer. This allows us to access either with a position object - # (preferred as it does validation) or direct access. - # - # @param index [Integer, Position] If it is an integer nothing special. If a - # Position it will be intepreted for us into the regular indexint. - # @param rest [Object] This (if intercepted) has the value we should assign - def []=(index, *rest) - if index.is_a?(Position) - return self[index.y_pos][index.x_pos] = rest[0] - else - super - end - end -end diff --git a/ui.md b/ui.md new file mode 100644 index 0000000..c4aac9f --- /dev/null +++ b/ui.md @@ -0,0 +1,3 @@ +# Designing UI + +I have decided to use godot to design the UI. (Hopefully it works...)