From 4a484bfde619da8b2d67dc56f3ec17f34d291d34 Mon Sep 17 00:00:00 2001 From: Art_Sakura <1754798088@qq.com> Date: Wed, 21 Oct 2026 14:07:18 +0800 Subject: [PATCH] =?UTF-8?q?add=20#=20=E6=96=B0=E5=A2=9E=E5=BC=80=E5=9E=A6?= =?UTF-8?q?=E3=80=81=E5=8D=87=E7=BA=A7=E5=9C=9F=E5=9C=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/land-rules.md | 47 +++ integrations/zhenxun/commands.py | 2 +- integrations/zhenxun/runtime.py | 25 +- src/farm_core/application.py | 5 +- src/farm_core/data/land_rules.json | 480 +++++++++++++++++++++++++++++ src/farm_core/presentation.py | 14 + src/farm_core/repository.py | 2 +- src/farm_render/data/metadata.json | 40 +-- src/farm_render/models.py | 5 +- src/farm_render/renderer.py | 39 +++ tests/test_render.py | 34 +- tests/test_transactions.py | 11 +- tests/test_zhenxun_runtime.py | 38 +++ 13 files changed, 709 insertions(+), 33 deletions(-) create mode 100644 docs/land-rules.md create mode 100644 src/farm_core/data/land_rules.json diff --git a/docs/land-rules.md b/docs/land-rules.md new file mode 100644 index 0000000..27b13fa --- /dev/null +++ b/docs/land-rules.md @@ -0,0 +1,47 @@ +# 土地配置与地块顺序 + +`src/farm_core/data/land_rules.json` 按用户提供的 `91.png` 逐行录入。 +这是用户选定的参考规则,不标记为已验证的现行官方全量配置。 + +- 开垦保留图片序号 7~24 的付费条件;红、黑、金土地各保留 1~24。 +- 图片金币单位为万,配置中的 `coin_cost` 为乘 10000 后的整数。 +- 前 6 块由 `initial_plot_count: 6` 表示,不重复保存空条件记录。 + 新用户默认 6 块空地是用户另行明确指定的规则。已有账号不自动补地。 +- `ordinal` 是图片表内的条件序号,不等同于实际地块 `slot`。 + 后续升级服务必须明确这两者的对应方式,不能混用。 +- 表外序号,以及材料、星钻替代支付、冷却和额外解锁条件没有在图中 + 给出。不能默认免费、默认通过,或套用最后一行。24 不是土地总上限。 +- 本次提供规则数据;开垦和土地升级指令尚未接入这份配置。 + +## 新用户注册 + +注册事务创建 1~6 号空地,初始农场币仍为 500。重复注册不额外发地。 +不修改、补齐或重编号已有数据库中的地块记录。 + +## 绘图编号顺序 + +沿原有五条斜排的坐标方向,前两排交错推进,后续逐排推进: + +```text + 1 3 5 7 9 11 + 2 4 6 8 10 12 +13 14 15 16 17 18 +19 20 21 22 23 24 +25 26 27 28 29 30 +``` + +前 6 块地形成两排、每排 3 块;前 12 块完成后,13 号起按单排推进。 +`metadata.json` 的 `positions[slot - 1]` 是统一的绘图映射,土地、作物、 +虫子和详情中的地块标注都使用它。地形坐标不变,只重排前 12 项。 +已有账号的同一 `slot` 保留原作物和资产,展示位置随此映射调整。 + +## 用户查询入口 + +真寻接入新增「地块编号」,支持「地块编号 高清」。它复用正式渲染器, +按当前账号快照标注全部 30 个位置:浅色编号为已开垦,绿色为待开垦。 +图片附带编号顺序和「农场详述」入口说明,并保留当前季节、作物及头像。 +该查询无需消息写入标识,不更改作物、土地或资产。缺少字体时返回文字清单。 + +「农场帮助」包含该入口;首次注册成功回复显示本次实际初始地块数并提示 +发送「地块编号」。重复注册只提示查询入口,不声称再次获得土地。 +日常「我的农场」仍不显示编号,编号图与普通图使用不同的成图缓存键。 diff --git a/integrations/zhenxun/commands.py b/integrations/zhenxun/commands.py index d1f0599..1fa9277 100644 --- a/integrations/zhenxun/commands.py +++ b/integrations/zhenxun/commands.py @@ -77,7 +77,7 @@ def handler_for(action): message_id = getattr(event, "id", None) try: avatar = (await avatar_for(user_id, platform) - if action in ("farm", "details") else None) + if action in ("farm", "details", "plot_ids") else None) response = await runtime.execute( action, args.extract_plain_text(), bot_id=str(bot.self_id), platform=platform, user_id=user_id, message_id=message_id, diff --git a/integrations/zhenxun/runtime.py b/integrations/zhenxun/runtime.py index 4f994ae..3040861 100644 --- a/integrations/zhenxun/runtime.py +++ b/integrations/zhenxun/runtime.py @@ -1,6 +1,7 @@ """真寻接入编排;核心私有导入,测试数据与旧版隔离。""" import hashlib +from dataclasses import replace import json import logging from pathlib import Path @@ -9,7 +10,9 @@ import shlex from ._vendor.farm_core import Catalogue, FarmService, Identity, SQLiteStore from ._vendor.farm_core.domain import FarmError from ._vendor.farm_core.storage import SCHEMA_VERSION -from ._vendor.farm_core.presentation import receipt_text, snapshot_text +from ._vendor.farm_core.presentation import ( + plot_ids_text, receipt_text, snapshot_text, +) from ._vendor.farm_render import FarmRenderer, RenderOptions, RenderService from ._vendor.farm_render.seasons import season_for from ._vendor.farm_render.shop import shop_page @@ -23,10 +26,11 @@ COMMANDS = { "种子商店":"shop", "农场帮助":"help", "购买种子":"buy", "播种":"sow", "收获":"harvest", "铲除":"eradicate", "作物加锁":"lock", "作物解锁":"unlock", "出售作物":"sell", - "农场除虫":"clear_pests", "除虫":"clear_pests", + "农场除虫":"clear_pests", "除虫":"clear_pests", "地块编号":"plot_ids", } HELP = ( "开通农场 [名称]|我的农场 [高清]|农场详述 [页码]\n" + "地块编号 [高清]|查看当前土地编号及开垦位置\n" "种子商店 [页码]|购买种子 作物 数量|播种 作物 [数量]\n" "收获|铲除|农场除虫|我的种子|我的作物\n" "作物加锁 作物|作物解锁 作物|出售作物 [作物] [数量]\n" @@ -232,10 +236,12 @@ class Runtime: if args: raise ValueError() return snapshot_text(await self.service.snapshot(who)) - if action == "farm": + if action in ("farm", "plot_ids"): if args not in ([], ["高清"]): raise ValueError() if not self.rendering: + if action == "plot_ids": + return plot_ids_text(await self.service.snapshot(who)) return "请配置 FARM_PREVIEW_FONT 为可用中文字体路径。" now = self.service.clock() theme = season_for(now) @@ -245,6 +251,8 @@ class Runtime: if args else RenderOptions(format=self.image_format, theme=theme) ) + if action == "plot_ids": + options = replace(options, show_plot_ids=True) picture = await self.rendering.my_farm( self.service, who, options=options, avatar=avatar, supported=("JPEG",), @@ -280,7 +288,16 @@ class Runtime: receipt = await getattr(self.service, action)(who, op) else: return HELP - return receipt_text(receipt) + text = receipt_text(receipt) + if action == 'register': + if receipt.data['created']: + count = receipt.data.get('plot_count') + granted = (f'已获得{count}块土地' if count is not None + else '已获得初始土地') + text += f'\n{granted},发送「地块编号」查看位置。' + else: + text += '\n发送「地块编号」查看土地位置。' + return text except FarmError as error: return ERRORS.get(error.code, "操作未完成:" + error.code) except ValueError: diff --git a/src/farm_core/application.py b/src/farm_core/application.py index 556f6da..4fa5f8d 100644 --- a/src/farm_core/application.py +++ b/src/farm_core/application.py @@ -55,7 +55,10 @@ class FarmService: def operation(repository, now): created = repository.register(name) - return self._result(repository, created=created) + return self._result( + repository, created=created, + plot_count=len(repository.plots()), + ) return await self._execute( identity, operation_id, "register", {"name": name}, operation, diff --git a/src/farm_core/data/land_rules.json b/src/farm_core/data/land_rules.json new file mode 100644 index 0000000..03b035e --- /dev/null +++ b/src/farm_core/data/land_rules.json @@ -0,0 +1,480 @@ +{ + "initial_plot_count": 6, + "expansion": { + "rows": [ + { + "ordinal": 7, + "required_level": 5, + "coin_cost": 10000 + }, + { + "ordinal": 8, + "required_level": 7, + "coin_cost": 20000 + }, + { + "ordinal": 9, + "required_level": 9, + "coin_cost": 30000 + }, + { + "ordinal": 10, + "required_level": 11, + "coin_cost": 50000 + }, + { + "ordinal": 11, + "required_level": 13, + "coin_cost": 70000 + }, + { + "ordinal": 12, + "required_level": 15, + "coin_cost": 90000 + }, + { + "ordinal": 13, + "required_level": 17, + "coin_cost": 120000 + }, + { + "ordinal": 14, + "required_level": 19, + "coin_cost": 150000 + }, + { + "ordinal": 15, + "required_level": 21, + "coin_cost": 180000 + }, + { + "ordinal": 16, + "required_level": 23, + "coin_cost": 230000 + }, + { + "ordinal": 17, + "required_level": 25, + "coin_cost": 300000 + }, + { + "ordinal": 18, + "required_level": 27, + "coin_cost": 500000 + }, + { + "ordinal": 19, + "required_level": 29, + "coin_cost": 850000 + }, + { + "ordinal": 20, + "required_level": 31, + "coin_cost": 1100000 + }, + { + "ordinal": 21, + "required_level": 33, + "coin_cost": 1300000 + }, + { + "ordinal": 22, + "required_level": 35, + "coin_cost": 1500000 + }, + { + "ordinal": 23, + "required_level": 37, + "coin_cost": 1700000 + }, + { + "ordinal": 24, + "required_level": 39, + "coin_cost": 2000000 + } + ] + }, + "upgrades": [ + { + "land_type": "red", + "name": "红土地", + "from_land_type": "normal", + "rows": [ + { + "ordinal": 1, + "required_level": 28, + "coin_cost": 200000 + }, + { + "ordinal": 2, + "required_level": 29, + "coin_cost": 220000 + }, + { + "ordinal": 3, + "required_level": 30, + "coin_cost": 240000 + }, + { + "ordinal": 4, + "required_level": 31, + "coin_cost": 260000 + }, + { + "ordinal": 5, + "required_level": 32, + "coin_cost": 290000 + }, + { + "ordinal": 6, + "required_level": 33, + "coin_cost": 320000 + }, + { + "ordinal": 7, + "required_level": 34, + "coin_cost": 350000 + }, + { + "ordinal": 8, + "required_level": 35, + "coin_cost": 380000 + }, + { + "ordinal": 9, + "required_level": 36, + "coin_cost": 410000 + }, + { + "ordinal": 10, + "required_level": 37, + "coin_cost": 440000 + }, + { + "ordinal": 11, + "required_level": 38, + "coin_cost": 480000 + }, + { + "ordinal": 12, + "required_level": 39, + "coin_cost": 520000 + }, + { + "ordinal": 13, + "required_level": 40, + "coin_cost": 560000 + }, + { + "ordinal": 14, + "required_level": 41, + "coin_cost": 600000 + }, + { + "ordinal": 15, + "required_level": 42, + "coin_cost": 650000 + }, + { + "ordinal": 16, + "required_level": 43, + "coin_cost": 700000 + }, + { + "ordinal": 17, + "required_level": 44, + "coin_cost": 770000 + }, + { + "ordinal": 18, + "required_level": 45, + "coin_cost": 900000 + }, + { + "ordinal": 19, + "required_level": 47, + "coin_cost": 1500000 + }, + { + "ordinal": 20, + "required_level": 49, + "coin_cost": 2000000 + }, + { + "ordinal": 21, + "required_level": 51, + "coin_cost": 3000000 + }, + { + "ordinal": 22, + "required_level": 53, + "coin_cost": 4000000 + }, + { + "ordinal": 23, + "required_level": 55, + "coin_cost": 5500000 + }, + { + "ordinal": 24, + "required_level": 57, + "coin_cost": 6800000 + } + ] + }, + { + "land_type": "black", + "name": "黑土地", + "from_land_type": "red", + "rows": [ + { + "ordinal": 1, + "required_level": 40, + "coin_cost": 500000 + }, + { + "ordinal": 2, + "required_level": 41, + "coin_cost": 600000 + }, + { + "ordinal": 3, + "required_level": 42, + "coin_cost": 700000 + }, + { + "ordinal": 4, + "required_level": 43, + "coin_cost": 800000 + }, + { + "ordinal": 5, + "required_level": 44, + "coin_cost": 900000 + }, + { + "ordinal": 6, + "required_level": 45, + "coin_cost": 1000000 + }, + { + "ordinal": 7, + "required_level": 46, + "coin_cost": 2000000 + }, + { + "ordinal": 8, + "required_level": 47, + "coin_cost": 2200000 + }, + { + "ordinal": 9, + "required_level": 48, + "coin_cost": 2400000 + }, + { + "ordinal": 10, + "required_level": 49, + "coin_cost": 2600000 + }, + { + "ordinal": 11, + "required_level": 50, + "coin_cost": 2800000 + }, + { + "ordinal": 12, + "required_level": 51, + "coin_cost": 3000000 + }, + { + "ordinal": 13, + "required_level": 52, + "coin_cost": 4000000 + }, + { + "ordinal": 14, + "required_level": 53, + "coin_cost": 4200000 + }, + { + "ordinal": 15, + "required_level": 54, + "coin_cost": 4400000 + }, + { + "ordinal": 16, + "required_level": 55, + "coin_cost": 4600000 + }, + { + "ordinal": 17, + "required_level": 56, + "coin_cost": 4800000 + }, + { + "ordinal": 18, + "required_level": 57, + "coin_cost": 5000000 + }, + { + "ordinal": 19, + "required_level": 59, + "coin_cost": 6000000 + }, + { + "ordinal": 20, + "required_level": 61, + "coin_cost": 6200000 + }, + { + "ordinal": 21, + "required_level": 63, + "coin_cost": 6400000 + }, + { + "ordinal": 22, + "required_level": 65, + "coin_cost": 6600000 + }, + { + "ordinal": 23, + "required_level": 67, + "coin_cost": 6800000 + }, + { + "ordinal": 24, + "required_level": 69, + "coin_cost": 7000000 + } + ] + }, + { + "land_type": "gold", + "name": "金土地", + "from_land_type": "black", + "rows": [ + { + "ordinal": 1, + "required_level": 58, + "coin_cost": 4000000 + }, + { + "ordinal": 2, + "required_level": 59, + "coin_cost": 4200000 + }, + { + "ordinal": 3, + "required_level": 60, + "coin_cost": 4400000 + }, + { + "ordinal": 4, + "required_level": 61, + "coin_cost": 4600000 + }, + { + "ordinal": 5, + "required_level": 62, + "coin_cost": 4800000 + }, + { + "ordinal": 6, + "required_level": 63, + "coin_cost": 5000000 + }, + { + "ordinal": 7, + "required_level": 64, + "coin_cost": 5200000 + }, + { + "ordinal": 8, + "required_level": 65, + "coin_cost": 5400000 + }, + { + "ordinal": 9, + "required_level": 66, + "coin_cost": 5600000 + }, + { + "ordinal": 10, + "required_level": 67, + "coin_cost": 5800000 + }, + { + "ordinal": 11, + "required_level": 68, + "coin_cost": 6000000 + }, + { + "ordinal": 12, + "required_level": 69, + "coin_cost": 6200000 + }, + { + "ordinal": 13, + "required_level": 70, + "coin_cost": 6600000 + }, + { + "ordinal": 14, + "required_level": 71, + "coin_cost": 7000000 + }, + { + "ordinal": 15, + "required_level": 72, + "coin_cost": 7400000 + }, + { + "ordinal": 16, + "required_level": 73, + "coin_cost": 7800000 + }, + { + "ordinal": 17, + "required_level": 74, + "coin_cost": 8200000 + }, + { + "ordinal": 18, + "required_level": 75, + "coin_cost": 8600000 + }, + { + "ordinal": 19, + "required_level": 77, + "coin_cost": 9500000 + }, + { + "ordinal": 20, + "required_level": 79, + "coin_cost": 10400000 + }, + { + "ordinal": 21, + "required_level": 81, + "coin_cost": 11300000 + }, + { + "ordinal": 22, + "required_level": 83, + "coin_cost": 12200000 + }, + { + "ordinal": 23, + "required_level": 85, + "coin_cost": 13100000 + }, + { + "ordinal": 24, + "required_level": 87, + "coin_cost": 14000000 + } + ] + } + ] +} diff --git a/src/farm_core/presentation.py b/src/farm_core/presentation.py index da2f222..2d0fd2a 100644 --- a/src/farm_core/presentation.py +++ b/src/farm_core/presentation.py @@ -33,6 +33,20 @@ def receipt_text(receipt: Receipt) -> str: return "\n".join(lines) +def plot_ids_text(snapshot: Snapshot) -> str: + """无图片能力时按实际快照返回地块编号。""" + owned = sorted(plot.slot for plot in snapshot.plots) + remaining = sorted(set(range(1, 31)) - set(owned)) + return '\n'.join(( + '地块编号', + f'已开垦({len(owned)}块):' + ('、'.join(map(str, owned)) or '无'), + f'待开垦({len(remaining)}块):' + + ('、'.join(map(str, remaining)) or '无'), + '前两排交错编号,之后逐排推进。', + '发送「农场详述」查看各地块的作物信息。', + )) + + def snapshot_text(snapshot: Snapshot) -> str: account = snapshot.account lines = [ diff --git a/src/farm_core/repository.py b/src/farm_core/repository.py index 3d990a0..6842192 100644 --- a/src/farm_core/repository.py +++ b/src/farm_core/repository.py @@ -42,7 +42,7 @@ class SQLiteRepository: ) self._db.executemany( "INSERT INTO plots(identity, slot) VALUES (?, ?)", - [(self._identity, slot) for slot in range(1, 4)], + [(self._identity, slot) for slot in range(1, 7)], ) self.asset(Asset.POINT, 500) return True diff --git a/src/farm_render/data/metadata.json b/src/farm_render/data/metadata.json index e6e6a4c..184509a 100644 --- a/src/farm_render/data/metadata.json +++ b/src/farm_render/data/metadata.json @@ -8282,46 +8282,46 @@ "x": 1352, "y": 552 }, - { - "x": 1462, - "y": 611 - }, - { - "x": 1572, - "y": 670 - }, - { - "x": 1682, - "y": 729 - }, - { - "x": 1792, - "y": 788 - }, - { - "x": 1902, - "y": 847 - }, { "x": 1237, "y": 606 }, + { + "x": 1462, + "y": 611 + }, { "x": 1350, "y": 665 }, + { + "x": 1572, + "y": 670 + }, { "x": 1457, "y": 724 }, + { + "x": 1682, + "y": 729 + }, { "x": 1567, "y": 783 }, + { + "x": 1792, + "y": 788 + }, { "x": 1677, "y": 842 }, + { + "x": 1902, + "y": 847 + }, { "x": 1787, "y": 901 diff --git a/src/farm_render/models.py b/src/farm_render/models.py index 563c4e2..744912d 100644 --- a/src/farm_render/models.py +++ b/src/farm_render/models.py @@ -14,8 +14,11 @@ class RenderOptions: quality: int = 75 # 有损编码质量,不代表分辨率。 method: int = 2 # WebP 编码力度,0最快,6最慢。 theme: str = "default" # 显式主题,不读取隐含季节或全局配置。 + show_plot_ids: bool = False # 编号图显示全部地块及开垦状态。 def __post_init__(self): + if type(self.show_plot_ids) is not bool: + raise ValueError("Invalid plot ID option") for value, low, high in ( (self.width, 256, 3072), (self.quality, 1, 95), (self.method, 0, 6), @@ -48,7 +51,7 @@ class RenderOptions: @dataclass(frozen=True) class PlotView: - slot: int # 旧布局的1到30地块编号。 + slot: int # 1到30地块编号,对应统一位置表的slot-1项。 state: str # empty/growing/ripe/withered。 sprite: str = "" # 包内受控素材名,不来自用户路径。 stage: int = 0 # 本季展示阶段。 diff --git a/src/farm_render/renderer.py b/src/farm_render/renderer.py index 966628e..f30d7a1 100644 --- a/src/farm_render/renderer.py +++ b/src/farm_render/renderer.py @@ -70,6 +70,8 @@ class FarmRenderer: self._pests(canvas, scene, options) self._decorations(canvas, options, "foreground") self._panel(canvas, scene, options, avatar) + if options.show_plot_ids: + self._plot_ids(canvas, scene, options) composed = perf_counter() content = canvas.encode(codec, options.quality, options.method) encoded = perf_counter() @@ -92,6 +94,43 @@ class FarmRenderer: def _scale(value, options): return round(value * options.width / 3072) + def _plot_ids(self, canvas, scene, options): + s = lambda n: self._scale(n, options) + owned = {plot.slot for plot in scene.plots} + soil_w, soil_h = self.assets.metadata['soil_size'] + size = max(1, s(40)) + for slot, pos in enumerate(self.assets.metadata['positions'], 1): + x = s(pos['x'] + soil_w/2) + y = s(pos['y'] + soil_h/2) + color = '#fff2ca' if slot in owned else '#d5e8bc' + canvas.rectangle( + (x-s(38), y-s(30), x+s(38), y+s(30)), + '#493b22', radius=s(30), + ) + canvas.rectangle( + (x-s(36), y-s(28), x+s(36), y+s(28)), + color, radius=s(28), + ) + label = str(slot) + width = canvas.measure_text(label, self.font_path, size) + box = font(self.font_path, size).getbbox(label) + canvas.text( + (round(x-width/2), round(y-(box[1]+box[3])/2)), + label, self.font_path, size, '#392917', + ) + canvas.rectangle( + (s(76), s(1676), s(2996), s(1836)), + '#fff2ca', radius=s(24), + ) + for y, text, text_size in ( + (1694, '地块编号 · 浅色:已开垦 | 绿色:待开垦', 46), + (1760, '前两排交错编号,之后逐排推进。作物信息:农场详述', 40), + ): + canvas.text( + (s(116), s(y)), text, self.font_path, + max(1, s(text_size)), '#392917', + ) + def _base(self, scene, options): slots = frozenset(p.slot for p in scene.plots) key = ("base", options.theme, options.width, slots) diff --git a/tests/test_render.py b/tests/test_render.py index 2b8c4b3..8e0c5e6 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -31,7 +31,7 @@ def snapshot(plots=None, **account): values = dict(name="演示农场", point=500, vip_point=0, experience=0) values.update(account) return Snapshot(Account(**values), (), tuple( - plots if plots is not None else [Plot(i) for i in range(1, 4)] + plots if plots is not None else [Plot(i) for i in range(1, 7)] )) @@ -42,6 +42,38 @@ class RenderTests(unittest.TestCase): self.who = Identity("test", "local", "qq", "one") self.plant = next(iter(Catalogue.bundled())) + def test_plot_ids_follow_positions_and_mark_owned_slots(self): + options = RenderOptions(width=1536, show_plot_ids=True) + scene = self.renderer.scene(self.who, snapshot(), 0) + canvas = Canvas.new((1536, 936), 'white') + with patch.object(canvas, 'text', wraps=canvas.text) as text: + self.renderer._plot_ids(canvas, scene, options) + self.assertEqual( + [call.args[1] for call in text.call_args_list[:30]], + [str(i) for i in range(1, 31)], + ) + for slot, color in ((1, '#fff2ca'), (6, '#fff2ca'), + (7, '#d5e8bc'), (30, '#d5e8bc')): + pos = self.renderer.assets.metadata['positions'][slot-1] + x, y = round((pos['x']+117)/2), round((pos['y']+78)/2) + from PIL import ImageColor + self.assertEqual(canvas.image.getpixel((x-15, y))[:3], + ImageColor.getrgb(color)) + + def test_plot_id_image_has_separate_cache_from_normal_farm(self): + plain = self.renderer.render(self.who, snapshot(), 0) + options = RenderOptions(show_plot_ids=True) + numbered = self.renderer.render(self.who, snapshot(), 0, options) + self.assertFalse(numbered.cache_hit) + self.assertNotEqual(plain.content, numbered.content) + self.assertTrue(self.renderer.render( + self.who, snapshot(), 0, options, + ).cache_hit) + self.assertEqual( + self.renderer.render(self.who, snapshot(), 0).content, + plain.content, + ) + def test_canvas_alpha_crop_text_and_formats(self): canvas = Canvas.new((100, 80), "white") overlay = Image.new("RGBA", (20, 20), (255, 0, 0, 128)) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index c3bc425..6ee30f7 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -61,13 +61,16 @@ class TransactionTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(snapshot.account.point, 500) self.assertEqual(snapshot.account.vip_point, 0) self.assertEqual(snapshot.account.experience, 0) - self.assertEqual(len(snapshot.plots), 3) + self.assertEqual([p.slot for p in snapshot.plots], list(range(1, 7))) self.assertTrue(all(p.plant is None for p in snapshot.plots)) repeated = await self.service.register( self.identity, "另一个名称", "register-again", ) self.assertFalse(repeated.data["created"]) self.assertEqual(repeated.data["account"]["point"], 500) + self.assertEqual( + (await self.service.snapshot(self.identity)).plots, snapshot.plots, + ) self.assertEqual(len(await self.store.ledger(self.identity)), 1) async def test_scopes_are_independent(self): @@ -158,13 +161,13 @@ class TransactionTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(result.data["account"]["vip_point"], 6) async def test_sow_all_and_explicit_overflow(self): - await self.service.buy(self.identity, "萝卜", 5, "buy") + await self.service.buy(self.identity, "萝卜", 8, "buy") before = await self.service.snapshot(self.identity) with self.assertRaisesRegex(FarmError, "insufficient_empty_plots"): - await self.service.sow(self.identity, "萝卜", 4, "too-many") + await self.service.sow(self.identity, "萝卜", 7, "too-many") self.assertEqual(await self.service.snapshot(self.identity), before) result = await self.service.sow(self.identity, "萝卜", None, "all") - self.assertEqual(result.data["quantity"], 3) + self.assertEqual(result.data["quantity"], 6) self.assertEqual(result.data["seeds"], 2) async def test_maturity_boundary_and_multi_crop_lifecycle(self): diff --git a/tests/test_zhenxun_runtime.py b/tests/test_zhenxun_runtime.py index fcd2906..5537558 100644 --- a/tests/test_zhenxun_runtime.py +++ b/tests/test_zhenxun_runtime.py @@ -45,6 +45,44 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase): **{**self.context, "message_id":message, **changes}, ) + async def test_plot_ids_command_registration_hint_and_read_only(self): + from io import BytesIO + from PIL import Image + from unittest.mock import AsyncMock + self.assertEqual(self.module.COMMANDS['地块编号'], 'plot_ids') + self.assertIn('地块编号', await self.call('help')) + first = await self.call('register') + self.assertIn('6块土地', first) + self.assertIn('地块编号', first) + repeated = await self.call('register', message='again') + self.assertNotIn('已获得6块土地', repeated) + before = await self.runtime.service.snapshot(self.runtime_identity()) + with patch.object(self.runtime.service, 'snapshot', + new=AsyncMock(return_value=before)) as query: + result = await self.call('plot_ids', message=None) + query.assert_awaited_once() + with Image.open(BytesIO(result)) as image: + self.assertEqual(image.width, 1024) + self.assertEqual(image.format, 'JPEG') + self.assertEqual( + await self.runtime.service.snapshot(self.runtime_identity()), + before, + ) + self.assertIn('参数有误', await self.call('plot_ids', '1')) + self.assertIn('开通农场', await self.call('plot_ids', user_id='other')) + + async def test_plot_ids_text_fallback_uses_actual_owned_slots(self): + await self.call('register') + rendering = self.runtime.rendering + self.runtime.rendering = None + try: + text = await self.call('plot_ids', message=None) + finally: + self.runtime.rendering = rendering + self.assertIn('已开垦(6块):1、2、3、4、5、6', text) + self.assertIn('待开垦(24块):7、8、9', text) + self.assertIn('农场详述', text) + async def test_original_commands_transactions_and_png(self): self.assertNotIn("测试农场", self.module.COMMANDS) self.assertIn("开通农场", self.module.COMMANDS)