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>
121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
"""In-process boti: DB ucty botov a ich rozhodovacie "mozgy" z rl/players.py.
|
|
|
|
Bot je normalny hrac na sedadle -- ma riadok v tabulke `players` (aby
|
|
historia, standings a restore fungovali bez zmeny), ale ziadny socket.
|
|
Tahovu slucku botov ma api/__init__.py (_run_bot_turns); tu je len to,
|
|
co potrebuje DB a rl vrstvu.
|
|
|
|
Bezpecnost botich uctov: totp_secret je nahodny a nikde sa neuklada v
|
|
citatelnej podobe, totp_last_step sa nastavi na aktualny krok -- ucet tym
|
|
padom NIE JE "nedokoncena registracia" (viz auth._is_unconfirmed), takze
|
|
register_account ho odmietne prepisat a login bez secretu neprejde.
|
|
"""
|
|
|
|
import os
|
|
from random import Random
|
|
|
|
import pyotp
|
|
from sqlalchemy import select
|
|
|
|
from api import auth
|
|
from db import crypto
|
|
from db.db import async_session
|
|
from db.models import Player
|
|
from rl.players import HeuristicPlayer, RandomPlayer
|
|
from rl.pure_net import DEFAULT_WEIGHTS_PATH, PureNet, PureNeuralPlayer
|
|
|
|
# Prefix je konvencia na rozpoznanie bota (aj po restarte servera, kedy sa
|
|
# sedadla obnovuju z DB len ako (player_id, username)).
|
|
BOT_PREFIX = "bot:"
|
|
DEFAULT_KIND = "heuristic"
|
|
|
|
# Natrenovana siet -- vahy (rl/weights/, export z rl/export.py) sa nacitaju
|
|
# raz a zdielaju medzi botmi (PureNet je bezstavovy, len cita).
|
|
_pure_net: PureNet | None = None
|
|
|
|
|
|
def _neural_brain() -> PureNeuralPlayer:
|
|
global _pure_net
|
|
if _pure_net is None:
|
|
_pure_net = PureNet.load()
|
|
return PureNeuralPlayer(_pure_net)
|
|
|
|
|
|
def neural_available() -> bool:
|
|
return os.path.exists(DEFAULT_WEIGHTS_PATH)
|
|
|
|
|
|
_BRAINS = {
|
|
"heuristic": lambda: HeuristicPlayer(Random()),
|
|
"random": lambda: RandomPlayer(Random()),
|
|
"neural": _neural_brain,
|
|
}
|
|
BOT_KINDS = tuple(_BRAINS)
|
|
|
|
|
|
def available_kinds() -> tuple:
|
|
"""Druhy botov ponuknutelne na tomto serveri (neural len s vahami)."""
|
|
return tuple(k for k in _BRAINS if k != "neural" or neural_available())
|
|
|
|
|
|
def is_bot_username(username: str) -> bool:
|
|
return bool(username) and username.startswith(BOT_PREFIX)
|
|
|
|
|
|
def kind_of(username: str) -> str:
|
|
"""'bot:heuristic-2' -> 'heuristic'; neznamy druh padne na DEFAULT_KIND."""
|
|
body = username[len(BOT_PREFIX):]
|
|
kind = body.rsplit("-", 1)[0]
|
|
return kind if kind in _BRAINS else DEFAULT_KIND
|
|
|
|
|
|
def make_brain(username: str):
|
|
"""Rozhodovaci objekt (guess/play rozhranie z rl/players.py) pre bota.
|
|
|
|
Neural bez suboru vah (napr. restore hry na serveri bez exportu) padne
|
|
na heuristiku -- sedadlo hra dalej, len inym mozgom.
|
|
"""
|
|
kind = kind_of(username)
|
|
if kind == "neural" and not neural_available():
|
|
kind = DEFAULT_KIND
|
|
return _BRAINS[kind]()
|
|
|
|
|
|
def _suffix_number(username: str) -> int:
|
|
try:
|
|
return int(username.rsplit("-", 1)[1])
|
|
except (IndexError, ValueError):
|
|
return 0
|
|
|
|
|
|
async def ensure_bot_account(kind: str, exclude_ids: set) -> dict:
|
|
"""Najde alebo zalozi boti ucet daneho druhu; vrati {player_id, username}.
|
|
|
|
`exclude_ids` su ucty uz obsadene v danej hre -- kazde sedadlo potrebuje
|
|
INY ucet (Game.playerN_id aj unikat v Guess predpokladaju 4 rozne ID).
|
|
Ucty sa cisluju bot:<kind>-1, -2, ... a recykluju sa medzi hrami.
|
|
"""
|
|
prefix = f"{BOT_PREFIX}{kind}-"
|
|
async with async_session() as session:
|
|
rows = (
|
|
await session.scalars(
|
|
select(Player).where(Player.username.like(prefix + "%"))
|
|
)
|
|
).all()
|
|
for player in sorted(rows, key=lambda p: _suffix_number(p.username)):
|
|
if player.id not in exclude_ids:
|
|
return {"player_id": player.id, "username": player.username}
|
|
|
|
number = 1 + max((_suffix_number(p.username) for p in rows), default=0)
|
|
username = f"{prefix}{number}"
|
|
player = Player(
|
|
username=username,
|
|
# nahodny secret, ktory sa zahodi -- nikto sa zan neprihlasi
|
|
totp_secret=crypto.encrypt(pyotp.random_base32()),
|
|
# nenulovy last_step = ucet sa netvari ako nedokoncena registracia
|
|
totp_last_step=auth._current_step(),
|
|
)
|
|
session.add(player)
|
|
await session.commit()
|
|
return {"player_id": player.id, "username": player.username}
|