Compare commits

...

14 Commits

23 changed files with 337 additions and 154 deletions

1
.adr-dir Normal file
View File

@ -0,0 +1 @@
adr

10
.drone.yml Normal file
View File

@ -0,0 +1,10 @@
kind: pipeline
name: default
steps:
- name: test
image: crystallang/crystal:latest-alpine
environment:
commands:
- make deps
- make build
- make test

View File

@ -7,6 +7,8 @@ run: build
./$(NAME) ./$(NAME)
build: build:
crystal build src/$(NAME).cr --stats --error-trace -Dentitas_enable_logging crystal build src/$(NAME).cr --stats --error-trace -Dentitas_enable_logging
build_entitas_logging:
crystal build src/$(NAME).cr --stats --error-trace -Dentitas_enable_logging
debug: debug:
crystal build src/$(NAME).cr --stats --error-trace -Dentitas_enable_logging Dentitas_debug_generator crystal build src/$(NAME).cr --stats --error-trace -Dentitas_enable_logging Dentitas_debug_generator
# I did not enabled --debug because it crashes. # I did not enabled --debug because it crashes.

View File

@ -1,6 +1,6 @@
# TETU Core # TETU Core
A strategy & simulation game in space, inspired by Stellaris PDX A strategy & simulation game in space, inspired by Stellaris PDX.
[![Build Status](https://drone.sceptique.eu/api/badges/TETU/Core/status.svg)](https://drone.sceptique.eu/TETU/Core) [![Build Status](https://drone.sceptique.eu/api/badges/TETU/Core/status.svg)](https://drone.sceptique.eu/TETU/Core)
@ -10,25 +10,21 @@ Install `git`, `sfml`, `crystal`, `make`, `imgui` (`imgui-sfml` with archlinux).
# install dependencies first # install dependencies first
make deps make deps
# make with imgui static linking (I think, I don't remember) # make an optimised build so it's fasteeer
export LD_LIBRARY_PATH="$(pwd)/cimgui" make release
make release
## Usage ## Usage
# run the
export LD_LIBRARY_PATH="$(pwd)/lib/imgui-sfml" export LD_LIBRARY_PATH="$(pwd)/lib/imgui-sfml"
./core ./core # run the server (high performance for data)
# there is also a make rule that handle the libraries
make run
## Development ## Development
* See the wiki <https://git.sceptique.eu/TETU/Core/wiki>. * See the wiki for documentation: <https://git.sceptique.eu/TETU/Core/wiki>.
* See the current kanban: <https://git.sceptique.eu/TETU/Core/projects> * See the current kanban for current WIP: <https://git.sceptique.eu/TETU/Core/projects>.
* Come talk on IRC **irc://irc.sceptique.eu#TETU** * Come talk on IRC: **irc://irc.sceptique.eu:6697#TETU**.
## Contributing ## Contributing

View File

@ -0,0 +1,19 @@
# 1. Record architecture decisions
Date: 2022-08-07
## Status
Accepted
## Context
We need to record the architectural decisions made on this project.
## Decision
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
## Consequences
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).

23
adr/0002-use-a-ecs.md Normal file
View File

@ -0,0 +1,23 @@
# 2. Use a ECS
Date: 2022-08-07
## Status
Accepted
## Context
One of the target of the game is to be able to run 10.000 planets galaxy on a standard computer. This may require a very careful approch of the architecture. Naive implementation will very likely ends up with non-scalable galaxy size and limited planet amount.
## Decision
An ECS pattern will be used to handle most of the code. In particular the economic system that handle the planets.
The ECS is entitas.cr (see the shards.yml file).
## Consequences
Developer(s) need to know this unusual pattern.
Performance are expected very high and scalable with threads.
The architecture of most of the code will be defined and limited by the ECS pattern.

View File

@ -60,7 +60,7 @@ items:
c_plant: c_plant:
<<: *template_default <<: *template_default
title: Chemical plant title: Chemical plant
description: Series of chemical transformation machines. description: Transformation machines that can produce almost any kind of material via chemistery, such as super-acides, rare gaz, etc. using lots of basic materials.
consumes: consumes:
mineral: mineral:
function: linear function: linear
@ -84,7 +84,7 @@ items:
l_plant: l_plant:
<<: *template_default <<: *template_default
title: Logistic assembly title: Logistic assembly
description: An assembly center site that make trunks, trains, planes, and civilan space transports. description: An assembly center site that make trunks, trains, planes, and civilan space transports. It also provide a better transportation network.
consumes: consumes:
alloy: alloy:
function: linear function: linear

View File

@ -36,7 +36,7 @@ items:
e_plant: e_plant:
<<: *template_default <<: *template_default
title: Energy plant title: Energy plant
description: Energy grid that produce energy using nuclear isotops, sun, wind, and fossil fuel. description: Energy grid that produce energy using nuclear isotops, sun, wind, and fossil fuel, and also can fill energy storage means with chemistery.
prods: prods:
energy: energy:
function: linear function: linear
@ -51,7 +51,7 @@ items:
mine: mine:
<<: *template_default <<: *template_default
title: Mines title: Mines
description: Mines are polluting industry to generate tons of basic ressources required for most of the industry. description: Mines are a polluting industry that can generate tons of basic ressources required for most of the industry. Network of mines generate it's own cheminals.
consumes: consumes:
energy: energy:
function: linear function: linear
@ -62,16 +62,20 @@ items:
function: linear function: linear
coefs: coefs:
a: 10 a: 10
chemical:
function: linear
coefs:
a: 0.01
wastes: wastes:
pollution: pollution:
function: linear function: linear
coefs: coefs:
a: 1 a: 1
farm: agrifood:
<<: *template_default <<: *template_default
title: Farmed lands title: Farmed lands
description: Large portions of the land space is used to produce nutrient. It growth crops, animal farms, etc. using standard mechanized tools and vehicules. description: Large portions of the land space is used to produce nutrient. It growth crops, animal farms, etc. using standard mechanized tools and vehicules. Food can then be distributed via a dense network of distributors.
consumes: consumes:
mineral: mineral:
function: linear function: linear

View File

@ -29,7 +29,7 @@ templates:
max: max:
function: linear function: linear
coefs: coefs:
b: 10_000 b: 40_000
items: items:
@ -51,12 +51,12 @@ items:
mineral: mineral:
function: linear function: linear
coefs: coefs:
a: 10000000 a: 10000
f_store: f_store:
<<: *template_default <<: *template_default
title: Food storage title: Food storage
description: Protective centers for nutrients and food supplies description: Protective centers for nutrients and food supplies.
consumes: consumes:
energy: energy:
function: linear function: linear

View File

@ -1,35 +1,35 @@
energy: energy:
name: Energy name: Energy
description: Represents how many machines we can move at the same time. description: Represents how many machines we can move at the same time. Energy unit represents both the energy immediate usage and storage by technological means.
food: food:
name: Food name: Food
description: Standard food with low technological ehancements description: Standard food with low technological ehancements, along with means and services to distribute it properly.
food2: food2:
name: Synthetic Food name: Synthetic Food
description: Artificialy engineered very efficient food description: Artificialy engineered very efficient food and distribution.
mineral: mineral:
name: Simple Minerals name: Simple Minerals in a proper concentration.
minerals2: minerals2:
name: Rare Minerals name: Rare Minerals.
chemical: chemical:
name: Chemicals name: Chemicals
description: Usually extracted from minerals with chemistery description: Materials usually extracted from minerals with chemistery, with the infrastructure for safe storage and exploitation.
require: mineral require: mineral
alloy: alloy:
name: Alloys name: Alloys
description: Standard alloys description: Standards alloys for most industrial and military usages.
require: mineral require: mineral
alloy2: alloy2:
name: Super Materials name: Super Materials
description: Super alloys with near-magical properties description: Super alloys with near-magical properties compared to modern age.
require: mineral require: mineral
weapon: weapon:
name: Weaponery name: Weaponery
description: Bombs, guns, bullets, armors, utilitary, tanks, fighters description: Bombs, guns, bullets, armors, utilitary, tanks, fighters, artillery, all the things that are small enough to fit in a carrier.
require: alloy require: alloy
logistic: logistic:
name: Logistic name: Logistic
description: Civilan ships for fret and travellers description: Civilan ships and means of transportations for high volume fret and travellers.
pollution: pollution:
name: Pollution name: Pollution
description: Represent the wastes of industrial activities, that is stored on the planet more or less randomly. description: Represent the wastes of industrial activities, that is stored on the planet more or less efficiently and have a negative impact on productivity and life.

View File

@ -36,7 +36,7 @@ Lalande
Lampadas Lampadas
Lankiveil Lankiveil
Lernaeus Lernaeus
Luyten's Star Luyten
Mu Arae Mu Arae
Muritan Muritan
Naraj Naraj
@ -53,11 +53,11 @@ Selusa
Sikun Sikun
Sun Sun
Synchrony Synchrony
Trappist
Tau Ceti Tau Ceti
Tauri Tauri
Tegeuse Tegeuse
Tleilax Tleilax
Trappist
Tupile Tupile
Upsilon Andromedae Upsilon Andromedae
Ursae Majoris Ursae Majoris
@ -65,4 +65,3 @@ Virginis
Wasp Wasp
Xo Xo
Yz Ceti Yz Ceti
c

View File

@ -1,9 +1,7 @@
require "./spec_helper" require "./spec_helper"
describe Core do describe TETU do
# TODO: Write tests
it "works" do it "works" do
false.should eq(true) true.should eq(true)
end end
end end

View File

@ -18,79 +18,6 @@ class TETU::Named < Entitas::Component
end end
require "./game/resources" require "./game/resources"
require "./game/infrastructure"
# TODO: why is it not a component ????? should fix that
class TETU::InfrastructureUpgrade
spoved_logger level: :info, io: STDOUT, bind: true
alias Costs = Hash(Resources::Name, Float64)
property id : String
property costs_by_tick : Costs
property costs_start : Costs
property end_tick : TETU::Tick
property current_tick : TETU::Tick
@finished = false
def to_s
"#{@id} (#{@current_tick}/#{@end_tick})"
end
def finished?
@finished
end
def finish!
@finished = true
end
def initialize(@id, @costs_by_tick, @costs_start, @end_tick, @current_tick = 0i64)
end
def self.from_infrastructure(id : String, tier : Int32)
# TODO: must read the properties of the blueprints to define the costs
free_instant(id)
end
def self.free_instant(id : String)
new(
id: id,
costs_by_tick: Costs.new,
costs_start: Costs.new,
end_tick: 0i64,
current_tick: 0i64,
)
end
spoved_logger level: :info, io: STDOUT, bind: true
def self.from_blueprint(infra_id : String, tier : Number)
blueprint = Helpers::InfrastructuresFileLoader.all[infra_id]
total_costs = blueprint.build.costs.transform_values { |f| f.execute(tier) }
upfront_costs = total_costs.transform_values { |v| v * blueprint.build.upfront }
duration = blueprint.build.duration.execute(tier)
tick_costs = total_costs.transform_values { |v| v * (1.0 - blueprint.build.upfront) / duration }
logger.debug { "" }
logger.debug { "> Create from blueprint" }
upgrade = new(
id: infra_id,
costs_by_tick: tick_costs,
costs_start: upfront_costs,
end_tick: duration.to_i64,
current_tick: 0i64,
)
logger.debug { {blueprint: blueprint} }
logger.debug { {upgrade: upgrade} }
logger.debug { "" }
upgrade
end
end
@[Context(Game)]
class TETU::InfrastructureUpgrades < Entitas::Component
prop :upgrades, Array(InfrastructureUpgrade), default: Array(InfrastructureUpgrade).new
def to_s
"InfrastructureUpgrades: #{upgrades.map(&.to_s)}"
end
end
require "./game/*" require "./game/*"

View File

@ -0,0 +1,73 @@
# TODO: why is it not a component ????? should fix that
class TETU::InfrastructureUpgrade
spoved_logger level: :info, io: STDOUT, bind: true
alias Costs = Hash(Resources::Name, Float64)
property id : String
property costs_by_tick : Costs
property costs_start : Costs
property end_tick : TETU::Tick
property current_tick : TETU::Tick
@finished = false
def to_s
"#{@id} (#{@current_tick}/#{@end_tick})"
end
def finished?
@finished
end
def finish!
@finished = true
end
def initialize(@id, @costs_by_tick, @costs_start, @end_tick, @current_tick = 0i64)
end
def self.from_infrastructure(id : String, tier : Int32)
# TODO: must read the properties of the blueprints to define the costs
free_instant(id)
end
def self.free_instant(id : String)
new(
id: id,
costs_by_tick: Costs.new,
costs_start: Costs.new,
end_tick: 0i64,
current_tick: 0i64,
)
end
spoved_logger level: :info, io: STDOUT, bind: true
def self.from_blueprint(infra_id : String, tier : Number)
blueprint = Helpers::InfrastructuresFileLoader.all[infra_id]
total_costs = blueprint.build.costs.transform_values { |f| f.execute(tier) }
upfront_costs = total_costs.transform_values { |v| v * blueprint.build.upfront }
duration = blueprint.build.duration.execute(tier)
tick_costs = total_costs.transform_values { |v| v * (1.0 - blueprint.build.upfront) / duration }
logger.debug { "" }
logger.debug { "> Create from blueprint" }
upgrade = new(
id: infra_id,
costs_by_tick: tick_costs,
costs_start: upfront_costs,
end_tick: duration.to_i64,
current_tick: 0i64,
)
logger.debug { {blueprint: blueprint} }
logger.debug { {upgrade: upgrade} }
logger.debug { "" }
upgrade
end
end
@[Context(Game)]
class TETU::InfrastructureUpgrades < Entitas::Component
prop :upgrades, Array(InfrastructureUpgrade), default: Array(InfrastructureUpgrade).new
def to_s
"InfrastructureUpgrades: #{upgrades.map(&.to_s)}"
end
end

View File

@ -1,15 +1,19 @@
@[Context(Game)] @[Context(Game)]
class TETU::Population < Entitas::Component class TETU::Population < Entitas::Component
alias Food = Hash(String, Float64)
DEFAULT_FOOD = { "food" => 1.0/100.0.millions }
prop :amount, Float64, default: 0.0 prop :amount, Float64, default: 0.0
prop :foods, Food, default: DEFAULT_FOOD
MIN_RANDOM_POP = 10_000.0 MIN_RANDOM_POP = 10.0.millions
MAX_RANDOM_POP = 10_000_000_000.0 MAX_RANDOM_POP = 10.0.billions
def self.generate(entity) def self.generate_for(entity)
entity.add_population amount: (MIN_RANDOM_POP..MAX_RANDOM_POP).sample.round entity.add_population amount: (MIN_RANDOM_POP..MAX_RANDOM_POP).sample.round
end end
def to_s def to_s(round : Int32 = 2)
Helpers::Numbers.humanize(number: @amount, round: 2) Helpers::Numbers.humanize(number: @amount, round: round)
end end
end end

View File

@ -27,6 +27,10 @@ class TETU::Resources < Entitas::Component
def humanize(sep = "\n") def humanize(sep = "\n")
map { |k, store| "#{k}: #{store.humanize}" }.join(sep) map { |k, store| "#{k}: #{store.humanize}" }.join(sep)
end end
def amount_hash
transform_values { |store| store.amount }
end
end end
# alias Prod = Tuple(Name, Float64) # alias Prod = Tuple(Name, Float64)
@ -93,7 +97,7 @@ class TETU::Resources < Entitas::Component
def self.default def self.default
stores = Stores.new stores = Stores.new
stores["pollution"] = Store.new(amount: 0.0, max: 1_000_000.0) stores["pollution"] = Store.new(amount: 0.0, max: 1.0.millions)
infras = Infras.new infras = Infras.new

View File

@ -15,3 +15,17 @@ module TETU::Helpers::Numbers
end end
end end
end end
struct Number
def billions
self * TETU::Helpers::Numbers::BILLION
end
def millions
self * TETU::Helpers::Numbers::MILLION
end
def thousands
self * TETU::Helpers::Numbers::THOUSAND
end
end

23
src/helpers/ratio.cr Normal file
View File

@ -0,0 +1,23 @@
module Ratio
alias IdValue = Hash(String, Number)
# minimum ratio of right/left, or nil if missing keys in right
def self.minimum_reverse(left : IdValue, right : IdValue)
return nil if left.keys & right.keys != left.keys
left.map do |id, left_value|
right_value = right[id]
right_value / left_value
end.min
end
# minimum ratio of left/right, or nil if missing keys in right
def self.minimum(left : IdValue, right : IdValue)
return nil if left.keys & right.keys != left.keys
left.map do |id, left_value|
right_value = right[id]
left_value / right_value
end.min
end
end

View File

@ -1,7 +1,7 @@
require "../components" require "../components"
class TETU::EconomicProductionSystem < Entitas::ReactiveSystem class TETU::EconomicProductionSystem < Entitas::ReactiveSystem
spoved_logger level: :info, io: STDOUT, bind: true spoved_logger level: :debug, io: STDOUT, bind: true
def initialize(@contexts : Contexts) def initialize(@contexts : Contexts)
@time_context = @contexts.time @time_context = @contexts.time
@ -21,9 +21,14 @@ class TETU::EconomicProductionSystem < Entitas::ReactiveSystem
producer_group = @contexts.game.get_group Entitas::Matcher.all_of(Resources, Population, ManpowerAllocation) producer_group = @contexts.game.get_group Entitas::Matcher.all_of(Resources, Population, ManpowerAllocation)
producer_group.entities.each do |e| producer_group.entities.each do |e|
next if !e.resources.can_produce? if !e.resources.can_produce?
logger.debug { "#{e.named} cannot produces" }
next
end
logger.debug { "#{e.named} produces now" }
e.resources.infras.each do |infra_id, infra| e.resources.infras.each do |infra_id, infra|
logger.debug { "#{e.named.to_s} produces now via #{infra.id}" }
rate = prod_rate(infra, e) rate = prod_rate(infra, e)
prod_rates = infra.prods.map { |res, prod| apply_prod(infra: infra, res: res, rate: rate, prod: prod) } prod_rates = infra.prods.map { |res, prod| apply_prod(infra: infra, res: res, rate: rate, prod: prod) }
real_prod_rate = prod_rates.empty? ? rate : prod_rates.max real_prod_rate = prod_rates.empty? ? rate : prod_rates.max
@ -42,8 +47,16 @@ class TETU::EconomicProductionSystem < Entitas::ReactiveSystem
end end
def prod_rate(infra : Resources::Infra, producer : GameEntity) : Float64 def prod_rate(infra : Resources::Infra, producer : GameEntity) : Float64
return 1.0 if infra.consumes.empty? if infra.consumes.empty?
return 0.0 if infra.consumes.any? { |res, _value| infra.stores[res]?.nil? } logger.debug { "no consumption, rate 1.0" }
return 1.0
end
if infra.consumes.any? { |res, _value| infra.stores[res]?.nil? }
logger.debug { "missing consumable, 0.0" }
return 0.0
end
# TODO: another function for pop.amount < manpower.optimal # TODO: another function for pop.amount < manpower.optimal
allocated_manpower = producer.manpower_allocation.absolute[infra.id] allocated_manpower = producer.manpower_allocation.absolute[infra.id]
maximal_rate = maximal_rate =
@ -54,14 +67,15 @@ class TETU::EconomicProductionSystem < Entitas::ReactiveSystem
end end
limited_rate = (infra.consumes.map { |res, value| infra.stores[res].amount / value } + [maximal_rate]).min limited_rate = (infra.consumes.map { |res, value| infra.stores[res].amount / value } + [maximal_rate]).min
# if infra.id == "mine" || true # if infra.id == "mine" || true
# logger.debug { "producer.named.name=#{producer.named.name}" } logger.debug { "" }
# logger.debug { "infra.id=#{infra.id}" } logger.debug { "producer.named.name=#{producer.named.name}" }
# logger.debug { "allocated_manpower=#{allocated_manpower}" } logger.debug { "infra.id=#{infra.id}" }
# logger.debug { "infra.manpower.optimal=#{infra.manpower.optimal}" } logger.debug { "allocated_manpower=#{allocated_manpower}" }
# logger.debug { "infra.manpower.min=#{infra.manpower.min} " } logger.debug { "infra.manpower.optimal=#{infra.manpower.optimal}" }
# logger.debug { "maximal_rate=#{maximal_rate} " } logger.debug { "infra.manpower.min=#{infra.manpower.min} " }
# logger.debug { "limited_rate=#{limited_rate}" } logger.debug { "maximal_rate=#{maximal_rate} " }
# logger.debug { "" } logger.debug { "limited_rate=#{limited_rate}" }
logger.debug { "" }
# end # end
limited_rate limited_rate
end end
@ -69,10 +83,10 @@ class TETU::EconomicProductionSystem < Entitas::ReactiveSystem
# returns the real production rate, limited by the storage # returns the real production rate, limited by the storage
# @param rate : the maximum production we should use # @param rate : the maximum production we should use
def apply_prod(infra : Resources::Infra, rate : Float64, res : Resources::Name, prod : Float64) : Float64 def apply_prod(infra : Resources::Infra, rate : Float64, res : Resources::Name, prod : Float64) : Float64
# logger.debug { "apply_prod wants rate=#{rate} res=#{res} prod=#{prod}" } logger.debug { "apply_prod wants rate=#{rate} res=#{res} prod=#{prod}" }
store = infra.stores[res]? store = infra.stores[res]?
if store.nil? || rate > 0 && store.amount == store.max if store.nil? || rate > 0 && store.amount == store.max
# logger.debug { "apply_prod applied rate=0.0 res=#{res} prod=#{prod}" } logger.debug { "max storage: apply_prod applied rate=0.0 res=#{res} prod=#{prod}" }
return 0.0 return 0.0
end end
@ -88,7 +102,7 @@ class TETU::EconomicProductionSystem < Entitas::ReactiveSystem
store.amount = new_amount store.amount = new_amount
# logger.debug { "apply_prod applied rate=#{rate} res=#{res} prod=#{prod}" } logger.debug { "ok: apply_prod applied rate=#{rate} res=#{res} prod=#{prod}, store=#{new_amount}" }
return rate return rate
end end

View File

@ -1,6 +1,6 @@
class TETU::GalaxyInitializerSystem class TETU::GalaxyInitializerSystem
include Entitas::Systems::InitializeSystem include Entitas::Systems::InitializeSystem
spoved_logger level: :info, io: STDOUT, bind: true spoved_logger level: :debug, io: STDOUT, bind: true
def initialize(@contexts : Contexts); end def initialize(@contexts : Contexts); end
@ -9,6 +9,7 @@ class TETU::GalaxyInitializerSystem
EMPIRE_AMOUNT = AI_AMOUNT + 1 # add the player EMPIRE_AMOUNT = AI_AMOUNT + 1 # add the player
AI_MIN_PLANETS = GALAXY_CONF["ai_start_populated_bodies_amount"].as_i AI_MIN_PLANETS = GALAXY_CONF["ai_start_populated_bodies_amount"].as_i
PLANET_POP_PROBA = TETU::GALAXY_CONF["populated_planets_proba"].as_f PLANET_POP_PROBA = TETU::GALAXY_CONF["populated_planets_proba"].as_f
AI_DEBUG_0_IS_PLANET = GALAXY_CONF["ai_debug_0_is_planet"].as_bool?
# NO_SPACE_EMPIRE_ID = 100001 # NO_SPACE_EMPIRE_ID = 100001
def init def init
@ -44,9 +45,11 @@ class TETU::GalaxyInitializerSystem
bodies_amount = Helpers::Planet::BODIES_STATISTICS.sample bodies_amount = Helpers::Planet::BODIES_STATISTICS.sample
bodies_amount = AI_MIN_PLANETS if !empire_id.nil? && bodies_amount < AI_MIN_PLANETS bodies_amount = AI_MIN_PLANETS if !empire_id.nil? && bodies_amount < AI_MIN_PLANETS
logger.debug { "generate bodies_amount=#{bodies_amount}" }
bodies_amount.times.map do |index| bodies_amount.times.map do |index|
body_type = Helpers::Planet::TYPES_STATISTICS.sample body_type = Helpers::Planet::TYPES_STATISTICS.sample
body_type = :planet if AI_DEBUG_0_IS_PLANET && index == 0
ids_trash[body_type] += 1 ids_trash[body_type] += 1
body = generate_body(star: star, index: index, body_type: body_type, ids_trash: ids_trash) body = generate_body(star: star, index: index, body_type: body_type, ids_trash: ids_trash)
if body_type == :asteroid_belt if body_type == :asteroid_belt
@ -104,19 +107,32 @@ class TETU::GalaxyInitializerSystem
body body
end end
DEFAULT_INFRASTRUCTURES = %w[e_store m_store f_store e_plant mine farm a_store l_store a_plant l_plant] DEFAULT_INFRASTRUCTURES = {
"e_store" => 2,
"m_store" => 2,
"f_store" => 5,
"e_plant" => 2,
"mine" => 2,
"agrifood" => 5,
"a_store" => 1,
"l_store" => 1,
"a_plant" => 1,
"l_plant" => 1,
}
def populate(body) def populate(body)
# logger.debug { "populate: #{body.named.name}..." } # logger.debug { "populate: #{body.named.name}..." }
pop_amount = ((10_000.0)..(10_000_000_000.0)).sample pop_amount = ((10_000.0)..(10.0.billions)).sample
body.add_population amount: pop_amount Population.generate_for(body)
body.replace_component(Resources.default_populated) body.replace_component(Resources.default_populated)
body.add_infrastructure_upgrades body.add_infrastructure_upgrades
body.add_manpower_allocation body.add_manpower_allocation
body.manpower_allocation.available = body.population.amount body.manpower_allocation.available = body.population.amount
DEFAULT_INFRASTRUCTURES.each do |infra_id| DEFAULT_INFRASTRUCTURES.each do |infra_id, infra_level|
upgrade = InfrastructureUpgrade.free_instant(id: infra_id) infra_level.times do
body.infrastructure_upgrades.upgrades << upgrade upgrade = InfrastructureUpgrade.free_instant(id: infra_id)
body.infrastructure_upgrades.upgrades << upgrade
end
end end
# logger.debug { "populated: #{body.named.name}, now #{body.resources.to_s}, with #{body.infrastructure_upgrades.upgrades.size} upgrade to do..." } # logger.debug { "populated: #{body.named.name}, now #{body.resources.to_s}, with #{body.infrastructure_upgrades.upgrades.size} upgrade to do..." }
body body

View File

@ -81,14 +81,14 @@ class TETU::InfrastructureUpgradesSystem < Entitas::ReactiveSystem
end end
def pay_upgrade_tick(resources : Resources, upgrade : InfrastructureUpgrade, costs : InfrastructureUpgrade::Costs) def pay_upgrade_tick(resources : Resources, upgrade : InfrastructureUpgrade, costs : InfrastructureUpgrade::Costs)
if costs.all? { |res, amount| resources.stores[res].amount >= amount } if !(missing_resource = costs.find { |res, amount| resources.stores[res].amount < amount })
# pay the upgrade with local store # pay the upgrade with local store
costs.all? { |res, amount| resources.stores[res].amount -= amount } costs.all? { |res, amount| resources.stores[res].amount -= amount }
upgrade.current_tick += 1 upgrade.current_tick += 1
logger.debug { "paid tick upgrade" } logger.debug { "paid tick upgrade" }
else else
# if we can't pay the upgrade, we will "loose" one tick due to maintenance # if we can't pay the upgrade, we will "loose" one tick due to maintenance
logger.debug { "cannot pay upgrade" } logger.debug { "cannot pay upgrade because #{missing_resource}" }
upgrade.end_tick += 1 upgrade.end_tick += 1
end end

View File

@ -1,7 +1,7 @@
require "../components" require "../components"
class TETU::PopulationGrowthSystem < Entitas::ReactiveSystem class TETU::PopulationGrowthSystem < Entitas::ReactiveSystem
spoved_logger level: :info, io: STDOUT, bind: true spoved_logger level: :debug, io: STDOUT, bind: true
def initialize(@contexts : Contexts) def initialize(@contexts : Contexts)
@time_context = @contexts.time @time_context = @contexts.time
@ -13,15 +13,62 @@ class TETU::PopulationGrowthSystem < Entitas::ReactiveSystem
end end
def execute(time_entities : Array(Entitas::IEntity)) def execute(time_entities : Array(Entitas::IEntity))
populateds = @contexts.game.get_group Entitas::Matcher.all_of(Population) populateds = @contexts.game.get_group Entitas::Matcher.all_of(Population, Resources)
populateds.entities.each do |e| populateds.entities.each do |e|
pop_amount = e.population.amount pop_amount = e.population.amount
# let's say every adult make 1.5 child average in its average lifespan (80 years) foods = e.population.foods
# and one tick is one day pop_foods_needs = foods.transform_values do |food_amount_per_pop|
reproduction_rate = 1.5 * (1.0/80.0) * (1.0/365) food_amount_per_pop * pop_amount
new_pop_amount = pop_amount + pop_amount * reproduction_rate end
# logger.debug { "population growth: {reproduction_rate:#{reproduction_rate}} {population:#{e.population.to_s}} {bonus:#{pop_amount * reproduction_rate}}" } logger.debug { "pop_foods_needs=#{pop_foods_needs}" }
e.replace_population(amount: new_pop_amount) logger.debug { "resouces=#{e.resources.stores}" }
minimum_food_ratio = Ratio.minimum_reverse(pop_foods_needs, e.resources.stores.amount_hash) || 0.0
logger.debug { "minimum_food_ratio=#{minimum_food_ratio}" }
total_food_modifier = food_modifier(minimum_food_ratio)
logger.debug { "total_food_modifier=#{total_food_modifier}" }
new_pop_amount = pop_amount + pop_amount * (reproduction_rate(total_food_modifier))
logger.debug { "population growth: #{pop_amount} * #{new_pop_amount / pop_amount} => #{new_pop_amount}" }
e.replace_population(amount: new_pop_amount, foods: foods)
pop_foods_needs.each do |food_id, food_need|
# only consumes the need, only "theorical availability" counts, like a richness factor
food_consumption = minimum_food_ratio < 1.0 ? food_need * minimum_food_ratio : food_need
logger.debug { "population food consumption: #{food_id}:#{food_need} -> consumes #{food_consumption}" }
e.resources.stores[food_id].amount -= food_consumption
end if minimum_food_ratio > 0.0
end end
end end
# we also add a modifier based on food availability
# that can be between -5 for famine with 0 food
# +0.0 if food required is reach no less no more
# up to 1.0 for post-scarcity inifity food (5x food required = +0.8)
def food_modifier(available_food_ratio : Float64)
return -5.0 if available_food_ratio <= 0.0
modifier = 1.0 / -available_food_ratio + 1.0
if modifier < -5.0
-5.0 # not worse than -5
else
modifier
end
end
# let's say every adult make 1.5 child average in its average lifespan (80 years)
# and one tick is one day (3 children per couple).
def reproduction_rate(food_modifier) : Float64
(children_per_pop_average + food_modifier) * (1.0/pop_lifespan_average) * (1.0/365.0)
end
def pop_per_food_per_tick : Float64
100.0.millions
end
def children_per_pop_average : Float64
1.5
end
def pop_lifespan_average : Float64
80.0
end
end end

View File

@ -8,14 +8,23 @@ class TETU::UiService::PlanetInfrastructure < TETU::UiService
def draw def draw
if ImGui.tree_node_ex("resources panel", ImGui::ImGuiTreeNodeFlags.new(ImGui::ImGuiTreeNodeFlags::DefaultOpen)) if ImGui.tree_node_ex("resources panel", ImGui::ImGuiTreeNodeFlags.new(ImGui::ImGuiTreeNodeFlags::DefaultOpen))
ImGui.text @planet.named.name
draw_population
draw_resources draw_resources
ImGui.tree_pop ImGui.tree_pop
end end
end end
private def draw_resources private def draw_population
ImGui.text @planet.named.name ImGui.text "Population:" + if @planet.has_population?
@planet.population.to_s(round: 4)
else
"None"
end
end
private def draw_resources
if @planet.has_resources? if @planet.has_resources?
draw_storage draw_storage
ImGui.text "" ImGui.text ""