Boti su sedadla bez socketu: ucty bot:<kind>-<n> v tabulke players (nehijacknutelne, recyklovane medzi hrami), handlery add_bot/remove_bot (len hostitel, pred startom) a tahova slucka _run_bot_turns s pauzou BOT_MOVE_DELAY_SECONDS a MC/inferenciou v executori. Druhy: heuristic, random, neural (pure-Python siet; bez suboru vah jasna chyba a pri restore fallback na heuristiku). Botie sedadla preziju restart servera. Oprava po ceste: emit kariet hracovi so sid=None (offline sedadlo po restore) broadcastoval jeho karty vsetkym klientom -- preskakuje sa. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
217 lines
8.4 KiB
Python
217 lines
8.4 KiB
Python
"""Testy in-process botov (api/bots.py + tahova slucka v api/__init__.py).
|
|
|
|
Rovnaky setup ako tests/test_history.py: docasny SQLite subor, env pred
|
|
importom. Socket.IO emity idu do prazdnych roomov (ziadny klient), takze
|
|
handlery a slucka sa daju volat priamo bez klienta.
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
import uuid
|
|
|
|
# Nastav DB/ENCRYPTION_KEY PRED importom db/api modulov.
|
|
_DB_FILE = os.path.join(tempfile.gettempdir(), f"bridzik_test_{uuid.uuid4().hex}.db")
|
|
os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///" + _DB_FILE.replace("\\", "/")
|
|
|
|
from cryptography.fernet import Fernet # noqa: E402
|
|
|
|
os.environ.setdefault("ENCRYPTION_KEY", Fernet.generate_key().decode())
|
|
|
|
from random import Random # noqa: E402
|
|
|
|
import api # noqa: E402
|
|
from api import auth, bots, history # noqa: E402
|
|
from db.db import init_db # noqa: E402
|
|
from rl.players import HeuristicPlayer, RandomPlayer # noqa: E402
|
|
|
|
|
|
def run(coro):
|
|
return asyncio.run(coro)
|
|
|
|
|
|
def _make_bot_accounts(n):
|
|
exclude = set()
|
|
accounts = []
|
|
for _ in range(n):
|
|
acc = run(bots.ensure_bot_account("random", exclude))
|
|
exclude.add(acc["player_id"])
|
|
accounts.append(acc)
|
|
return accounts
|
|
|
|
|
|
def _make_game(seat_accounts, brains):
|
|
"""Postavi zacatu in-memory hru + Game riadok v DB."""
|
|
gid = str(uuid.uuid4())
|
|
game = api.Game(gid, "test")
|
|
for seat, acc in enumerate(seat_accounts):
|
|
player = api.Player(None, acc["username"], seat, acc["player_id"])
|
|
if brains[seat] is not None:
|
|
player.is_bot = True
|
|
player.brain = brains[seat]
|
|
player.connected = True
|
|
else:
|
|
player.connected = False
|
|
game.players.append(player)
|
|
game.start()
|
|
api.games[gid] = game
|
|
run(history.record_game_started(
|
|
gid, "test", [acc["player_id"] for acc in seat_accounts]
|
|
))
|
|
return game
|
|
|
|
|
|
class BotAccountCase(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
run(init_db())
|
|
api.BOT_MOVE_DELAY_SECONDS = 0
|
|
|
|
def test_username_conventions(self):
|
|
self.assertTrue(bots.is_bot_username("bot:heuristic-1"))
|
|
self.assertFalse(bots.is_bot_username("alice"))
|
|
self.assertEqual(bots.kind_of("bot:heuristic-2"), "heuristic")
|
|
self.assertEqual(bots.kind_of("bot:random-1"), "random")
|
|
self.assertEqual(bots.kind_of("bot:neural-1"), "neural")
|
|
self.assertEqual(bots.kind_of("bot:nezmysel-9"), bots.DEFAULT_KIND)
|
|
self.assertIsInstance(bots.make_brain("bot:heuristic-1"), HeuristicPlayer)
|
|
self.assertIsInstance(bots.make_brain("bot:random-3"), RandomPlayer)
|
|
|
|
@unittest.skipUnless(bots.neural_available(),
|
|
'chyba export vah (py -m rl.export)')
|
|
def test_neural_kind(self):
|
|
from rl.pure_net import PureNeuralPlayer
|
|
self.assertIn("neural", bots.available_kinds())
|
|
brain = bots.make_brain("bot:neural-1")
|
|
self.assertIsInstance(brain, PureNeuralPlayer)
|
|
# zdielana instancia PureNet (vahy sa nacitavaju len raz)
|
|
self.assertIs(brain.net, bots.make_brain("bot:neural-2").net)
|
|
|
|
def test_ensure_bot_account_reuse_and_exclude(self):
|
|
first = run(bots.ensure_bot_account("heuristic", set()))
|
|
self.assertTrue(first["username"].startswith("bot:heuristic-"))
|
|
# bez vylucenia sa ucet recykluje
|
|
again = run(bots.ensure_bot_account("heuristic", set()))
|
|
self.assertEqual(first["player_id"], again["player_id"])
|
|
# s vylucenim vznikne dalsi ucet s inym ID
|
|
second = run(bots.ensure_bot_account("heuristic", {first["player_id"]}))
|
|
self.assertNotEqual(first["player_id"], second["player_id"])
|
|
self.assertNotEqual(first["username"], second["username"])
|
|
|
|
def test_bot_account_cannot_be_hijacked(self):
|
|
acc = run(bots.ensure_bot_account("heuristic", set()))
|
|
# registracia mena zlyha -- ucet sa netvari ako nedokoncena registracia
|
|
with self.assertRaises(auth.AuthError):
|
|
run(auth.register_account(acc["username"]))
|
|
# login zlyha na kode (secret nikto nepozna), NIE na RegistrationIncomplete
|
|
with self.assertRaises(auth.AuthError) as ctx:
|
|
run(auth.login(acc["username"], "000000"))
|
|
self.assertNotIsInstance(ctx.exception, auth.RegistrationIncomplete)
|
|
|
|
|
|
class BotTurnLoopCase(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
run(init_db())
|
|
api.BOT_MOVE_DELAY_SECONDS = 0
|
|
|
|
def setUp(self):
|
|
api.games.clear()
|
|
api.sessions.clear()
|
|
api.accounts.clear()
|
|
|
|
def test_four_bots_play_whole_game(self):
|
|
accounts = _make_bot_accounts(4)
|
|
brains = [RandomPlayer(Random(seat)) for seat in range(4)]
|
|
game = _make_game(accounts, brains)
|
|
|
|
run(api._run_bot_turns(game.gid))
|
|
|
|
self.assertTrue(game.bridzik_core.is_completed())
|
|
# cela hra je zapisana: 4 serie x 8 kol, ended_at nastaveny
|
|
detail = run(history.get_game_detail(game.gid))
|
|
self.assertIsNotNone(detail["ended_at"])
|
|
self.assertEqual(len(detail["rounds"]), history.FULL_GAME_ROUNDS * 4)
|
|
|
|
def test_bots_stop_at_human_turn(self):
|
|
accounts = _make_bot_accounts(3)
|
|
# sedadlo 0 = clovek; identitu v DB mu robi dalsi (nepouzity) boti
|
|
# ucet -- pre historiu je to len player_id, wrapper bez mozgu = clovek
|
|
human_acc = run(bots.ensure_bot_account(
|
|
"random", {a["player_id"] for a in accounts}
|
|
))
|
|
game = _make_game(
|
|
[human_acc] + accounts,
|
|
[None, HeuristicPlayer(Random(1), n_samples=20),
|
|
RandomPlayer(Random(2)), RandomPlayer(Random(3))],
|
|
)
|
|
core = game.bridzik_core
|
|
rnd = core.series[-1].get_last_round()
|
|
|
|
async def scenario():
|
|
# na tahu je clovek (first_player serie 0 je sedadlo 0) -> boti nic
|
|
await api._run_bot_turns(game.gid)
|
|
self.assertEqual(len(rnd.guesses), 0)
|
|
# clovek tipne -> boti dotipuju a hraju az po dalsi tah cloveka
|
|
core.add_player_guess(0, 1)
|
|
await api._run_bot_turns(game.gid)
|
|
|
|
run(scenario())
|
|
self.assertTrue(rnd.is_guessing_completed())
|
|
self.assertEqual(rnd.get_active_player(), 0)
|
|
|
|
def test_add_and_remove_bot_handlers(self):
|
|
async def scenario():
|
|
gid = str(uuid.uuid4())
|
|
api.games[gid] = api.Game(gid, "lobby-test")
|
|
host = api.Player("sid-host", "hostiteľ", 0, 999_100)
|
|
api.games[gid].players.append(host)
|
|
api.sessions["sid-host"] = {"gid": gid, "order": 0}
|
|
api.sessions["sid-guest"] = {"gid": gid, "order": 1}
|
|
|
|
# nehostitel nesmie pridat bota
|
|
await api.add_bot("sid-guest", gid)
|
|
self.assertEqual(len(api.games[gid].players), 1)
|
|
|
|
# hostitel prida dvoch botov -> rozne ucty, najnizsie volne sedadla
|
|
await api.add_bot("sid-host", gid)
|
|
await api.add_bot("sid-host", gid, "random")
|
|
players = api.games[gid].players
|
|
self.assertEqual(len(players), 3)
|
|
bots_added = [p for p in players if p.is_bot]
|
|
self.assertEqual(len(bots_added), 2)
|
|
self.assertEqual({p.order for p in bots_added}, {1, 2})
|
|
self.assertNotEqual(bots_added[0].player_id, bots_added[1].player_id)
|
|
self.assertIsNotNone(bots_added[0].brain)
|
|
|
|
# remove_bot: odmietne cloveka, odoberie bota
|
|
await api.remove_bot("sid-host", gid, 0)
|
|
self.assertEqual(len(api.games[gid].players), 3)
|
|
await api.remove_bot("sid-host", gid, 1)
|
|
self.assertEqual(len(api.games[gid].players), 2)
|
|
self.assertIsNone(api.games[gid].player_by_order(1))
|
|
|
|
run(scenario())
|
|
|
|
def test_restore_marks_bots(self):
|
|
from bridzik import Bridzik
|
|
info = {
|
|
"gid": str(uuid.uuid4()),
|
|
"name": "obnova",
|
|
"seats": [(1, "alice"), (2, "bot:heuristic-1"),
|
|
(3, "bot:random-1"), (4, "bob")],
|
|
"core": Bridzik(),
|
|
}
|
|
game = api._load_game_into_memory(info)
|
|
self.assertFalse(game.players[0].is_bot)
|
|
self.assertFalse(game.players[0].connected)
|
|
self.assertTrue(game.players[1].is_bot)
|
|
self.assertTrue(game.players[1].connected)
|
|
self.assertIsInstance(game.players[1].brain, HeuristicPlayer)
|
|
self.assertIsInstance(game.players[2].brain, RandomPlayer)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main(verbosity=2)
|