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)
build:
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:
crystal build src/$(NAME).cr --stats --error-trace -Dentitas_enable_logging Dentitas_debug_generator
# I did not enabled --debug because it crashes.

View File

@ -1,6 +1,6 @@
# 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)
@ -11,24 +11,20 @@ Install `git`, `sfml`, `crystal`, `make`, `imgui` (`imgui-sfml` with archlinux).
# install dependencies first
make deps
# make with imgui static linking (I think, I don't remember)
export LD_LIBRARY_PATH="$(pwd)/cimgui"
# make an optimised build so it's fasteeer
make release
## Usage
# run the
export LD_LIBRARY_PATH="$(pwd)/lib/imgui-sfml"
./core
# there is also a make rule that handle the libraries
make run
./core # run the server (high performance for data)
## Development
* See the wiki <https://git.sceptique.eu/TETU/Core/wiki>.
* See the current kanban: <https://git.sceptique.eu/TETU/Core/projects>
* Come talk on IRC **irc://irc.sceptique.eu#TETU**
* See the wiki for documentation: <https://git.sceptique.eu/TETU/Core/wiki>.
* See the current kanban for current WIP: <https://git.sceptique.eu/TETU/Core/projects>.
* Come talk on IRC: **irc://irc.sceptique.eu:6697#TETU**.
## 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:
<<: *template_default
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:
mineral:
function: linear
@ -84,7 +84,7 @@ items:
l_plant:
<<: *template_default
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:
alloy:
function: linear

View File

@ -36,7 +36,7 @@ items:
e_plant:
<<: *template_default
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:
energy:
function: linear
@ -51,7 +51,7 @@ items:
mine:
<<: *template_default
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:
energy:
function: linear
@ -62,16 +62,20 @@ items:
function: linear
coefs:
a: 10
chemical:
function: linear
coefs:
a: 0.01
wastes:
pollution:
function: linear
coefs:
a: 1
farm:
agrifood:
<<: *template_default
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:
mineral:
function: linear

View File

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

View File

@ -1,35 +1,35 @@
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:
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:
name: Synthetic Food
description: Artificialy engineered very efficient food
description: Artificialy engineered very efficient food and distribution.
mineral:
name: Simple Minerals
name: Simple Minerals in a proper concentration.
minerals2:
name: Rare Minerals
name: Rare Minerals.
chemical:
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
alloy:
name: Alloys
description: Standard alloys
description: Standards alloys for most industrial and military usages.
require: mineral
alloy2:
name: Super Materials
description: Super alloys with near-magical properties
description: Super alloys with near-magical properties compared to modern age.
require: mineral
weapon:
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
logistic:
name: Logistic
description: Civilan ships for fret and travellers
description: Civilan ships and means of transportations for high volume fret and travellers.
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
Lankiveil
Lernaeus
Luyten's Star
Luyten
Mu Arae
Muritan
Naraj
@ -53,11 +53,11 @@ Selusa
Sikun
Sun
Synchrony
Trappist
Tau Ceti
Tauri
Tegeuse
Tleilax
Trappist
Tupile
Upsilon Andromedae
Ursae Majoris
@ -65,4 +65,3 @@ Virginis
Wasp
Xo
Yz Ceti
c

View File

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

View File

@ -18,79 +18,6 @@ class TETU::Named < Entitas::Component
end
require "./game/resources"
# 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/infrastructure"
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)]
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 :foods, Food, default: DEFAULT_FOOD
MIN_RANDOM_POP = 10_000.0
MAX_RANDOM_POP = 10_000_000_000.0
MIN_RANDOM_POP = 10.0.millions
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
end
def to_s
Helpers::Numbers.humanize(number: @amount, round: 2)
def to_s(round : Int32 = 2)
Helpers::Numbers.humanize(number: @amount, round: round)
end
end

View File

@ -27,6 +27,10 @@ class TETU::Resources < Entitas::Component
def humanize(sep = "\n")
map { |k, store| "#{k}: #{store.humanize}" }.join(sep)
end
def amount_hash
transform_values { |store| store.amount }
end
end
# alias Prod = Tuple(Name, Float64)
@ -93,7 +97,7 @@ class TETU::Resources < Entitas::Component
def self.default
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

View File

