Skip to content

Commit 1b8bd35

Browse files
committed
Model actions in game as actions with immediate effect or events processed in the future
1 parent 9f59a49 commit 1b8bd35

17 files changed

+472
-25
lines changed

lib/galaxies/building.ex

-3
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ defmodule Galaxies.Building do
1616

1717
field :image_src, :string
1818

19-
field :upgrade_time_formula, :string
2019
field :upgrade_cost_formula, :string
21-
field :production_formula, :string
22-
field :energy_consumption_formula, :string
2320

2421
field :list_order, :integer
2522

lib/galaxies/buildings.ex

+8-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ defmodule Galaxies.Buildings do
33
Utility functions related to buildings, including building cost, duration and occupied fields.
44
"""
55

6-
# TODO: Maybe read hardcoded IDs from file or Application.compile_env?
7-
@production_building_ids [1, 2, 3, 4, 5]
6+
# Maybe read hardcoded IDs from file or Application.compile_env?
7+
@resource_production_building_ids [1, 2, 3]
8+
@energy_production_building_ids [4, 5]
9+
@production_building_ids @resource_production_building_ids ++ @energy_production_building_ids
810
@terraformer_building_id 13
911

1012
def production_building?(building_id) do
1113
building_id in @production_building_ids
1214
end
1315

16+
def resource_production_building?(building_id) do
17+
building_id in @resource_production_building_ids
18+
end
19+
1420
@doc """
1521
Determines the increase in used fields when upgrading a building.
1622
An upgrade will increase the number of used fields by 1 while a downgrade

lib/galaxies/fleet.ex

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule Galaxies.Fleet do
2+
use Galaxies.Schema
3+
4+
@moduledoc """
5+
A fleet in group of ships (units) traveling to a destination.
6+
A fleet always originates from an existing planet, but multiple mission types
7+
allow for destinations which are not planets (e.g. exploration, colonization).
8+
"""
9+
10+
@orbits [planet: 1, moon: 2, debris_field: 3]
11+
12+
schema "fleets" do
13+
field :destination_galaxy, :integer
14+
field :destination_system, :integer
15+
field :destination_slot, :integer
16+
field :destination_orbit, Ecto.Enum, values: @orbits
17+
18+
field :cargo_metal_units, :integer
19+
field :cargo_crystal_units, :integer
20+
field :cargo_deuterium_units, :integer
21+
field :cargo_dark_matter_units, :integer
22+
field :cargo_artifacts, :map
23+
24+
field :arriving_at, :utc_datetime_usec
25+
field :returning_at, :utc_datetime_usec
26+
27+
belongs_to :origin_planet, Galaxies.Planet
28+
29+
has_many :fleet_ships, Galaxies.FleetShip
30+
31+
timestamps(type: :utc_datetime_usec)
32+
end
33+
end

lib/galaxies/fleet_ship.ex

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule Galaxies.FleetShip do
2+
use Galaxies.Schema
3+
4+
schema "fleet_ships" do
5+
belongs_to :fleet, Galaxies.FleetInMotion
6+
belongs_to :ship, Galaxies.Unit
7+
8+
field :amount, :integer
9+
end
10+
end

lib/galaxies/formulas/buildings.ex

+29
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,35 @@ defmodule Galaxies.Formulas.Buildings do
99
universe_speed: @universe_speed
1010
}
1111

12+
@metal_mine_building_id 1
13+
@crystal_mine_building_id 2
14+
@deuterium_synthesizer_building_id 3
15+
@solar_plant_building_id 4
16+
@fusion_reactor_building_id 12
17+
18+
@doc """
19+
Returns the total energy consumption of a particular building at a given level.
20+
"""
21+
def energy_consumption(@metal_mine_building_id, level),
22+
do: trunc(10 * level * :math.pow(1.1, level))
23+
24+
def energy_consumption(@crystal_mine_building_id, level),
25+
do: trunc(10 * level * :math.pow(1.1, level))
26+
27+
def energy_consumption(@deuterium_synthesizer_building_id, level),
28+
do: trunc(20 * level * :math.pow(1.1, level))
29+
30+
def energy_consumption(_building_id, _level), do: 0
31+
32+
@doc """
33+
Returns the total energy production of a particular building at a given level.
34+
"""
35+
def energy_production(@solar_plant_building_id, level, _energy_level),
36+
do: trunc(20 * level * :math.pow(1.1, level))
37+
38+
def energy_production(@fusion_reactor_building_id, level, energy_level),
39+
do: trunc(30 * level * :math.pow(1.05 + energy_level * 0.01, level))
40+
1241
def construction_time_seconds(building_id, level, robotics_level, nanites_level) do
1342
{metal, crystal, _deuterium, _energy} = upgrade_cost(building_id, level)
1443

