"""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()