Files
JWeBot/tests/test_release_063.py
T
2026-10-15 13:57:32 +08:00

253 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Behavioral coverage for multiplayer lottery and indexed game records."""
import json
import tempfile
import threading
import unittest
from pathlib import Path
from unittest import mock
from test_jwebot_edge import MODULE, config
class Release063GameTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.engine = MODULE.EdgeApplication(config(Path(self.temp.name)))
self.counter = 0
def tearDown(self):
self.engine.ledger.connection.close()
self.temp.cleanup()
def command(
self,
question,
*,
sender="Alice",
group="group-a",
event_id=None,
):
self.counter += 1
row = {
"question": question,
"sender": sender,
"group_fingerprint": group,
"event_id": event_id or f"test-{self.counter}",
}
return self.engine.local.handle(row, {})
def state(self, sender="Alice", group="group-a"):
key = self.engine.ledger.resolve_person(group, sender)["person_key"]
row = self.engine.ledger.connection.execute(
"""
SELECT state_json FROM player_accounts
WHERE group_fingerprint = ? AND person_key = ?
""",
(group, key),
).fetchone()
return json.loads(row[0])
def test_lottery_number_forms_sort_and_validate(self):
parse = self.engine.local.players.parse_lottery_entry
self.assertEqual(parse("347"), ((3, 4, 7), 10))
self.assertEqual(parse("743"), ((3, 4, 7), 10))
self.assertEqual(parse("报名 3 4 7 100"), ((3, 4, 7), 100))
self.assertEqual(parse("报名347100"), ((3, 4, 7), 100))
self.assertEqual(parse("743金额500"), ((3, 4, 7), 500))
with self.assertRaisesRegex(ValueError, "互不重复"):
parse("337")
with self.assertRaisesRegex(ValueError, "1~500"):
parse("347501")
def test_multiplayer_lottery_status_draw_and_payouts(self):
self.assertIn("第 1 把已开始", self.command("/彩票开始"))
self.assertIn("alice #1", self.command("/彩票347100"))
self.assertIn(
"bob #1",
self.command("/彩票 报名 9 3 7 200", sender="Bob"),
)
self.assertIn(
"carol #1",
self.command("/彩票 1 2 3 100", sender="Carol"),
)
self.assertIn(
"Dave #1",
self.command("/彩票045100", sender="Dave"),
)
status = self.command("/彩票当前状态", sender="Bob")
self.assertIn("已报名 4 人", status)
self.assertIn("alice #1:3 4 7,下注 100", status)
self.assertIn("bob #1:3 7 9,下注 200", status)
self.assertIn("只有本轮主持人 alice", self.command(
"/彩票开奖", sender="Bob"))
with mock.patch.object(
self.engine.local.players.rng,
"sample",
return_value=[7, 3, 9],
):
result = self.command("/彩票开奖", event_id="draw-round-1")
self.assertIn("开奖号:3 7 9", result)
self.assertIn("alice #1:命中 2 个", result)
self.assertIn("返还 120", result)
self.assertIn("bob #1:命中 3 个", result)
self.assertIn("返还 600", result)
self.assertIn("carol #1:命中 1 个", result)
self.assertIn("返还 50", result)
self.assertIn("Dave #1:命中 0 个", result)
self.assertEqual(self.state()["balance"], 1020)
self.assertEqual(self.state("Bob")["balance"], 1400)
self.assertEqual(self.state("Carol")["balance"], 950)
self.assertEqual(self.state("Dave")["balance"], 900)
self.assertIn("命中2个", self.command("/游戏记录"))
self.assertIn("命中3个", self.command(
"/个人记录", sender="Bob"))
self.assertIn("没有进行中", self.command("/彩票当前状态"))
def test_registration_update_keeps_index_and_cancel_refunds(self):
self.command("/彩票 开始")
first = self.command("/彩票 347 100")
updated = self.command("/彩票 743 200", sender="a1")
self.assertIn("alice #1", first)
self.assertIn("alice #1", updated)
self.assertIn("报名已更新", updated)
self.assertEqual(self.state()["balance"], 800)
status = self.command("/彩票 状态")
self.assertIn("已报名 1 人", status)
self.assertIn("3 4 7,下注 200", status)
self.assertIn("只有本轮主持人 alice", self.command(
"/彩票 取消", sender="Bob"))
self.assertIn("已全额退回", self.command("/彩票取消"))
self.assertEqual(self.state()["balance"], 1000)
self.assertIn("净变化 +0", self.command("/游戏记录"))
self.command("/彩票开始")
second = self.command("/彩票347")
self.assertIn("alice #2", second)
def test_concurrent_registration_replay_debits_once(self):
self.command("/彩票开始")
replies = []
errors = []
def register():
try:
replies.append(
self.command("/彩票347500", event_id="same-entry")
)
except BaseException as error:
errors.append(error)
threads = [threading.Thread(target=register) for _ in range(8)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
self.assertFalse(errors)
self.assertEqual(len(set(replies)), 1)
self.assertEqual(self.state()["balance"], 500)
self.assertIn("已报名 1 人", self.command("/彩票状态"))
def test_blackjack_uses_1_to_500_and_indexed_history(self):
def tied_deck(cards):
first = [9, 6, 8, 7]
cards[:] = first + [card for card in cards if card not in first]
with mock.patch.object(
self.engine.local.players.rng,
"shuffle",
side_effect=tied_deck,
):
started = self.command("/21点开始500")
self.assertTrue(started.startswith("alice #1:"))
self.assertEqual(self.state()["balance"], 500)
settled = self.command("/21点停牌")
self.assertTrue(settled.startswith("alice #1:"))
self.assertIn("平局", settled)
self.assertEqual(self.state()["balance"], 1000)
history = self.command("/游戏记录")
self.assertIn("alice #1:21点 平局", history)
self.assertIn("下注 500,返还 500", history)
self.assertIn("1~500", self.command("/21点开始501"))
def test_prize_draw_has_indexed_history(self):
with mock.patch.object(
self.engine.local.players.rng,
"randrange",
return_value=99,
):
first = self.command("/抽奖10")
second = self.command("/抽奖10")
self.assertTrue(first.startswith("alice #1:"))
self.assertTrue(second.startswith("alice #2:"))
history = self.command("/gamehistory")
self.assertIn("alice #2:抽奖 未中奖", history)
self.assertIn("alice #1:抽奖 未中奖", history)
def test_open_round_and_history_survive_restart(self):
root = Path(self.temp.name)
self.command("/彩票开始")
self.command("/彩票347100")
self.engine.ledger.connection.close()
self.engine = MODULE.EdgeApplication(config(root))
self.assertIn("alice #1:3 4 7", self.command("/彩票状态"))
with mock.patch.object(
self.engine.local.players.rng,
"sample",
return_value=[3, 4, 7],
):
self.command("/彩票开奖")
self.engine.ledger.connection.close()
self.engine = MODULE.EdgeApplication(config(root))
history = self.command("/游戏记录")
self.assertIn("alice #1:彩票 命中3个", history)
self.assertEqual(self.state()["balance"], 1200)
def test_registration_rolls_back_when_response_building_fails(self):
ledger = self.engine.ledger
ledger.start_group_lottery(
"group-a",
"Alice",
"start",
"lottery:start",
lambda _: "started",
)
def fail(_):
raise RuntimeError("render failed")
with self.assertRaisesRegex(RuntimeError, "render failed"):
ledger.register_group_lottery(
"group-a",
"Alice",
"register",
"lottery:347100",
(3, 4, 7),
100,
fail,
)
account = ledger.connection.execute(
"SELECT COUNT(*) FROM player_accounts"
).fetchone()[0]
entries = ledger.connection.execute(
"SELECT COUNT(*) FROM group_lottery_entries"
).fetchone()[0]
actions = ledger.connection.execute(
"SELECT COUNT(*) FROM player_actions WHERE event_id = 'register'"
).fetchone()[0]
self.assertEqual((account, entries, actions), (0, 0, 0))
if __name__ == "__main__":
unittest.main()