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