Working hard, decided that we need to commit to Git

This commit is contained in:
2025-10-02 14:57:16 -06:00
commit 72bd5fb9c0
11 changed files with 659 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
**/doc
**/.byebug*
**/.yard*
+98
View File
@@ -0,0 +1,98 @@
# Goals:
Make a multiplayer game that will run in the browser. This game should be a
planet conquest type game where you can create a lobby and then players can
join. Once you start a game only those players can play. This game should be
time based. You will start with a system of planets and then over time you will
gain ships and resources. The goal is to either eliminate all of the other
players or to control a portion of the universe. As you explore you can
encounter other planets and players. Each planet will have specific
bonuses. Some can produce ships faster, others will produce more resources and
in others you can find bonuses for different kinds of ships. I want to make it
so that you can also upgrade the planets to improve the value of them.
The maximum player count in a game will be 20 however I want this to be scalable.
# Thoughts:
I do not want the players to directly communicate with each other. I want each
one to talk to the server and then the server to use an API for getting updates
for each player and their stuff. This means that potentially there could be
multiple clients for the game however I want to start with a web based client
as I feel that is easiest for me.
Each player should be able to log in and then see what games they are playing
in and select a game to resume. The game will continue even when the player is
not logged in. This means that the player could be attacked even when they are
gone. This means that the worlds need to be persistent.
As a follow up that means that each world will be always running. This means
that if there is a main loop for the game it should be very lightweight so that
it does not consume massive amounts of resources.
I think that each universe should be randomly generated that way the players
have to explore it.
I do eventually want to have AI players however for now I just want human
players. Make sure that you help me to keep this easy as I am programming.
Here are the resources that I am thinking of for now:
- Food
- Metal
- Fuel/energy
How do we have resources travel be twee planets? I am not sure yet. Maybe have
another type of ship called a transport that can transport resources. This
should be automated though so that needed resources are transported
auto-magically.
I also want there to be some form of tech tree that the player can choose and
can research. Maybe add some rare resources.
# Vocab:
- Ship: a space ship. There are several different types:
- Explorer: Has a high vision range and can see far. It can not however
attack or defend
- Fighter: This is a ship that can fight other ships. It will also have sub
classes as well
- Colonizer: This ship is used to colonize a planet and is defenseless while
flying. However when a planet is colonized the ship can be used to defend
the planet
- Universe: The entire game space. It contains planets and other objects
- Planet: A planet that can be colonized/owned by a player. It can produce
resources and ships
- Planet Defense: Structures that can be built on a planet that will defend it
from attack.
# Final thoughts:
I am comfortable with Python and Ruby mostly. I would rather separate the game
into the server stuff which will be API driven. I have done some Ruby on Rails
however I am not very good at it. This means that I would prefer if we used
rails for it to be purely an API.
I also am very inexperienced at authentication. I think for now I will just
have player names. Anyone can use any name. This does mean however that if two
people use the same name they will be indistinguishable in the server. Probably
just taking a password and hashing it on the client and then using that hash as
a 'token' of sorts would fix this. I am not super concerned with player account
security however the server does need to be secure.
# API overview:
The `Universe` class will contain everything in the universe. This means planets, ships, and other stelar objects. This will be the API that I can think of:
- universe/
- ships/
- create/
- update/
- move/
- planets/
- update/
+27
View File
@@ -0,0 +1,27 @@
# 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
desc 'Create the YARD documentation for the project'
task :doc do
res = sh 'yard doc *.rb'
end
+8
View File
@@ -0,0 +1,8 @@
# Nathan Hinton 1 Oct 2025
# Mostly for testing right now:
require './universe'
universe = Universe.new('test universe')
universe.print_universe(simple: true)
+36
View File
@@ -0,0 +1,36 @@
# Nathan Hinton 2 Oct 2025
# A helper class for storing and converting universe point formats:
class Position
attr_reader :x_pos, :y_pos, :a, :b, :c
def initialize(*args)
if args.length == 2
# initialize with x, y position:
if args.sum.odd?
raise ArgumentError.new('A position created in (x, y) must sum to an even number!')
end
x_pos = args[0]
y_pos = args[1]
elsif args.length == 3
# initialize with a, b, c position
raise NotImplementedError.new('(a, b, c) positioning not implimented!')
else
raise ArgumentError.new('Creation of a position requires two or three integer args for a position.')
end
end
# Returns the corrdinates in the 2d plane in a 3 axis notation.
#
# @return [Array<Integer>] The (a, b, c) position
def two_d_hexagonal
return [a, b, c]
end
# Returns the coordinates in the 2d plane
#
# @return [Array<Integer>] The (x, y) position
def two_d_cartesian
return [x_pos, y_pos]
end
end
+1
View File
@@ -0,0 +1 @@
# Nathan Hinton 2 Oct 2025
+15
View File
@@ -0,0 +1,15 @@
# Nathan Hinton 1 Oct 2025
PLANET_PRODUCTION_RESOURCES = [:ships]
# This class holds information about a planet.
class Planet
attr_reader :owner
# Create a planet
def initialize(random)
@random = random
@prod_rate = @random.rand
@resource = PLANET_PRODUCTION_RESOURCES[(@random.rand * PLANET_PRODUCTION_RESOURCES.length).floor]
@owner = nil
end
end
+60
View File
@@ -0,0 +1,60 @@
# Nathan Hinton 2 Oct 2025
# This class will be the container for ships. For now they are all the same
class Ship
def initialize(x_pos, y_pos)
@position = [x_pos, y_pos]
@speed = 0.1
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
+95
View File
@@ -0,0 +1,95 @@
# Nathan Hinton 2 Oct 2025
# Test file for the ship object
require './ship'
RSpec.describe Ship do
let(:initial_x) { 0 }
let(:initial_y) { 0 }
subject(:ship) { Ship.new(initial_x, initial_y) }
describe '#initialize' do
it 'stores the given position' do
expect(ship.instance_variable_get(:@position)).to eq([initial_x, initial_y])
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 (but does not raise an exception)' do
expect { ship.tick }.not_to raise_error
end
end
describe '#move' do
context 'when coordinates are valid' do
it 'accepts adjacent moves whose sum is even' do
# adjacent positions that satisfy the rules
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 x + y 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
# distance > 1: difference of sums != 2
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 between 0 and 5' do
(0..5).each do |direction|
expect { ship.move_direction(direction) }.not_to raise_error
# verify the @moving vector matches the spec
expected = case direction
when 0 then [2, 0]
when 1 then [1, 1]
when 2 then [-1, 1]
when 3 then [-2, 0]
when 4 then [-1, -1]
when 5 then [1, -1]
end
expect(ship.instance_variable_get(:@moving)).to eq(expected)
end
end
it 'raises an 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
+284
View File
@@ -0,0 +1,284 @@
# Nathan Hinton 1 Oct 2025
require './planet'
# 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
def initialize(name, universe_size: 4, create_chances: {planet: 1}, seed: nil, verbose: true, debug: false)
@name = name
@universe_size = universe_size
@create_chances = create_chances
@verbose = verbose
@debug = debug
# Setup internal lists:
@player_list = []
@ship_list = []
# initizlize RNG
if seed.nil?
@random = Random.new()
@seed = @random.seed
else
@seed = seed
@random = Random.new(@seed)
end
# Set center of universe
@center = Position.new([@universe_size + 2, @universe_size ])
puts "Generating Universe '#{@name}' with seed #{@seed}" if @verbose
puts "Center is #{@center}"
@universe_map = []
# 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
# Get what the planet should be:
spot = nil
if @random.rand < @create_chances[:planet]
spot = Planet.new(@random)
end
puts "Creating something at (#{position.x_pos} #{position.y_pos})" if @debug
@universe_map[position.y_pos][position.x_pos] = spot
end # Create y points
end # Create x points
end # Function initialize
# 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
# Sanity check, our coordinates should add to be even:
if (dist_x + dist_y).odd?
raise RangeError.new('When calculating the distance we calculated a non even number. This is not allowed!')
end
res = (dist_x / 2) + dist_y
if dist_x >= dist_y
res = (dist_x + dist_y) / 2
end
puts "Distance: #{point_a} is #{point_b} #{res} units away" if @debug
return res
end # Function distance
# Prints the universe to the screen.
def print_universe(simple: false)
(0 .. (@universe_size * 2)).each do |y_pos|
# Print empty space before chart (for angles)
nextline = ''
if y_pos < @universe_size
nextline += ' ' * ((@universe_size - y_pos).abs - 1)
else
nextline += ' ' * ((@universe_size - y_pos).abs)
end
# Print more empty space for the odd places (provides offset)
if y_pos.odd?
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?
# Print empty space if we are outside the universe (For items)
if distance(@center, [x_pos, y_pos]) >= @universe_size
print ' '
next
end
# Print what we are:
position = @universe_map[y_pos][x_pos]
nextline += ' '
if [x_pos, y_pos] == @center
print 'O'
elsif position.class == 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, [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
##################################################
################### 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
def get_position(player, 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)
end
# 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)
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
##################################################
##################### 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)
@player_list.push(player)
return 'Added player to universe'
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?
return 'Failed to remove player! Does not exist in this universe!'
end
return 'Removed player from universe'
end
# Returns a list of ships owned by the player
#
# @param player [Player] The player who wants to get their ships.
def get_player_ships(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)
result = []
@planet_list.each do |planet|
if planet.owner == player
result.push(planet)
end
end
return result
end
end # Class Universe
+32
View File
@@ -0,0 +1,32 @@
# Nathan Hinton 1 Oct 2025
# Create a custom hash class for the universe.
# This will contain the universe hash inside it. It will allow for array type
# indexing as well as point type indexing
class UniverseHash
def initialize()
@universe_hash = {}
end
# Setter for values in the hash
def =[](key, value)
if key.class == Array
@universe_hash[key[0]] = {} if @universe_hash[key[0]].nil?
@universe_hash[key[0]][key[1]] = {} if @universe_hash[key[0]][key[1]].nil?
@universe_hash[key[0]][key[1]][key[2]] = value
else
raise NotImplementedError.new('You can only set based on point values.')
end
end
def [](key)
if key.class == Array
return @universe_hash[key[0]][key[1]][key[2]]
elsif key.class == Integer
return @universe_hash[key]
else
raise NotImplementedError
end
end
end