@ -15,3 +15,17 @@ module TETU::Helpers::Numbers
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"
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)
@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.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|
logger.debug { "#{e.named.to_s} produces now via #{infra.id}" }
rate = prod_rate(infra, e)
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
@ -42,8 +47,16 @@ class TETU::EconomicProductionSystem < Entitas::ReactiveSystem
end
def prod_rate(infra : Resources::Infra, producer : GameEntity) : Float64
return 1.0 if infra.consumes.empty?
return 0.0 if infra.consumes.any? { |res, _value| infra.stores[res]?.nil? }
if infra.consumes.empty?
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
allocated_manpower = producer.manpower_allocation.absolute[infra.id]
maximal_rate =
@ -54,14 +67,15 @@ class TETU::EconomicProductionSystem < Entitas::ReactiveSystem
end
limited_rate = (infra.consumes.map { |res, value| infra.stores[res].amount / value } + [maximal_rate]).min
# if infra.id == "mine" || true
# logger.debug { "producer.named.name=#{producer.named.name}" }
# logger.debug { "infra.id=#{infra.id}" }
# logger.debug { "allocated_manpower=#{allocated_manpower}" }
# logger.debug { "infra.manpower.optimal=#{infra.manpower.optimal}" }
# logger.debug { "infra.manpower.min=#{infra.manpower.min} " }
# logger.debug { "maximal_rate=#{maximal_rate} " }
# logger.debug { "limited_rate=#{limited_rate}" }
# logger.debug { "" }
logger.debug { "" }
logger.debug { "producer.named.name=#{producer.named.name}" }
logger.debug { "infra.id=#{infra.id}" }
logger.debug { "allocated_manpower=#{allocated_manpower}" }
logger.debug { "infra.manpower.optimal=#{infra.manpower.optimal}" }
logger.debug { "infra.manpower.min=#{infra.manpower.min} " }
logger.debug { "maximal_rate=#{maximal_rate} " }
logger.debug { "limited_rate=#{limited_rate}" }
logger.debug { "" }
# end
limited_rate
end
@ -69,10 +83,10 @@ class TETU::EconomicProductionSystem < Entitas::ReactiveSystem
# returns the real production rate, limited by the storage
# @param rate : the maximum production we should use
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]?
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
end
@ -88,7 +102,7 @@ class TETU::EconomicProductionSystem < Entitas::ReactiveSystem
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
end

View File

@ -1,6 +1,6 @@
class TETU::GalaxyInitializerSystem
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
@ -9,6 +9,7 @@ class TETU::GalaxyInitializerSystem
EMPIRE_AMOUNT = AI_AMOUNT + 1 # add the player
AI_MIN_PLANETS = GALAXY_CONF["ai_start_populated_bodies_amount"].as_i
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
def init
@ -44,9 +45,11 @@ class TETU::GalaxyInitializerSystem
bodies_amount = Helpers::Planet::BODIES_STATISTICS.sample
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|
body_type = Helpers::Planet::TYPES_STATISTICS.sample
body_type = :planet if AI_DEBUG_0_IS_PLANET && index == 0
ids_trash[body_type] += 1
body = generate_body(star: star, index: index, body_type: body_type, ids_trash: ids_trash)
if body_type == :asteroid_belt
@ -104,19 +107,32 @@ class TETU::GalaxyInitializerSystem
body
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)
# logger.debug { "populate: #{body.named.name}..." }
pop_amount = ((10_000.0)..(10_000_000_000.0)).sample
body.add_population amount: pop_amount
pop_amount = ((10_000.0)..(10.0.billions)).sample
Population.generate_for(body)
body.replace_component(Resources.default_populated)
body.add_infrastructure_upgrades
body.add_manpower_allocation
body.manpower_allocation.available = body.population.amount
DEFAULT_INFRASTRUCTURES.each do |infra_id|
upgrade = InfrastructureUpgrade.free_instant(id: infra_id)
body.infrastructure_upgrades.upgrades << upgrade
DEFAULT_INFRASTRUCTURES.each do |infra_id, infra_level|
infra_level.times do
upgrade = InfrastructureUpgrade.free_instant(id: infra_id)
body.infrastructure_upgrades.upgrades << upgrade
end
end
# logger.debug { "populated: #{body.named.name}, now #{body.resources.to_s}, with #{body.infrastructure_upgrades.upgrades.size} upgrade to do..." }
body

View File

@ -81,14 +81,14 @@ class TETU::InfrastructureUpgradesSystem < Entitas::ReactiveSystem
end
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
costs.all? { |res, amount| resources.stores[res].amount -= amount }
upgrade.current_tick += 1
logger.debug { "paid tick upgrade" }
else
# 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
end

View File

@ -1,7 +1,7 @@
require "../components"
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)
@time_context = @contexts.time
@ -13,15 +13,62 @@ class TETU::PopulationGrowthSystem < Entitas::ReactiveSystem
end
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|
pop_amount = e.population.amount
# let's say every adult make 1.5 child average in its average lifespan (80 years)
# and one tick is one day
reproduction_rate = 1.5 * (1.0/80.0) * (1.0/365)
new_pop_amount = pop_amount + pop_amount * reproduction_rate
# logger.debug { "population growth: {reproduction_rate:#{reproduction_rate}} {population:#{e.population.to_s}} {bonus:#{pop_amount * reproduction_rate}}" }
e.replace_population(amount: new_pop_amount)
foods = e.population.foods
pop_foods_needs = foods.transform_values do |food_amount_per_pop|
food_amount_per_pop * pop_amount
end
logger.debug { "pop_foods_needs=#{pop_foods_needs}" }
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
# 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

View File

@ -8,14 +8,23 @@ class TETU::UiService::PlanetInfrastructure < TETU::UiService
def draw
if ImGui.tree_node_ex("resources panel", ImGui::ImGuiTreeNodeFlags.new(ImGui::ImGuiTreeNodeFlags::DefaultOpen))
ImGui.text @planet.named.name
draw_population
draw_resources
ImGui.tree_pop
end
end
private def draw_resources
ImGui.text @planet.named.name
private def draw_population
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?
draw_storage
ImGui.text ""