pyyol 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +58 -23
- package/dist/crash.d.ts +57 -0
- package/dist/crash.js +143 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -2
- package/dist/models.d.ts +82 -0
- package/dist/models.js +10 -0
- package/dist/movetools.d.ts +24 -0
- package/dist/movetools.js +79 -1
- package/dist/runtime.d.ts +18 -16
- package/dist/runtime.js +116 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watch.d.ts +47 -0
- package/dist/watch.js +122 -0
- package/package.json +1 -1
- package/rules/games.md +190 -9
- package/rules/llms-full.txt +372 -33
- package/skill/references/games/_engine_reference.md +190 -9
- package/skill/references/templates/_shared.mjs +71 -0
- package/skill/references/templates/goofspiel_agent.mjs +67 -0
- package/skill/references/templates/mafia_agent.mjs +72 -0
- package/skill/references/templates/monopoly_agent.mjs +124 -0
- package/skill/references/templates/_shared.py +0 -62
- package/skill/references/templates/goofspiel_agent.py +0 -66
- package/skill/references/templates/mafia_agent.py +0 -77
- package/skill/references/templates/monopoly_agent.py +0 -82
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
"""Goofspiel agent — 2 players, 13 rounds, simultaneous bidding.
|
|
2
|
-
|
|
3
|
-
Read references/games/goofspiel.md first. Replace `decide_card`; leave the rest.
|
|
4
|
-
|
|
5
|
-
pyyol login && pyyol dev --matches 5
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from typing import List, Tuple
|
|
11
|
-
|
|
12
|
-
from _shared import MatchMemory
|
|
13
|
-
from pyyol import Adapter
|
|
14
|
-
from pyyol.models import GoofspielMove, GoofspielView
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class GoofspielAgent(Adapter):
|
|
18
|
-
name = "atlas-goofspiel"
|
|
19
|
-
supported_games = ["goofspiel"]
|
|
20
|
-
|
|
21
|
-
def __init__(self) -> None:
|
|
22
|
-
self.mem = MatchMemory()
|
|
23
|
-
|
|
24
|
-
def step(self, view: GoofspielView) -> GoofspielMove:
|
|
25
|
-
safe = min(view.legal_actions) if view.legal_actions else 1
|
|
26
|
-
|
|
27
|
-
if self.mem.already_answered(view.match_id, view.round):
|
|
28
|
-
return GoofspielMove(round=view.round, card=safe, rationale="replayed turn")
|
|
29
|
-
|
|
30
|
-
try:
|
|
31
|
-
card, why = self.decide_card(view)
|
|
32
|
-
except Exception as e: # noqa: BLE001 — never let the deadline decide
|
|
33
|
-
return GoofspielMove(round=view.round, card=safe, rationale=f"fallback: {e}")
|
|
34
|
-
|
|
35
|
-
# The model will occasionally name a card you do not hold. Sending it is
|
|
36
|
-
# recorded as YOUR illegal move.
|
|
37
|
-
if card not in view.legal_actions:
|
|
38
|
-
card, why = safe, f"model chose an illegal card; {why}"
|
|
39
|
-
|
|
40
|
-
return GoofspielMove(round=view.round, card=card, rationale=why[:200])
|
|
41
|
-
|
|
42
|
-
# --- your strategy -----------------------------------------------------
|
|
43
|
-
|
|
44
|
-
def decide_card(self, view: GoofspielView) -> Tuple[int, str]:
|
|
45
|
-
"""Return (card, one-line reason).
|
|
46
|
-
|
|
47
|
-
Bid against `prize_pool`, not `current_prize` — ties carry.
|
|
48
|
-
"""
|
|
49
|
-
opp = self.opponent_hand(view)
|
|
50
|
-
threat = max(opp) if opp else 0
|
|
51
|
-
beats = [c for c in view.legal_actions if c > threat]
|
|
52
|
-
|
|
53
|
-
# Win by ONE. Pips saved on cheap prizes buy the expensive ones later.
|
|
54
|
-
if beats and view.prize_pool >= 7:
|
|
55
|
-
return min(beats), f"pool {view.prize_pool}: cheapest card over {threat}"
|
|
56
|
-
return min(view.legal_actions), f"pool {view.prize_pool}: conceding cheaply"
|
|
57
|
-
|
|
58
|
-
@staticmethod
|
|
59
|
-
def opponent_hand(view: GoofspielView) -> List[int]:
|
|
60
|
-
"""Exactly what they still hold — identical starting hands mean their played
|
|
61
|
-
cards tell you the rest."""
|
|
62
|
-
spent = {r["opp_card"] for r in (view.history or []) if r.get("opp_card")}
|
|
63
|
-
return [c for c in range(1, 14) if c not in spent]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
agent = GoofspielAgent()
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
"""Mafia agent — 12 seats, hidden roles, phase machine.
|
|
2
|
-
|
|
3
|
-
Read references/games/mafia.md first. Note the view uses `legal`, NOT `legal_actions`,
|
|
4
|
-
and `day`/`phase` rather than `round`.
|
|
5
|
-
|
|
6
|
-
Replace `decide`; leave the rest.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from typing import Tuple
|
|
12
|
-
|
|
13
|
-
from _shared import MatchMemory
|
|
14
|
-
from pyyol import Adapter
|
|
15
|
-
from pyyol.models import MafiaMove, MafiaView
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class MafiaAgent(Adapter):
|
|
19
|
-
name = "atlas-mafia"
|
|
20
|
-
supported_games = ["mafia"]
|
|
21
|
-
|
|
22
|
-
def __init__(self) -> None:
|
|
23
|
-
self.mem = MatchMemory()
|
|
24
|
-
|
|
25
|
-
def step(self, view: MafiaView) -> MafiaMove:
|
|
26
|
-
# `legal` — not `legal_actions`. Getting this wrong means every move is
|
|
27
|
-
# rejected and the engine plays for you.
|
|
28
|
-
legal = view.legal or []
|
|
29
|
-
if not legal:
|
|
30
|
-
return MafiaMove(action="", rationale="nothing legal this phase")
|
|
31
|
-
fallback = legal[0]
|
|
32
|
-
|
|
33
|
-
# A turn is (day, phase) here — there is no round number.
|
|
34
|
-
if self.mem.already_answered(view.match_id, (view.day, view.phase)):
|
|
35
|
-
return MafiaMove(action=fallback, rationale="replayed turn")
|
|
36
|
-
|
|
37
|
-
try:
|
|
38
|
-
action, target, text, why = self.decide(view)
|
|
39
|
-
except Exception as e: # noqa: BLE001
|
|
40
|
-
return MafiaMove(action=fallback, rationale=f"fallback: {e}")
|
|
41
|
-
|
|
42
|
-
if action not in legal:
|
|
43
|
-
action, target, text, why = fallback, -1, "", f"illegal action; {why}"
|
|
44
|
-
|
|
45
|
-
# target stays -1 when unused: seat 0 is a REAL player, so a forgotten target
|
|
46
|
-
# would otherwise silently act on them.
|
|
47
|
-
return MafiaMove(action=action, target=target, text=text[:400], rationale=why[:200])
|
|
48
|
-
|
|
49
|
-
# --- your strategy -----------------------------------------------------
|
|
50
|
-
|
|
51
|
-
def decide(self, view: MafiaView) -> Tuple[str, int, str, str]:
|
|
52
|
-
"""Return (action, target, text, reason).
|
|
53
|
-
|
|
54
|
-
`view.public` is the table transcript; `view.private` carries what only you
|
|
55
|
-
know (a Detective's finding arrives there and nowhere else). Rebuild your
|
|
56
|
-
read of each seat from them every turn.
|
|
57
|
-
"""
|
|
58
|
-
notes = self.mem.get(view.match_id)["notes"]
|
|
59
|
-
living = [s for s, ok in (view.alive or {}).items() if ok and s != view.your_seat]
|
|
60
|
-
suspect = max(living, key=lambda s: notes.get(s, 0), default=-1)
|
|
61
|
-
|
|
62
|
-
if "message" in (view.legal or []):
|
|
63
|
-
return (
|
|
64
|
-
"message",
|
|
65
|
-
-1,
|
|
66
|
-
f"Seat {suspect} has been quiet. Thoughts?",
|
|
67
|
-
"opening a line on the current suspect",
|
|
68
|
-
)
|
|
69
|
-
if "vote" in (view.legal or []) and suspect >= 0:
|
|
70
|
-
return "vote", suspect, "", f"voting {suspect}, my standing read"
|
|
71
|
-
for act in ("investigate", "protect", "night_kill", "profile"):
|
|
72
|
-
if act in (view.legal or []) and suspect >= 0:
|
|
73
|
-
return act, suspect, "", f"{act} on {suspect}"
|
|
74
|
-
return (view.legal or [""])[0], -1, "", "no better option this phase"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
agent = MafiaAgent()
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"""Monopoly agent — 2–8 players, board, near-perfect information.
|
|
2
|
-
|
|
3
|
-
Read references/games/monopoly.md first. Drive off `phase` + `legal_actions`; the
|
|
4
|
-
board lives in the raw `state` dict.
|
|
5
|
-
|
|
6
|
-
Replace `decide`; leave the rest.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from typing import Any, Dict, Tuple
|
|
12
|
-
|
|
13
|
-
from _shared import MatchMemory
|
|
14
|
-
from pyyol import Adapter
|
|
15
|
-
from pyyol.models import MonopolyMove, MonopolyView
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class MonopolyAgent(Adapter):
|
|
19
|
-
name = "atlas-monopoly"
|
|
20
|
-
supported_games = ["monopoly"]
|
|
21
|
-
|
|
22
|
-
def __init__(self) -> None:
|
|
23
|
-
self.mem = MatchMemory()
|
|
24
|
-
|
|
25
|
-
def step(self, view: MonopolyView) -> MonopolyMove:
|
|
26
|
-
legal = view.legal_actions or []
|
|
27
|
-
if not legal:
|
|
28
|
-
return MonopolyMove(action="", rationale="nothing legal this phase")
|
|
29
|
-
fallback = "end_turn" if "end_turn" in legal else legal[0]
|
|
30
|
-
|
|
31
|
-
# Monopoly has no round number — the phase plus the board's turn counter is
|
|
32
|
-
# the closest thing, so key on both.
|
|
33
|
-
turn_no = (view.state or {}).get("turn", 0)
|
|
34
|
-
if self.mem.already_answered(view.match_id, (turn_no, view.phase)):
|
|
35
|
-
return MonopolyMove(action=fallback, rationale="replayed turn")
|
|
36
|
-
|
|
37
|
-
try:
|
|
38
|
-
action, prop, amount, why = self.decide(view)
|
|
39
|
-
except Exception as e: # noqa: BLE001
|
|
40
|
-
return MonopolyMove(action=fallback, rationale=f"fallback: {e}")
|
|
41
|
-
|
|
42
|
-
if action not in legal:
|
|
43
|
-
action, prop, amount, why = fallback, 0, 0, f"illegal action; {why}"
|
|
44
|
-
|
|
45
|
-
return MonopolyMove(action=action, property=prop, amount=amount, rationale=why[:200])
|
|
46
|
-
|
|
47
|
-
# --- your strategy -----------------------------------------------------
|
|
48
|
-
|
|
49
|
-
def decide(self, view: MonopolyView) -> Tuple[str, int, int, str]:
|
|
50
|
-
"""Return (action, property, amount, reason).
|
|
51
|
-
|
|
52
|
-
Read `phase` for the situation and `legal_actions` for what is allowed —
|
|
53
|
-
do not assume fixed field names in `state`.
|
|
54
|
-
"""
|
|
55
|
-
legal = view.legal_actions or []
|
|
56
|
-
me: Dict[str, Any] = (view.state or {}).get("players", {}).get(str(view.seat), {})
|
|
57
|
-
cash = int(me.get("cash", 0) or 0)
|
|
58
|
-
|
|
59
|
-
if view.phase == "acquire" and "buy" in legal:
|
|
60
|
-
# Keep a reserve: bankruptcy is the only true loss condition, and it is
|
|
61
|
-
# usually caused by buying into a rent spike.
|
|
62
|
-
if cash > 400:
|
|
63
|
-
return "buy", 0, 0, f"buying with {cash} cash in hand"
|
|
64
|
-
return (
|
|
65
|
-
"decline" if "decline" in legal else legal[0],
|
|
66
|
-
0,
|
|
67
|
-
0,
|
|
68
|
-
f"declining, only {cash} cash",
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
if view.phase == "auction" and "pass" in legal:
|
|
72
|
-
return "pass", 0, 0, "not overpaying at auction"
|
|
73
|
-
|
|
74
|
-
if view.phase == "roll" and "roll" in legal:
|
|
75
|
-
return "roll", 0, 0, "rolling"
|
|
76
|
-
|
|
77
|
-
if "end_turn" in legal:
|
|
78
|
-
return "end_turn", 0, 0, "nothing worth doing this phase"
|
|
79
|
-
return legal[0], 0, 0, "first legal action"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
agent = MonopolyAgent()
|