lib/galaxies/planets/action.ex

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule Galaxies.Planets.Action do
2+
@moduledoc """
3+
An action is a command that a player performs on a planet that is processed immediately.
4+
"""
5+
6+
@callback perform(%Galaxies.Accounts.Player{}, %Galaxies.Planets.PlanetAction{}) ::
7+
{:ok, map()} | {:error, map()}
8+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
defmodule Galaxies.Planets.Actions.EnqueueBuilding do
2+
@behaviour Galaxies.Planets.Action
3+
4+
import Ecto.Query
5+
6+
require Logger
7+
alias Galaxies.Prerequisites
8+
alias Galaxies.Planets.PlanetEvent
9+
alias Galaxies.Planets.PlanetAction
10+
11+
@building_queue_max_size 5
12+
13+
def perform(player, %PlanetAction{type: :enqueue_building} = action) do
14+
%{planet_id: planet_id, data: %{building_id: building_id, demolish: demolish}} = action.data
15+
16+
Ecto.Multi.new()
17+
|> Ecto.Multi.run(:planet, fn repo, _changes ->
18+
planet = repo.one!(from p in Planet, where: p.id == ^planet_id)
19+
20+
if planet.player_id != player.id do
21+
Logger.notice(
22+
"Player #{player.id} tried to build on a planet that does not belong to them (planet_id: #{planet_id})"
23+
)
24+
25+
{:error, :invalid_player_action_build_on_other_player_planet}
26+
else
27+
{:ok, planet}
28+
end
29+
end)
30+
|> Ecto.Multi.run(:build_queue, fn repo, _changes ->
31+
build_queue =
32+
repo.all(
33+
from pe in PlanetEvent,
34+
where:
35+
pe.planet_id == ^planet_id and pe.type == ^:construction_complete and
36+
not pe.is_processed and not pe.is_cancelled,
37+
order_by: [asc: pe.inserted_at]
38+
)
39+
40+
if length(build_queue) >= @building_queue_max_size do
41+
{:error, :building_queue_full}
42+
else
43+
{:ok, build_queue}
44+
end
45+
end)
46+
|> Ecto.Multi.run(:enqueue_building, fn repo, %{build_queue: queue, planet: planet} ->
47+
base_event =
48+
PlanetEvent.changeset(%PlanetEvent{}, %{
49+
planet_id: planet_id,
50+
type: :construction_complete,
51+
is_processed: false,
52+
is_cancelled: false,
53+
building_event: %{
54+
building_id: building_id,
55+
demolish: demolish
56+
}
57+
})
58+
59+
if Enum.empty?(queue) do
60+
# no buildings in progress, check for prerequisites
61+
planet = repo.preload(planet, [:buildings])
62+
63+
64+
65+
if planet.used_fields >= planet.total_fields do
66+
{:error, :not_enough_planet_fields}
67+
else
68+
building = Enum.find(planet.buildings, fn b -> b.id == building_id end)
69+
70+
if building do
71+
{:error, :building_already_exists}
72+
else
73+
{:ok, base_event}
74+
end
75+
end
76+
else
77+
# adding the building to the end of the queue
78+
repo.insert(base_event)
79+
end
80+
end)
81+
|> Repo.transaction()
82+
83+
building_queue = get_building_queue(planet_id)
84+
85+
case length(building_queue) do
86+
length when length >= @building_queue_max_size ->
87+
{:error, :building_queue_full}
88+
89+
0 ->
90+
# no other buildings in progress, subtract building cost
91+
planet_buildings =
92+
Repo.all(
93+
from pb in PlanetBuilding,
94+
where: pb.planet_id == ^planet_id,
95+
select: pb,
96+
preload: [:building]
97+
)
98+
99+
planet_building = Enum.find(planet_buildings, fn pb -> pb.building_id == building_id end)
100+
101+
{cost_metal, cost_crystal, cost_deuterium, _energy} =
102+
Galaxies.calc_upgrade_cost(planet_building.building.upgrade_cost_formula, level)
103+
104+
%{
105+
metal_units: planet_metal,
106+
crystal_units: planet_crystal,
107+
deuterium_units: planet_deuterium
108+
} =
109+
_planet =
110+
Repo.one!(
111+
from p in Planet,
112+
where: p.id == ^planet_id
113+
)
114+
115+
if planet_metal >= cost_metal and planet_crystal >= cost_crystal and
116+
planet_deuterium >= cost_deuterium do
117+
add_resources(planet_id, -cost_metal, -cost_crystal, -cost_deuterium)
118+
119+
# TODO: replace with a real formula
120+
construction_duration_seconds = :math.pow(2, level) |> trunc()
121+
122+
event_id = Ecto.UUID.generate()
123+
now = DateTime.utc_now(:second)
124+
125+
Repo.insert!(%PlanetEvent{
126+
planet_id: planet_id,
127+
completed_at: DateTime.add(now, construction_duration_seconds),
128+
type: :building_construction,
129+
building_event: %BuildingEvent{
130+
id: event_id,
131+
list_order: 1,
132+
planet_id: planet_id,
133+
building_id: building_id,
134+
level: level,
135+
demolish: false,
136+
started_at: now,
137+
completed_at: DateTime.add(now, construction_duration_seconds)
138+
}
139+
})
140+
141+
:ok
142+
else
143+
{:error, :not_enough_resources}
144+
end
145+
146+
_ ->
147+
# adding at the end of the queue which means level could be wrong
148+
# (e.g. trying to update metal mine twice from level 10 will yield 2 events with level = 11)
149+
# so we need to set level to a proper value (highest in queue + 1)
150+
{level, list_order} =
151+
Enum.reduce(building_queue, {level, 1}, fn %EnqueuedBuilding{
152+
list_order: lo,
153+
building_id: b_id,
154+
level: lvl
155+
},
156+
{acc_level, acc_order} ->
157+
level =
158+
if b_id == building_id and lvl >= acc_level do
159+
lvl + 1
160+
else
161+
acc_level
162+
end
163+
164+
order =
165+
if lo >= acc_order do
166+
lo + 1
167+
else
168+
acc_order
169+
end
170+
171+
{level, order}
172+
end)
173+
174+
# TODO: replace with a real formula
175+
construction_duration_seconds = :math.pow(2, level) |> trunc()
176+
177+
now = DateTime.utc_now(:second)
178+
179+
# no PlanetEvent is inserted because we are inserting at the end of the queue,
180+
# so an event already exists to fetch from the head of the queue.
181+
Repo.insert!(%EnqueuedBuilding{
182+
planet_id: planet_id,
183+
list_order: list_order,
184+
building_id: building_id,
185+
level: level,
186+
demolish: false,
187+
started_at: now,
188+
completed_at: DateTime.add(now, construction_duration_seconds, :second)
189+
})
190+
191+
:ok
192+
end
193+
end
194+
end

lib/galaxies/planets/event.ex

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
defmodule Galaxies.Planets.Event do
2+
@moduledoc """
3+
A planet event is an event that occurs on a planet,
4+
typically some time after an action was taken.
5+
As an example, a player may perform an action to upgrade a building on a planet,
6+
which has immediate effects, but the building upgrade will only complete at a later time.
7+
"""
8+
9+
@callback process(%Galaxies.Planets.PlanetEvent{}, planet_id :: integer()) ::
10+
{:ok, nil} | {:error, map()}
11+
end

0 commit comments

Comments
 (0)