Skip to content

Commit ea55c95

Browse files
committed
✨ Simulator and AIs for the steak market.
1 parent eab3790 commit ea55c95

File tree

9 files changed

+465
-2
lines changed

9 files changed

+465
-2
lines changed

py/simulator/__main__.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import logging
2+
import os
3+
import random
24
from time import monotonic_ns
5+
from typing import Optional
36

47
import structlog
58
import typer
@@ -86,21 +89,28 @@ def simulator(
8689
time: str = "7d",
8790
summary: bool = False,
8891
seconds: bool = False, # Run with 1 second ticks instead of 1 minute.
92+
seed: Optional[int] = None,
93+
tick_length: Optional[int] = None,
8994
):
9095
if not verbose:
9196
structlog.configure(
9297
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO)
9398
)
9499
log = structlog.get_logger(mod="main")
95-
tick_length = 1 if seconds else 60
100+
if tick_length is None:
101+
tick_length = 1 if seconds else 60
96102
ticks = parse_time(time, tick_length)
103+
if seed is None:
104+
seed = int.from_bytes(os.urandom(4), byteorder="big")
97105
log.info(
98106
"Starting simulation",
99107
ticks=ticks,
100108
tick_length=tick_length,
101109
ai=ai,
102110
player=player,
111+
seed=seed,
103112
)
113+
random.seed(seed)
104114
game = Game(ai_class=get_ai(ai))
105115
PLAYERS[player](game)
106116
game.player.silver += STARTER_SILVER

py/simulator/ai/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from .base import AI
22
from .fisher import FisherAI
33
from .silly import SillyAI
4+
from .steak import SimpleSteakAI, SleepySteakAI, ThresholdSteakAI
45

56
AIS: dict[str, type] = {
67
"silly": SillyAI,
78
"fisher": FisherAI,
9+
"simple_steak": SimpleSteakAI,
10+
"threshold_steak": ThresholdSteakAI,
11+
"sleepy_steak": SleepySteakAI,
812
}
913

1014

py/simulator/ai/base.py

+4
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ def explore(self) -> Optional[Location]:
3232
def process(self) -> None:
3333
"""Handle all other per-tick logic."""
3434
return None
35+
36+
def finish(self) -> None:
37+
"""Do any cleanup before the end."""
38+
return None

py/simulator/ai/fisher.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
class FisherAI(AI):
9-
"""An AI that makes net and does fishing."""
9+
"""An AI that makes nets and does fishing."""
1010

1111
def fish(self) -> Optional[Location]:
1212
return Location["Large Island"]

