This commit is contained in:
@@ -110,7 +110,14 @@ class LocalExtensionInventoryTests(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
combined,
|
||||
{
|
||||
"single-player-games": ("wallet", "prize_draw", "lottery", "blackjack", "game_rules"),
|
||||
"single-player-games": (
|
||||
"wallet",
|
||||
"prize_draw",
|
||||
"lottery",
|
||||
"blackjack",
|
||||
"game_history",
|
||||
"game_rules",
|
||||
),
|
||||
"person-directory": ("person_directory", "person_info"),
|
||||
"person-memory": (
|
||||
"remember",
|
||||
|
||||
@@ -125,10 +125,6 @@ class Release061Tests(unittest.TestCase):
|
||||
self.command("/draw 100")
|
||||
self.assertIn("金币不足", self.command("/draw 2"))
|
||||
self.assertEqual(self.state()["balance"], 0)
|
||||
with mock.patch.object(self.local.players.rng, "randrange", return_value=123):
|
||||
reply = self.command("/买彩票 123 10", sender="Bob")
|
||||
self.assertIn("返还 5000", reply)
|
||||
self.assertEqual(self.state("Bob")["balance"], 5990)
|
||||
|
||||
def test_blackjack_is_private_persistent_and_settles_exactly_once(self):
|
||||
# Player 10+7, dealer 9+8: standing produces a tie.
|
||||
|
||||
@@ -56,7 +56,7 @@ class Release062BlackjackTests(unittest.TestCase):
|
||||
side_effect=self.hit_deck,
|
||||
):
|
||||
start = self.command("/21点开始100", event_id="compact-start")
|
||||
self.assertTrue(start.startswith("alice:"))
|
||||
self.assertTrue(start.startswith("alice #1:"))
|
||||
self.assertIn("下注 100", start)
|
||||
self.assertEqual(self.state()["balance"], 900)
|
||||
self.assertEqual(len(self.state()["game"]["player"]), 2)
|
||||
@@ -69,7 +69,7 @@ class Release062BlackjackTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(replay, first)
|
||||
self.assertTrue(first.startswith("alice:"))
|
||||
self.assertTrue(first.startswith("alice #1:"))
|
||||
self.assertIn("13点", first)
|
||||
self.assertEqual(self.state()["balance"], 900)
|
||||
self.assertEqual(len(self.state()["game"]["player"]), 3)
|
||||
@@ -92,7 +92,7 @@ class Release062BlackjackTests(unittest.TestCase):
|
||||
started = self.command(command, sender=sender)
|
||||
self.assertTrue(
|
||||
started.startswith(
|
||||
"alice:" if sender == "Alice" else "bob:"
|
||||
"alice #1:" if sender == "Alice" else "bob #1:"
|
||||
)
|
||||
)
|
||||
self.assertIn("下注 50", started)
|
||||
@@ -109,7 +109,7 @@ class Release062BlackjackTests(unittest.TestCase):
|
||||
):
|
||||
reply = self.command("/21点开始")
|
||||
|
||||
self.assertTrue(reply.startswith("alice:"))
|
||||
self.assertTrue(reply.startswith("alice #1:"))
|
||||
self.assertIn("下注 50", reply)
|
||||
self.assertEqual(self.state()["balance"], 950)
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user