feat(engine): 四座自动化设施——喷灌 / 收割机 / 售货机 / 磨坊

为什么:自动化建筑只有水井和喂食器,中期以后每天的操作只剩浇水喂食
的重复劳动。新设施把"替代行动点动作"的思路推到完整流水线:
浇水(井/喷灌)→ 收割机收熟地 → 磨坊把陈粮变饲料 → 喂食器喂牲口
→ 售货机趁高价清库存,人不在也能持续产出。

- config.BUILDINGS 注册 4 座:喷灌系统 2800 金(全量浇水)、
  自动收割机 900 金 ×3(每天各收 2 块熟地,刻意不给经验——机器收的
  不算劳动,给手动玩法留价值)、自动售货机 1200 金(市价 > 基准价才
  出手、每天上限 5 份、越贵越先卖)、饲料磨坊 500 金(1 份最便宜的
  作物磨 3 份饲料)。
- engine._run_buildings 扩成六段流水线,顺序即依赖:磨坊排在喂食器前,
  早上磨的饲料当天就能喂上;收割机遇满仓停摆、作物留地(同喂食器
  缺料停摆哲学——自动系统绝不硬来,更绝不自动花钱)。
- webui / build 动作 / catalog 全部由 config.BUILDINGS 驱动,零改动。
- 测试 +6(喷灌全量浇水、收割入库且无经验、满仓停摆留作物、磨坊挑
  最便宜、磨坊→喂食器同日链路、售货机阈值与配额)。修复断言时确认
  两个既有行为:引擎自带 START_FEED=5;advance_day 末尾会把动物 fed
  复位("喂到了"的证据是 pending 累计而非 fed 本身)。
- 门禁:534 项 OK(skip 39)、ruff/pyright 全绿。
This commit is contained in:
snowy-yang committed 2026-09-25 21:05:27 +08:00
1 parent 2676927c1a
commit cb4c1e7800
7 files changed
+332 -10

No files matched your search

+97
View File
@@ -760,6 +760,103 @@ class GrowthTestCase(unittest.TestCase):
self.assertEqual(plot.growth, plot.grow_days)
class AutomationTestCase(unittest.TestCase):
"""四座自动化设施:喷灌 / 收割机 / 磨坊 / 售货机。
直接改 ``state.buildings`` 绕过等级门槛——解锁检查是 build() 的通用
逻辑(有独立用例),这里只验"建成后每天自动干了什么"。
"""
def setUp(self):
self.engine = make_engine()
def test_sprinkler_waters_every_dry_plot(self):
"""水井只能救一块地,喷灌系统要全救。"""
e = self.engine
for no in (1, 2, 3):
e.plant(no, "wheat")
e.state.plots[0].moisture = 0
e.state.plots[1].moisture = 0
e.state.buildings["sprinkler"] = 1
events = e.advance_day(weather="sunny")
self.assertEqual(e.state.plots[0].moisture, config.WATER_CAPACITY - 1)
self.assertEqual(e.state.plots[1].moisture, config.WATER_CAPACITY - 1)
self.assertEqual(sum(text.count("喷灌系统") for text in events), 2)
def test_harvester_auto_harvests_ripe_plots_without_exp(self):
e = self.engine
e.plant(1, "wheat") # 2 天成熟
e.state.buildings["harvester"] = 1
e.advance_day(weather="sunny")
e.advance_day(weather="sunny")
exp_before = e.state.exp
events = e.advance_day(weather="sunny")
self.assertTrue(any("自动收割机" in text for text in events))
self.assertIsNone(e.state.plots[0].crop)
self.assertGreaterEqual(e.state.inventory.get("wheat", 0), 1)
self.assertEqual(e.state.exp, exp_before, "机器收的不该给经验")
def test_harvester_stalls_when_granary_full(self):
e = self.engine
e.plant(1, "wheat")
e.state.buildings["harvester"] = 1
e.advance_day(weather="sunny")
e.advance_day(weather="sunny")
e.state.inventory["carrot"] = config.GRANARY_BASE # 塞满谷仓
events = e.advance_day(weather="sunny")
self.assertTrue(any("停摆" in text for text in events))
self.assertEqual(e.state.plots[0].crop, "wheat", "满仓时作物留在地里")
def test_mill_grinds_the_cheapest_crop(self):
e = self.engine
e.state.inventory = {"wheat": 2, "pumpkin": 1}
e.state.feed = 0 # 引擎自带 START_FEED,清零才好数磨坊的产出
e.state.buildings["mill"] = 1
events = e.advance_day(weather="sunny")
self.assertEqual(e.state.inventory["wheat"], 1)
self.assertEqual(e.state.inventory["pumpkin"], 1, "南瓜更贵,不该被磨")
self.assertEqual(e.state.feed, config.MILL_OUTPUT)
self.assertTrue(any("饲料磨坊" in text for text in events))
def test_mill_feeds_the_feeder_in_the_same_morning(self):
"""流水线顺序:磨坊产饲料 → 喂食器当天就能吃上。
注意 fed 会在当天的牲畜结算末尾被复位(今天的粮今天消化),
所以"喂到了"的证据是周期推进 / 产出累计,不是 fed 本身。
"""
e = self.engine
e.buy_animal("chicken")
e.state.buildings = {"mill": 1, "feeder": 1}
e.state.inventory = {"wheat": 1}
e.state.feed = 0
e.advance_day(weather="sunny")
self.assertEqual(e.state.animals[0].pending, 1, "当天喂到就该产出 1 份待收")
def test_vendor_sells_above_baseline_respecting_quota(self):
e = self.engine
e.state.inventory = {"wheat": 8, "carrot": 3}
e.state.market["wheat"] = 20.0 # 基准 14,高于基准
e.state.market["carrot"] = 10.0 # 基准 26,低于基准
e.state.buildings["vendor"] = 1
gold_before = e.state.gold
events = e.advance_day(weather="sunny")
self.assertEqual(dict(e.state.inventory), {"wheat": 3, "carrot": 3})
self.assertEqual(e.state.gold - gold_before, 100) # 5 份 × 20
self.assertTrue(any("自动售货机" in text for text in events))
class AdvanceDayTestCase(unittest.TestCase):
def setUp(self):
self.engine = make_engine()