py/simulator/ai/steak.py

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import random
2+
3+
from .base import AI
4+
5+
6+
class SteakAI(AI):
7+
"""Base class for steak AI experiments."""
8+
9+
def _buy_steaks(self, price: int) -> bool:
10+
return False
11+
12+
def _sell_steaks(self, price: int) -> bool:
13+
return not self._buy_steaks(price)
14+
15+
def _buy_kabobs(self, price: int) -> bool:
16+
return False
17+
18+
def _sell_kabobs(self, price: int) -> bool:
19+
return not self._buy_kabobs(price)
20+
21+
def process(self) -> None:
22+
market = self.game.steak_market
23+
player = self.game.player
24+
25+
if self._buy_steaks(market.steak_price):
26+
market.buy_steaks(player)
27+
elif self._sell_steaks(market.steak_price):
28+
market.sell_steaks(player)
29+
30+
if self._buy_kabobs(market.kabob_price):
31+
market.buy_kabobs(player)
32+
elif self._sell_kabobs(market.kabob_price):
33+
market.sell_kabobs(player)
34+
35+
def finish(self) -> None:
36+
market = self.game.steak_market
37+
player = self.game.player
38+
market.sell_steaks(player)
39+
market.sell_kabobs(player)
40+
41+
42+
class SimpleSteakAI(SteakAI):
43+
"""The simplest behavior, sell over average, buy below."""
44+
45+
def _buy_steaks(self, price: int) -> bool:
46+
return price < 50_000
47+
48+
def _buy_kabobs(self, price: int) -> bool:
49+
return price < 10_000
50+
51+
52+
class ThresholdSteakAI(SteakAI):
53+
"""Buy and sell thresholds."""
54+
55+
# BUY_STEAK = 40_000
56+
# SELL_STEAK = 55_000
57+
# BUY_KABOB = 9_900
58+
# SELL_KABOB = 10_100
59+
60+
BUY_STEAK = 30_000
61+
SELL_STEAK = 60_000
62+
BUY_KABOB = 9_600
63+
SELL_KABOB = 10_250
64+
65+
def _buy_steaks(self, price: int) -> bool:
66+
return price <= self.BUY_STEAK
67+
68+
def _sell_steaks(self, price: int) -> bool:
69+
return price >= self.SELL_STEAK
70+
71+
def _buy_kabobs(self, price: int) -> bool:
72+
return price <= self.BUY_KABOB
73+
74+
def _sell_kabobs(self, price: int) -> bool:
75+
return price >= self.SELL_KABOB
76+
77+
78+
class SleepySteakAI(ThresholdSteakAI):
79+
""" "Like Threshold but it has to work and sleep."""
80+
81+
WORK_HOURS = range(9, 17)
82+
SLEEP_HOURS = range(0, 8)
83+
84+
def _is_playing(self) -> bool:
85+
hour = self.game.time.hour
86+
if hour in self.SLEEP_HOURS:
87+
return False
88+
if hour in self.WORK_HOURS:
89+
# 50% chance to be blocked until the next hour.
90+
work_blocked = getattr(self, "work_blocked", None)
91+
if hour == work_blocked:
92+
return False
93+
if random.random() < 0.5:
94+
self.work_blocked = hour
95+
return False
96+
else:
97+
self.work_blocked = None
98+
return True
99+
return True
100+
101+
def _buy_steaks(self, price: int) -> bool:
102+
if self._is_playing():
103+
return super()._buy_steaks(price)
104+
return False
105+
106+
def _sell_steaks(self, price: int) -> bool:
107+
if self._is_playing():
108+
return super()._sell_steaks(price)
109+
return False
110+
111+
def _buy_kabobs(self, price: int) -> bool:
112+
if self._is_playing():
113+
return super()._buy_kabobs(price)
114+
return False
115+
116+
def _sell_kabobs(self, price: int) -> bool:
117+
if self._is_playing():
118+
return super()._sell_kabobs(price)
119+
return False

py/simulator/game.py

+11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import datetime
34
from io import StringIO
45
from typing import Optional
56

@@ -10,6 +11,7 @@
1011
from .buildings import HayField, RaptorPen, Sawmill
1112
from .farm import Farm
1213
from .player import Player
14+
from .steak_market import SteakMarket
1315
from .utils import format_number
1416

1517

@@ -24,21 +26,26 @@ class Game:
2426
sawmill: Sawmill = SelfFactory(Sawmill)
2527
hay_field: HayField = SelfFactory(HayField)
2628
raptor_pen: RaptorPen = SelfFactory(RaptorPen)
29+
steak_market: SteakMarket = SelfFactory(SteakMarket)
2730
ai_class: Optional[type] = None
2831
ai: Optional[AI] = None
2932

33+
time: datetime.datetime = datetime.datetime.now()
34+
3035
log: structlog.stdlib.BoundLogger = structlog.stdlib.get_logger(mod="farm")
3136

3237
def __attrs_post_init__(self):
3338
if self.ai is None and self.ai_class is not None:
3439
self.ai = self.ai_class(self)
3540

3641
def tick(self, seconds: int) -> None:
42+
self.time += datetime.timedelta(seconds=seconds)
3743
self.player.tick(seconds)
3844
self.farm.tick(seconds)
3945
self.sawmill.tick(seconds)
4046
self.hay_field.tick(seconds)
4147
self.raptor_pen.tick(seconds)
48+
self.steak_market.tick(seconds)
4249

4350
def process_ai(self):
4451
if self.ai is None:
@@ -79,6 +86,9 @@ def run(self, iterations: int = 60, interval: int = 60) -> None:
7986
)
8087
self.process_ai()
8188
self.tick(interval)
89+
# Finalize the AI.
90+
if self.ai:
91+
self.ai.finish()
8292

8393
def summary(self) -> str:
8494
"""Render a string summary of the game state."""
@@ -92,4 +102,5 @@ def summary(self) -> str:
92102
out.write("Overflow:\n")
93103
for item, count in self.player.overflow_items.items():
94104
out.write(f"\t{item.name}: {count}\n")
105+
out.write(self.steak_market.summary())
95106
return out.getvalue()

0 commit comments

Comments
 (0)