diff --git a/spec/building_spec.cr b/spec/building_spec.cr new file mode 100644 index 0000000..9456a7a --- /dev/null +++ b/spec/building_spec.cr @@ -0,0 +1,21 @@ +require "./spec_helper" + +describe Game::Building do + it "should create storehouse" do + bg = Game::Building.new Game::Building::Type::Storehouse, "Storehouse", storage: 100 + bg.storage.should eq 100 + end + + it "should create crystal miner" do + bg = Game::Building.new Game::Building::Type::CrystalMiner, "Cryslal Miner", **{ + production: Game::Production.new( + ts: 20, + input: Game::Resources.new, + output: Game::Resources.new({ + Game::Resources::Type::Crystals => 100, + }) + ), + } + bg.production.as(Game::Production).ts.should eq 20 + end +end diff --git a/spec/command_spec.cr b/spec/command_spec.cr new file mode 100644 index 0000000..634e916 --- /dev/null +++ b/spec/command_spec.cr @@ -0,0 +1,18 @@ +require "./spec_helper" + +describe Game::Command do + it "should complete build command" do + world = Game::World.new create_map_2x2 + point = Game::Point.new 1, 0 + building = Game::Building.new Game::Building::Type::StartPoint, "Dummy", **{ + construction: Game::Construction.free 10, + } + command = Game::BuildCommand.new point, building + world.push command + tile = world.map.get point + tile.should be_a(Game::ConstructionSiteTile) + world.run 10 + tile = world.map.get point + tile.should be_a(Game::BuildingTile) + end +end diff --git a/spec/resources_spec.cr b/spec/resources_spec.cr index 2770931..261e60c 100644 --- a/spec/resources_spec.cr +++ b/spec/resources_spec.cr @@ -1,8 +1,29 @@ require "./spec_helper" -describe Game::Resources do - it "should be created from hash" do - res = Game::Resources.new({Game::Resources::Type::Crystals => 100}) - res[Game::Resources::Type::Crystals].should eq 100 +module Test::GameResources + alias Res = Game::Resources + alias ResType = Game::Resources::Type + + describe Game::Resources do + it "should be created from hash" do + res = Res.new({ResType::Crystals => 100}) + res[ResType::Crystals].should eq 100 + end + + it "should check single type" do + res = Res.new({ResType::Crystals => 100}) + res.has(ResType::Crystals, 100).should be_true + end + + it "should check resource bag" do + res = Res.new({ResType::Crystals => 100}) + res.has({ResType::Crystals => 50}).should be_true + res.has({ResType::Crystals => 150}).should be_false + end + + it "should check empty value" do + res = Res.new + res.has({ResType::Crystals => 50}).should be_false + end end end diff --git a/src/game/building.cr b/src/game/building.cr new file mode 100644 index 0000000..35c1f32 --- /dev/null +++ b/src/game/building.cr @@ -0,0 +1,74 @@ +module Game + struct Production + def initialize(@ts : TimeSpan, @input : Resources, @output : Resources) + end + + getter ts + getter input + getter output + end + + struct Restoration + def initialize(@ts : TimeSpan, @type : Resources::Type, @cap : Capacity) + end + + getter ts + getter type + getter cap + end + + struct Construction + def initialize(@ts : TimeSpan, @cost : Resources, @requirements : Array(Building::Type)) + end + + getter ts + getter cost + getter requirements + + def self.immediatly + Construction.new 0, Resources.new, [] of Building::Type + end + + def self.free(ts : TimeSpan) + Construction.new ts, Resources.new, [] of Building::Type + end + end + + class Building + enum Role + Storehouse + end + + enum Type + StartPoint + Storehouse + CrystalMiner + CrystalRestorer + end + + def initialize( + @type : Type, + @name : String, + *, + roles : Array(Role) | Nil = nil, + construction : Construction | Nil = nil, + production : Production | Nil = nil, + restoration : Restoration | Nil = nil, + storage : Capacity | Nil = nil + ) + @roles = roles.nil? ? Array(Role).new : roles + @construction = construction.nil? ? Construction.immediatly : construction.as(Construction) + @production = production + @restoration = restoration + @storage = storage.nil? ? 0 : storage + end + + getter type + getter name + getter roles + getter construction + getter production + getter restoration + getter storage + end +end diff --git a/src/game/building_list.cr b/src/game/building_list.cr new file mode 100644 index 0000000..1b7dcb0 --- /dev/null +++ b/src/game/building_list.cr @@ -0,0 +1,41 @@ +module Game + class BuildingList + def initialize + @items = [] of NamedTuple(t: Building::Type, b: Building) + + add( + Building.new Building::Type::StartPoint, "Start Point", storage: 100 + ) + + add( + Building.new Building::Type::CrystalMiner, "Crystal Miner", **{ + production: Production.new( + ts: 20, + input: Resources.new, + output: Resources.new({ + Resources::Type::Crystals => 100, + }) + ), + } + ) + + add( + Building.new Building::Type::CrystalRestorer, **{ + cost: Resources.new({ + Resources::Type::Crystals => 100, + }), + restoration: Restoration.new( + ts: 30, + type: Resources::Type::Crystals, + cap: 50 + ), + } + ) + end + + private def add(building : Building) + t = building.type + @items << {t: t, b: building} + end + end +end diff --git a/src/game/command.cr b/src/game/command.cr index d264c48..4804d0a 100644 --- a/src/game/command.cr +++ b/src/game/command.cr @@ -2,18 +2,36 @@ require "./tile" module Game abstract class Command - abstract def start(world : World) : Int32 + abstract def start(world : World) : TimeSpan abstract def finish(world : World) abstract def desc : String end + class BuildCommand < Command + def initialize(@point : Point, @building : Building) + end + + def desc : String + sprintf "Building '%s'", building.name + end + + def start(world : World) : TimeSpan + world.map.set(ConstructionSiteTile.new(@point)) + @building.construction.ts + end + + def finish(world : World) + world.map.set(BuildingTile.new(@point, @building)) + end + end + class BuildCrystalHarvesterCommand < Command BUILD_TIME = 30 def initialize(@point : Point) end - def start(world : World) : Int32 + def start(world : World) : TimeSpan tile = world.map.get(@point) if !tile.can_build? raise InvalidPlaceForBuilding.new @@ -41,7 +59,7 @@ module Game @value = 0 end - def start(world : World) : Int32 + def start(world : World) : TimeSpan deposit_tile = nearest_deposit(world) stock_tile = nearest_stock(world) if deposit_tile && stock_tile @@ -83,7 +101,7 @@ module Game def initialize(@point : Point) end - def start(world : World) : Int32 + def start(world : World) : TimeSpan tile = world.map.get(@point) if !tile.can_build? raise InvalidPlaceForBuilding.new @@ -113,7 +131,7 @@ module Game def initialize(@point : Point) end - def start(world : World) : Int32 + def start(world : World) : TimeSpan @target_tile = nearest_deposit(world) if @target_tile dist = @point.distance(@target_tile.as(Tile).point) @@ -148,7 +166,7 @@ module Game def initialize(@point : Point) end - def start(world : World) : Int32 + def start(world : World) : TimeSpan tile = world.map.get(@point) if !tile.can_build? raise InvalidPlaceForBuilding.new @@ -178,7 +196,7 @@ module Game @can_terr = false end - def start(world : World) : Int32 + def start(world : World) : TimeSpan if world.resources.has(Resources::Type::Crystals, CRYSTAL_REQUIRED) world.resources.dec(Resources::Type::Crystals, CRYSTAL_REQUIRED) @can_terr = true diff --git a/src/game/game.cr b/src/game/game.cr new file mode 100644 index 0000000..eeb24e6 --- /dev/null +++ b/src/game/game.cr @@ -0,0 +1,5 @@ +module Game + alias TimePoint = Int64 + alias TimeSpan = Int32 | Int64 + alias Capacity = Int32 +end diff --git a/src/game/resources.cr b/src/game/resources.cr index e016596..b3c97e6 100644 --- a/src/game/resources.cr +++ b/src/game/resources.cr @@ -8,12 +8,17 @@ class Game::Resources alias Capacity = Int32 - def initialize - @values = {} of Type => Capacity - end + alias ResourceBag = Hash(Type, Capacity) - def initialize(vals : Hash(Type, Capacity)) - @values = vals.clone + def initialize(vals : ResourceBag | Nil = nil) + @values = {} of Type => Capacity + Type.each { |t, v| @values[t] = 0 } + if vals.is_a?(ResourceBag) + vals.each do |i| + t, v = i + @values[t] = v + end + end end def [](t : Type) @@ -21,7 +26,15 @@ class Game::Resources end def has(t : Type, value : Capacity) : Bool - @values.fetch(t, 0) >= value + @values[t] >= value + end + + def has(vs : ResourceBag) : Bool + vs.reduce true do |acc, entry| + t = entry[0] + v = entry[1] + acc && @values[t] >= v + end end def inc(t : Type, value : Capacity) @@ -32,6 +45,14 @@ class Game::Resources @values[t] = new_value end + def inc?(vs : ResourceBag) : Bool + false unless has(vs) + vs.each do |t, c| + @values[t] = @values[t] + c + end + true + end + def dec(t : Type, value : Capacity) inc(t, -value) end diff --git a/src/game/tile.cr b/src/game/tile.cr index aa0ae43..9b07243 100644 --- a/src/game/tile.cr +++ b/src/game/tile.cr @@ -7,6 +7,7 @@ module Game Plateau Terraformer Warehouse + Building end abstract class Tile @@ -20,9 +21,12 @@ module Game getter cap getter cur - abstract def letter : Char abstract def has_role(role : TileRole) : Bool + def letter : Char + ' ' + end + def withdraw(value) if value >= @cur wd = @cur @@ -118,4 +122,15 @@ module Game role == TileRole::Terraformer end end + + class BuildingTile < Tile + def initialize(@point : Point, @building : Building) + end + + def has_role(role : TileRole) : Bool + role == TileRole::Building + end + + getter building + end end diff --git a/src/game/world.cr b/src/game/world.cr index 8da4a30..5416312 100644 --- a/src/game/world.cr +++ b/src/game/world.cr @@ -1,9 +1,9 @@ require "./resources" class Game::World - property ts : Int64 + property ts : TimePoint - def initialize(@map : Map, @ts = 0_i64) + def initialize(@map : Map, @ts : TimePoint = 0_i64) @start_ts = @ts @resources = Resources.new @queue = Queue.new @@ -23,7 +23,7 @@ class Game::World @queue.push(done_at, command) end - def run(ts : Int64) + def run(ts : TimePoint) loop do item = @queue.pop(ts) if item.nil?