Inventory of the TODO markers living in the two first-party game packages.
contrai-core is clean — zero markers. All four remaining sit in
contrai-engine. No markers exist in contrai-analyzer or contrai-scraper
either, so the list below is the complete inventory for the whole workspace.
Line numbers are as of fix/engine-open-todos @ 120e6e9.
The original inventory listed seven markers. Three are resolved and one moved to
its own issue — see Resolved at the bottom.
1. Seat configuration is hardcoded (2 markers)
# cli.py
# TODO: replace with a seat picker on the landing screen. For now the
# layout matches the design handoff exactly: South is the human, the
# other three seats are AI (expert — the default strategies) — unless
# ``--autoplay`` is set, in which case South is an AI too (see
# ``_build_game``).
HUMAN_SEAT = Position.SOUTH
# landing.py — _panel_players()
TODO: replace with a configurable seat picker when we expose
difficulty / player config on the landing screen.
Two halves of the same feature: _build_game() always seats the human at South
with three AiPlayers, and _panel_players() renders that same layout from its
own literal roles map. The two are only consistent by hand — nothing forces the
landing panel to describe the game the CLI actually builds. Both already handle
the --autoplay case separately, which is two places encoding one fact.
Resolving this means a seat/difficulty picker on the landing screen feeding a
single setup description that both the view and _build_game() read.
MVC note: the picker is user input, so it belongs on the view/controller side;
the resulting configuration should reach Game as data, not as view state.
Scheduling: this overlaps the Contrée-variants roadmap's step 4
(feat/engine-ruleset-ux), whose scope is a game-setup screen listing presets
with a resolved-knob summary and a persisted last-used choice. Building a seat
picker first means building a setup screen that step 4 then rebuilds — the seat
and difficulty pickers are better folded into that screen as one piece of work.
2. Solo Slam is gated exactly like Slam (1 marker)
(SLAM_NUMERIC, {}, 0, 0, 8, False), # Slam — only the trick estimator gates it.
# TODO: tune SoloSlam gate — currently shares Slam's gate. A
# stricter rule (e.g. holds the 8 top trumps in trump-led play,
# or all aces + trump master) would make this conservative.
(SOLO_SLAM_NUMERIC, {}, 0, 0, 8, False), # Solo Slam — same gate as Slam for now.
Both all-tricks rows in BIDDING_TABLE carry an identical gate (tricks_min=8,
no trump/ace requirement), but the two bids are not equivalent: Slam only asks
that the team take all eight tricks, Solo Slam that the bidder personally
does. A hand strong enough for Slam with partner support is routinely not strong
enough for Solo Slam, so the AI over-bids 500 whenever it reaches 250.
The comment already sketches candidate rules (8 top trumps in trump-led play, or
all aces + trump master). Deciding between them needs a hand-strength argument
rather than a code change — see contree-domain.md for what Solo Slam actually
commits the bidder to.
Scheduling: the gate fires rarely (the trick estimator has to reach 8), so
the over-bid is a low-frequency correctness issue rather than a live drag on
strength. Two roadmap steps touch the same rows — step 1 adds an
allow_solo_slam_bid knob, step 2 rewrites the per-mode bid ceilings — so the
rule is worth deciding now and implementing against whichever lands first.
Kept open here deliberately: it is a strategy decision, not deferred work.
Resolved
Closed by fix/engine-open-todos:
Leading-card strategy doesn't exploit a known trump-void table
(card_play.py) — fixed. _play_leading_card hoisted the trump-void check out
of the pull guard and restricts the ace/master search to plain suits when the
opponents are proven void, so a trump ace is no longer cashed where a plain one
wins the same trick. The original framing understated it: the fall-through was
reached as a defender, with no contract, or holding no trump — not only in the
"opponents are out of trump" case it claimed — and the inference had never
reached defenders at all, because the only call sat behind an "our side
declared" guard.
_is_master_card takes a suit where a boolean would do
(card_play.py) — resolved, though not as originally specified. The premise
died with the TrumpRules seam: _get_higher_ranks no longer exists, so
trump_suit had stopped being a bare equality operand and "replace it with a
boolean" became the wrong move. The helper now takes the round's TrumpRules
directly — every call site already holds one, and it was previously re-resolved
once per candidate card inside list comprehensions — with the missing type
hints added.
Game.__init__ requires every player to carry a position (game.py) —
implemented. BasePlayer.position is optional and Game([p1, p2, p3, p4])
seats an unseated roster in list order against list(Position). The
auto-assignment is deterministic; randomised seating stays a caller-side
shuffle, which keeps the RNG where a self-play harness or a test can seed it. A
half-seated list raises rather than being completed. (The Position enum this
was waiting on had already landed by the time the work started.)
Moved out:
Two claims in the original inventory were stale and have been dropped: the 4-AI
simulation mode does have a CLI entry point (--autoplay), and the Position
enum was described as "in flight" when it had already merged.
Inventory of the
TODOmarkers living in the two first-party game packages.contrai-coreis clean — zero markers. All four remaining sit incontrai-engine. No markers exist incontrai-analyzerorcontrai-scrapereither, so the list below is the complete inventory for the whole workspace.
Line numbers are as of
fix/engine-open-todos@120e6e9.The original inventory listed seven markers. Three are resolved and one moved to
its own issue — see Resolved at the bottom.
1. Seat configuration is hardcoded (2 markers)
packages/contrai-engine/src/contrai_engine/cli.py:29packages/contrai-engine/src/contrai_engine/view/screens/landing.py:135Two halves of the same feature:
_build_game()always seats the human at Southwith three
AiPlayers, and_panel_players()renders that same layout from itsown literal
rolesmap. The two are only consistent by hand — nothing forces thelanding panel to describe the game the CLI actually builds. Both already handle
the
--autoplaycase separately, which is two places encoding one fact.Resolving this means a seat/difficulty picker on the landing screen feeding a
single setup description that both the view and
_build_game()read.MVC note: the picker is user input, so it belongs on the view/controller side;
the resulting configuration should reach
Gameas data, not as view state.Scheduling: this overlaps the Contrée-variants roadmap's step 4
(
feat/engine-ruleset-ux), whose scope is a game-setup screen listing presetswith a resolved-knob summary and a persisted last-used choice. Building a seat
picker first means building a setup screen that step 4 then rebuilds — the seat
and difficulty pickers are better folded into that screen as one piece of work.
2. Solo Slam is gated exactly like Slam (1 marker)
packages/contrai-engine/src/contrai_engine/model/player/rule_based/bidding.py:64Both all-tricks rows in
BIDDING_TABLEcarry an identical gate (tricks_min=8,no trump/ace requirement), but the two bids are not equivalent: Slam only asks
that the team take all eight tricks, Solo Slam that the bidder personally
does. A hand strong enough for Slam with partner support is routinely not strong
enough for Solo Slam, so the AI over-bids 500 whenever it reaches 250.
The comment already sketches candidate rules (8 top trumps in trump-led play, or
all aces + trump master). Deciding between them needs a hand-strength argument
rather than a code change — see
contree-domain.mdfor what Solo Slam actuallycommits the bidder to.
Scheduling: the gate fires rarely (the trick estimator has to reach 8), so
the over-bid is a low-frequency correctness issue rather than a live drag on
strength. Two roadmap steps touch the same rows — step 1 adds an
allow_solo_slam_bidknob, step 2 rewrites the per-mode bid ceilings — so therule is worth deciding now and implementing against whichever lands first.
Kept open here deliberately: it is a strategy decision, not deferred work.
Resolved
Closed by
fix/engine-open-todos:Leading-card strategy doesn't exploit a known trump-void table(
card_play.py) — fixed._play_leading_cardhoisted the trump-void check outof the pull guard and restricts the ace/master search to plain suits when the
opponents are proven void, so a trump ace is no longer cashed where a plain one
wins the same trick. The original framing understated it: the fall-through was
reached as a defender, with no contract, or holding no trump — not only in the
"opponents are out of trump" case it claimed — and the inference had never
reached defenders at all, because the only call sat behind an "our side
declared" guard.
_is_master_cardtakes a suit where a boolean would do(
card_play.py) — resolved, though not as originally specified. The premisedied with the
TrumpRulesseam:_get_higher_ranksno longer exists, sotrump_suithad stopped being a bare equality operand and "replace it with aboolean" became the wrong move. The helper now takes the round's
TrumpRulesdirectly — every call site already holds one, and it was previously re-resolved
once per candidate card inside list comprehensions — with the missing type
hints added.
(Game.__init__requires every player to carry a positiongame.py) —implemented.
BasePlayer.positionis optional andGame([p1, p2, p3, p4])seats an unseated roster in list order against
list(Position). Theauto-assignment is deterministic; randomised seating stays a caller-side
shuffle, which keeps the RNG where a self-play harness or a test can seed it. A
half-seated list raises rather than being completed. (The
Positionenum thiswas waiting on had already landed by the time the work started.)
Moved out:
False(bidding.py:238) → engine: implement the AI's redouble strategy (_should_redouble is a permanent False) #10. It is aclassic-ruleset gap needing a hand-strength argument and a signature change, not
a marker to clear, so it earns its own issue.
Two claims in the original inventory were stale and have been dropped: the 4-AI
simulation mode does have a CLI entry point (
--autoplay), and thePositionenum was described as "in flight" when it had already merged.