Skip to content

chore: release v0.2.0 - core PlayState, expert AI card play, TUI review#4

Merged
valmathieu merged 84 commits into
mainfrom
dev/review_tui_core
Jul 25, 2026
Merged

chore: release v0.2.0 - core PlayState, expert AI card play, TUI review#4
valmathieu merged 84 commits into
mainfrom
dev/review_tui_core

Conversation

@valmathieu

Copy link
Copy Markdown
Owner

What

The v0.2.0 milestone (84 commits): the core gains a real play-phase model, the expert AI a sound card-play brain, and the whole TUI a review pass — closed out with the changelog and lockstep version bumps to 0.2.0.

  • CorePlayState, an immutable trick-by-trick state machine owning follow/trump legality, turn order, and the typed OUT_OF_TURN/CARD_NOT_IN_HAND violations; PlayObservation, the imperfect-information projection a single seat is allowed to see — the input surface for AI card-play strategies.
  • Engine, play — the trick loop now runs off the core PlayState, and card-play strategies receive a frozen PlayObservation from which they derive card tracking (fallen cards, per-seat voids). On that base the expert AI stops pulling trumps once both opponents are void, preserves master cards behind a winning partner, and anticipates opponent ruffs.
  • Engine, bidding — internal representation unified on typed Bid/Auction objects (legacy wire-format bridge removed); partner-support escalation capped at the team ceiling; suit tie-break and bid-rejection-hint fixes.
  • Engine, lifecycle — sudden-death tiebreaker rounds when both teams finish level at or above the target, so a game always names a winner.
  • View/TUI — the view split into screen modules (landing, bidding, trick, round recap, end game) plus theme/layout/formatting helpers; wording and label review (double/redouble vocabulary, expert seat labels, trump glyphs in the log).
  • Docs & diagrams — class/sequence diagrams and package docs updated to match the new package splits.

Why

This closes the "review the TUI, harden the core" phase: the play phase is modeled in contrai-core where future search-based AIs can fork it, strategies are sealed behind an observation boundary (groundwork for the AI ladder), and the CLI is presentable end to end.

Release plan

Rebase-merge (the branch is atomic Conventional Commits), then tag v0.2.0 on main and publish the GitHub release with the changelog section.

Annotate Game.manage_round's return value with a new RoundResult
TypedDict and type its `view` parameter as RichView | None. Add
`from __future__ import annotations` plus a TYPE_CHECKING import so the
View can be referenced unquoted without the Model importing it at
runtime (preserves MVC layering).
Annotate Game.check_game_over's return value with a new GameOverStatus
TypedDict and type its `target_score` parameter as int, matching the
treatment given to manage_round.
Add tests for the previously untested round orchestrator and the
players-order helper: the completed-round and all-pass redeal paths,
cross-round score accumulation, view deal/redeal callbacks, the
shuffle-vs-cut branch, anticlockwise ordering after the dealer, and the
random first-dealer branch. A FakeRound double isolates manage_round from
the full bidding/trick lifecycle.
The CLI discarded manage_round's return value and read game.current_contract
and game.scores directly; the returned RoundResult merely snapshotted that live
state and baked a presentation-only 'message' string into the Model. Remove the
TypedDict and let manage_round return None, mutating Game/Round in place. Tests
now assert on game state rather than the dict; diagrams and engine docs updated.
GameOverStatus genuinely flows Model to View (the CLI reads .game_over and
forwards the whole verdict to show_end_game / the game-over banner). Replacing
the TypedDict with a frozen dataclass gives attribute access and immutability;
consumers switch from status['game_over'] to status.game_over and the banner
helper drops its defensive dict .get() defaults now that the fields are
guaranteed. Tests and the class diagram updated to match.
Replace the French terms (dix de der, der, chute, contre) with their
English equivalents across the recap view, the scoring model, and
their test suites. Rename the recap-breakdown keys dix_de_der ->
last_trick_bonus and dix_count -> last_trick_counts, and the affected
test names, to match.
Align with the English-only vocabulary rule (Slam, not capot): the
Round/RoundScore attribute, the scoring locals and 250-substitute
constant, and the recap's slam_label parameter, with tests, the engine
doc page, and the three affected diagrams (re-rendered) following.
Also translate the last French term left in those docs - dix-de-der
becomes the last-trick bonus in index.md, flow_scoring.mmd, and
class_engine.puml.
Add short Google-style docstrings to the remaining undocumented panel
builders, prompt builders, and formatting helpers in view/ (trick,
endgame, landing, layout, formatting, rich_view).
The "Unrecognized bid" notice showed a fixed '80 h' example even when
the auction had moved past 80. It now derives the example from
Auction.legal_actions like the adaptive prompt does — '100 h' once 90
stands — and drops the numeric example when only Slam raises remain.
- Round.play_trick now returns None: the winner was already published
  on last_trick_winner and no caller consumed the return value.
- Drop the unused trick_num/winner locals in play_all_tricks.
- The on_all_pass_redeal view hook is now a pure announcement: its
  unused round_ parameter is gone, and Game's call site and the test
  view doubles follow suit.
- Unquote TYPE_CHECKING-only forward references (Player, Deck, Round);
  deferred annotation evaluation makes the quotes redundant.
- Trim stale section pointers from scoring's rule comments.
- Sync the engine class/round/trick diagrams (sources + PNGs) with the
  play_trick and on_all_pass_redeal signature changes.
…ndings

Turn the over-limit rule_based module (1054 lines, pylint C0302) into a
rule_based/ package - bidding.py and card_play.py, re-exported from
__init__.py so existing imports are unchanged. Along the way fix every
other pylint finding: trailing whitespace, over-long lines,
no-else-return, consider-using-in, the unused voids argument on
_play_following_card, and too-many-locals in _evaluate_suit_as_trump
(table scan extracted to _max_table_contract). Only the deliberate TODO
markers (W0511) remain.
When the team already holds the trick and the seat follows suit, the
expert AI now plays the highest-points non-master card, keeping a card
its partner's own play just promoted (their Ace makes our Ten the new
suit master) to win a later trick. Trump-led tricks get the same guard;
when the seat's only card of the led suit is that master, the forced
play goes through unchanged.

The stacked-deck lifecycle pin moves to the new deterministic playout:
the preserved trump ten now cashes late and flips the last trick, so
the declarers close at 278/14 instead of 261/31 on the same deal.
Track per-suit voids for every seat (any failure to follow proves the
led-suit void; the trump inference keeps its partner-master exemption)
and predict a ruff when an opponent still to play is void in the led
suit and may hold trump. Behind a winning partner the AI then concedes
its cheapest card instead of piling points; on a losing trick it beats
with the smallest winning card instead of the highest-points one.
A game could end with no winner when both teams finished a round level
at or above the target score. Such a tie is now sudden death:
check_game_over keeps the game running (tied_teams flags the state),
the round recap announces the tiebreaker round, and play continues
until one team strictly leads - so the game-over banner always names
a winner.
…star

The event-log "Contract set" line carried no trump hint; it now slots
the suit glyph after the value ("Contract set: 110 ♠ by N."). The Round
panel's Trump line loses its ★ flourish, so the star flag on
_format_trump_label is gone entirely (the recap already opted out).
The summarize pass reflowed the Fixed section and lost the AI-seat-label
bullet in the process; restore it. Add the never-logged bullet for
illegal-play errors naming the acting seat, and drop a stray double
space left by the same reflow.
Close out the changelog — the Unreleased section becomes 0.2.0, dated
2026-07-25, with a fresh empty Unreleased above it and updated compare
links — and bump the four workspace packages to 0.2.0 in lockstep
(pyproject.toml files and uv.lock).
@valmathieu
valmathieu merged commit 879e938 into main Jul 25, 2026
2 checks passed
@valmathieu
valmathieu deleted the dev/review_tui_core branch July 25, 2026 15:44
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

v0.2.0: core PlayState, expert AI card-play rules, TUI review pass

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds PlayState/PlayObservation to contrai-core: immutable play-phase state +
 imperfect-information AI input.
• Rewires engine play/bidding to use core PlayState/Auction/Bid and strict illegal-play
 errors.
• Upgrades expert AI bidding/card-play via observation-derived tracking; fixes escalation and
 tie-breaks.
• Splits monolithic Rich TUI into screen + helper modules; refreshes wording, labels, and prompts.
• Publishes v0.2.0 docs/diagrams/changelog and lockstep bumps workspace package versions.
Diagram

graph TD
  Round["Round (engine)"] --> PlayState["PlayState (core)"] --> Observation["PlayObservation"] --> CardPlay["RuleBasedCardPlayStrategy"]
  Round --> Auction["Auction/Bid (core)"] --> Bidding["RuleBasedBiddingStrategy"]
  Round --> RichView["RichView orchestrator"]
  RichView --> Screens["screens/* renderers"]
  Screens --> Shared["theme/formatting/layout/parsing"]
  Game["Game lifecycle"] --> Round
Loading
High-Level Assessment

The PR’s approach is consistent with the existing core design: extend the immutable Auction/Bid state-machine pattern to card play (PlayState + typed violations) and add a clear observation boundary (PlayObservation) before introducing stronger AI. Given the goals (search-ready core, strategy isolation, strict legality), this is the most maintainable direction; alternatives (continuing with mutable engine objects or per-strategy stateful tracking) would keep information-leak risks and compound future refactors.

Files changed (80) +10492 / -2586 · 3 not counted

Enhancement (8) +2116 / -0
__init__.pyRe-export new core play APIs +4/-0

Re-export new core play APIs

• Updates core package exports to include new play-phase types introduced in this release.

packages/contrai-core/src/contrai_core/init.py

exceptions.pyAdd OUT_OF_TURN and CARD_NOT_IN_HAND violations +8/-0

Add OUT_OF_TURN and CARD_NOT_IN_HAND violations

• Extends PlayRuleViolation for strict turn-order and hand-membership enforcement by PlayState.apply.

packages/contrai-core/src/contrai_core/exceptions.py

play.pyIntroduce PlayState and PlayObservation +754/-0

Introduce PlayState and PlayObservation

• Adds immutable PlayState (legal_actions/apply/with_hands/observe) and PlayObservation (imperfect-information view) plus supporting helpers for trump/follow obligations and typed illegal-play classification.

packages/contrai-core/src/contrai_core/play.py

ai.pyAdd AiPlayer delegating to strategies +51/-0

Add AiPlayer delegating to strategies

• New AiPlayer holds bidding/card-play strategy instances (factory-injected) and delegates choose_bid/choose_card to them.

packages/contrai-engine/src/contrai_engine/model/player/ai.py

levels.pyAdd AI level wiring helpers +37/-0

Add AI level wiring helpers

• Defines level factories/mappings for constructing AI players with chosen strategies.

packages/contrai-engine/src/contrai_engine/model/player/levels.py

card_play.pyRewrite expert card-play on PlayObservation-derived tracking +709/-0

Rewrite expert card-play on PlayObservation-derived tracking

• Implements expert card-play decisions as a pure function of PlayObservation, deriving fallen cards and void suits from public history to improve trump management and ruff anticipation.

packages/contrai-engine/src/contrai_engine/model/player/rule_based/card_play.py

strategy.pyAdd pluggable strategy interfaces and PlayerStateMixin +99/-0

Add pluggable strategy interfaces and PlayerStateMixin

• Defines BiddingStrategy/CardPlayStrategy interfaces and a mixin that provides live read access to the owning player’s state for strategy implementations.

packages/contrai-engine/src/contrai_engine/model/player/strategy.py

round.pyDrive trick loop via core PlayState + project observations to AI +454/-0

Drive trick loop via core PlayState + project observations to AI

• Replaces engine-side legality/trick bookkeeping with PlayState as source of truth; mirrors hands/trick for view; AI receives PlayObservation; illegal/absent card now raises IllegalPlayError instead of being skipped.

packages/contrai-engine/src/contrai_engine/model/round/round.py

Bug fix (4) +704 / -51
cli.pyUse typed GameOverStatus and tiebreaker recap flags +9/-5

Use typed GameOverStatus and tiebreaker recap flags

• Updates main game loop to use Game.check_game_over() dataclass result and passes is_tiebreaker into recap; updates AI seat comment to expert.

packages/contrai-engine/src/contrai_engine/cli.py

game.pyIntroduce GameOverStatus and sudden-death semantics +81/-46

Introduce GameOverStatus and sudden-death semantics

• Adds a typed GameOverStatus, types manage_round/check_game_over, and changes game-over logic to require a strict lead at/above target (ties trigger sudden death).

packages/contrai-engine/src/contrai_engine/model/game.py

bidding.pyRewrite expert bidding on typed Auction/Bid + cap support escalation +560/-0

Rewrite expert bidding on typed Auction/Bid + cap support escalation

• Implements the expert bidding table directly on typed core Bid/Auction objects, fixes suit tie-break behavior, and caps partner-support escalation at a team ceiling to prevent ratcheting.

packages/contrai-engine/src/contrai_engine/model/player/rule_based/bidding.py

bidding_rules.pyCentralize illegal-bid reason/hint logic +54/-0

Centralize illegal-bid reason/hint logic

• Introduces helper used by TUI to explain illegal bids and suggest the cheapest legal raise (instead of a fixed example).

packages/contrai-engine/src/contrai_engine/view/bidding_rules.py

Refactor (27) +3160 / -2243
card.pySmall core card adjustments +1/-1

Small core card adjustments

• Minor adjustments to card implementation to support new play/strategy and diagram updates.

packages/contrai-core/src/contrai_core/card.py

contract.pyAlign contract helpers with new play model +4/-2

Align contract helpers with new play model

• Small contract updates to support PlayState/observation use and consistency across engine/scoring.

packages/contrai-core/src/contrai_core/contract.py

deck.pyMinor deck tweaks for updated round flow +1/-1

Minor deck tweaks for updated round flow

• Small changes aligning deck interactions with new engine round/trick management.

packages/contrai-core/src/contrai_core/deck.py

hand.pySmall hand adjustments +2/-2

Small hand adjustments

• Minor hand behavior tweaks to support the new play-state mirroring and tests.

packages/contrai-core/src/contrai_core/hand.py

team.pyMinor team adjustments +1/-1

Minor team adjustments

• Small updates to team domain model used across engine legality and scoring paths.

packages/contrai-core/src/contrai_core/team.py

trick.pyExtract current_winner helper for partial tricks +55/-26

Extract current_winner helper for partial tricks

• Adds module-level current_winner(plays, trump_suit) and rewires Trick.get_current_winner to use it, enabling consistent winner checks during in-progress tricks.

packages/contrai-core/src/contrai_core/trick.py

__init__.pyAdjust model exports for new package splits +6/-3

Adjust model exports for new package splits

• Updates imports/exports to reflect moving player and round into subpackages.

packages/contrai-engine/src/contrai_engine/model/init.py

__init__.pyCreate player package public surface +25/-0

Create player package public surface

• Introduces player package initializer exporting new Player/AiPlayer/HumanPlayer/strategy plumbing and preserving legacy imports where needed.

packages/contrai-engine/src/contrai_engine/model/player/init.py

base.pyDefine Player and HumanPlayer ABCs +97/-0

Define Player and HumanPlayer ABCs

• Adds abstract engine-side Player (decision hooks) and HumanPlayer that defers both decisions to the view.

packages/contrai-engine/src/contrai_engine/model/player/base.py

__init__.pyExpose rule-based strategy implementations +17/-0

Expose rule-based strategy implementations

• Adds package initializer exporting expert rule-based bidding and card-play strategies.

packages/contrai-engine/src/contrai_engine/model/player/rule_based/init.py

__init__.pyCreate round package exports +14/-0

Create round package exports

• Adds round package initializer exposing Round and scoring utilities after splitting the old round.py file.

packages/contrai-engine/src/contrai_engine/model/round/init.py

scoring.pyExtract scoring into pure scoring module +324/-0

Extract scoring into pure scoring module

• Moves round scoring logic into a focused module returning structured results consumed by the view and game lifecycle.

packages/contrai-engine/src/contrai_engine/model/round/scoring.py

__init__.pyDocument and re-export RichView from view package +17/-1

Document and re-export RichView from view package

• Adds view package docstring describing the new module split and re-exports RichView/RoundSummary for simpler imports.

packages/contrai-engine/src/contrai_engine/view/init.py

formatting.pyExtract shared formatting helpers +216/-0

Extract shared formatting helpers

• Adds stateless formatting utilities for cards, suits, positions, and contract labels used across screens and logs.

packages/contrai-engine/src/contrai_engine/view/formatting.py

layout.pyExtract shared layout builders +120/-0

Extract shared layout builders

• Adds helpers for common Rich layout composition (panels, columns, prompt areas) reused across screens.

packages/contrai-engine/src/contrai_engine/view/layout.py

parsing.pyExtract human input parsing for bids and cards +119/-0

Extract human input parsing for bids and cards

• Adds parsers to convert user input strings into typed Bid/Card values using shared theme tables and legality rules.

packages/contrai-engine/src/contrai_engine/view/parsing.py

rich_view.pyRefactor RichView into orchestrator; move rendering to screen modules +143/-2206

Refactor RichView into orchestrator; move rendering to screen modules

• Removes most embedded rendering/parsing/layout logic and rewires RichView to call dedicated screen and helper modules while keeping all terminal I/O orchestration centralized.

packages/contrai-engine/src/contrai_engine/view/rich_view.py

__init__.pyAdd screens package initializer +7/-0

Add screens package initializer

• Creates screens package to hold per-screen rendering modules for the TUI.

packages/contrai-engine/src/contrai_engine/view/screens/init.py

bidding.pyAdd bidding screen rendering module +251/-0

Add bidding screen rendering module

• Extracts bidding UI: history panel, bidding diamond, prompts, and AI announcements with reviewed wording and better illegal-bid messaging.

packages/contrai-engine/src/contrai_engine/view/screens/bidding.py

endgame.pyAdd end-game screen rendering module +188/-0

Add end-game screen rendering module

• Extracts end-game banner, final scoreboard, and replay/quit prompt; supports sudden-death end conditions.

packages/contrai-engine/src/contrai_engine/view/screens/endgame.py

landing.pyAdd landing screen rendering module +168/-0

Add landing screen rendering module

• Extracts landing screen UI (title/subtitle/suit ribbon/target selection/player roster) and updates labels (expert AI seats).

packages/contrai-engine/src/contrai_engine/view/screens/landing.py

recap.pyAdd round recap screen rendering module +612/-0

Add round recap screen rendering module

• Extracts between-round recap rendering including contract made/failed, slam tags, and tiebreaker announcements.

packages/contrai-engine/src/contrai_engine/view/screens/recap.py

trick.pyAdd in-trick screen rendering module +564/-0

Add in-trick screen rendering module

• Extracts in-game table rendering (round panel, current/last trick, hand row, prompts) with live winner highlight and belote badges.

packages/contrai-engine/src/contrai_engine/view/screens/trick.py

state_helpers.pyAdd view state-reader helpers +129/-0

Add view state-reader helpers

• Introduces helpers to compute view-facing derived state (current winner, belote status, constraints explanation, delays, hand sorting).

packages/contrai-engine/src/contrai_engine/view/state_helpers.py

theme.pyAdd TUI design tokens + shared constants +70/-0

Add TUI design tokens + shared constants

• Centralizes color palette and shared lookup tables (target options, suit aliases, valid bid values) to keep view modules consistent.

packages/contrai-engine/src/contrai_engine/view/theme.py

main.pyMinor scraper adjustments +7/-0

Minor scraper adjustments

• Small updates to keep the scraper aligned with workspace versioning and packaging changes.

packages/contrai-scraper/main.py

run.pyMinor runner adjustments +2/-0

Minor runner adjustments

• Small updates to keep run entrypoints aligned with packaging and workspace changes.

packages/contrai-scraper/run.py

Documentation (19) +856 / -267
CHANGELOG.mdAdd v0.2.0 release notes and compare links +57/-5

Add v0.2.0 release notes and compare links

• Adds a comprehensive 0.2.0 section documenting the new core play model, AI improvements, TUI refactor, and bug fixes; updates Unreleased comparison links.

CHANGELOG.md

contree-domain.mdRefresh domain documentation for new core model +27/-1

Refresh domain documentation for new core model

• Updates domain notes to reflect the new play-phase state/observation model and related engine/view adjustments.

contree-domain.md

rule_based.mdUpdate rule-based AI ladder docs +4/-2

Update rule-based AI ladder docs

• Adjusts documentation to match the new strategy interfaces and observation-driven card-play rules.

docs/ai-ladder/rule_based.md

architecture.mdUpdate architecture index reference +1/-1

Update architecture index reference

• Minor documentation tweak to keep architecture docs aligned with the package restructuring.

docs/architecture.md

index.mdDocument PlayState and PlayObservation in core docs +10/-5

Document PlayState and PlayObservation in core docs

• Updates core package documentation to describe the new play-phase state machine and imperfect-information observation boundary.

docs/core/index.md

class_core.pumlAdd PlayState/PlayObservation to core class diagram +101/-0

Add PlayState/PlayObservation to core class diagram

• Updates the core class diagram to include PlayState, PlayObservation, and their relationships to existing domain objects.

docs/diagrams/class_core.puml

class_engine.pumlRevise engine class diagram for module splits +260/-146

Revise engine class diagram for module splits

• Reworks the engine diagram to reflect new model package splits (player/, round/, view/screens) and updated dependencies on core types.

docs/diagrams/class_engine.puml

class_workspace.pngRegenerate workspace diagram PNG not counted

Regenerate workspace diagram PNG

• Updates the rendered diagram image to match the updated diagram sources.

docs/diagrams/class_workspace.png

class_workspace.pumlUpdate workspace package diagram source +0/-2

Update workspace package diagram source

• Adjusts package/component diagram source to match the new module/package layout.

docs/diagrams/class_workspace.puml

flow_scoring.mmdAdd/update scoring flow diagram +55/-0

Add/update scoring flow diagram

• Adds a scoring flow diagram reflecting the extracted scoring module and updated round lifecycle outputs used by the view.

docs/diagrams/flow_scoring.mmd

flow_scoring.pngRegenerate scoring flow PNG not counted

Regenerate scoring flow PNG

• Updates rendered flow diagram image to match the updated Mermaid source.

docs/diagrams/flow_scoring.png

index.mdUpdate diagrams index +3/-1

Update diagrams index

• Keeps diagrams index in sync with newly added/updated diagram assets.

docs/diagrams/index.md

seq_bidding.pumlUpdate bidding sequence diagram +21/-26

Update bidding sequence diagram

• Adjusts the bidding sequence to show typed Bid/Auction flow and updated view hooks.

docs/diagrams/seq_bidding.puml

seq_cli.pngRegenerate CLI sequence PNG not counted

Regenerate CLI sequence PNG

• Updates rendered CLI sequence image to match updated sequence source(s).

docs/diagrams/seq_cli.png

seq_cli.pumlUpdate CLI sequence diagram for new lifecycle +123/-0

Update CLI sequence diagram for new lifecycle

• Updates CLI sequence to reflect new sudden-death game-over flow and view screen orchestration.

docs/diagrams/seq_cli.puml

seq_round.pumlUpdate round sequence diagram +56/-13

Update round sequence diagram

• Revises round lifecycle sequence for PlayState-driven trick loop and updated scoring/publication steps.

docs/diagrams/seq_round.puml

seq_trick.pumlUpdate trick sequence diagram for PlayState/observation +81/-39

Update trick sequence diagram for PlayState/observation

• Updates trick sequence to show Round advancing PlayState and projecting PlayObservation to strategies.

docs/diagrams/seq_trick.puml

state_cli_screens.mmdUpdate CLI screens state diagram +7/-4

Update CLI screens state diagram

• Updates screen state machine to reflect the extracted screen modules and new recap/endgame conditions (tiebreaker).

docs/diagrams/state_cli_screens.mmd

index.mdUpdate engine docs for new module layout +50/-22

Update engine docs for new module layout

• Documents the new player/round/view module structure and revised responsibilities (strategies, screens, scoring).

docs/engine/index.md

Other (22) +3656 / -25
.gitignoreIgnore additional generated files +6/-1

Ignore additional generated files

• Extends ignore rules to cover additional generated/temporary artifacts introduced during development and docs generation.

.gitignore

pyproject.tomlBump contrai-analyzer to v0.2.0 +1/-1

Bump contrai-analyzer to v0.2.0

• Lockstep workspace version bump to 0.2.0.

packages/contrai-analyzer/pyproject.toml

pyproject.tomlBump contrai-core to v0.2.0 +1/-1

Bump contrai-core to v0.2.0

• Lockstep workspace version bump to 0.2.0.

packages/contrai-core/pyproject.toml

conftest.pyExpand core test fixtures +36/-0

Expand core test fixtures

• Adds/extends fixtures to support new play-state and observation test suites.

packages/contrai-core/tests/conftest.py

test_auction.pyUpdate auction tests for typed objects +6/-0

Update auction tests for typed objects

• Small updates ensuring auction behaviors remain correct under the new typed bid usage patterns.

packages/contrai-core/tests/test_auction.py

test_bid.pyUpdate bid tests +5/-0

Update bid tests

• Small adjustments to bid tests to align with the updated bid usage and documentation.

packages/contrai-core/tests/test_bid.py

test_card.pyUpdate card tests +6/-0

Update card tests

• Small test updates for card behaviors used by the new play logic and strategies.

packages/contrai-core/tests/test_card.py

test_contract.pyUpdate contract tests +6/-0

Update contract tests

• Small test updates to keep contract behavior consistent with updated engine/core usage.

packages/contrai-core/tests/test_contract.py

test_deck.pyUpdate deck tests +9/-0

Update deck tests

• Small test updates reflecting deck interactions used by the new round lifecycle.

packages/contrai-core/tests/test_deck.py

test_exceptions.pyUpdate exceptions tests for new violations +5/-1

Update exceptions tests for new violations

• Extends tests to cover newly added play rule violation codes and error contexts.

packages/contrai-core/tests/test_exceptions.py

test_play_legality.pyAdd PlayState legality tests +380/-0

Add PlayState legality tests

• New tests covering follow-suit, trumping, over-trumping, partner-master exemption, and discard rules via PlayState.legal_actions.

packages/contrai-core/tests/test_play_legality.py

test_play_observation.pyAdd PlayObservation projection tests +249/-0

Add PlayObservation projection tests

• New tests ensuring observations contain only observer hand + public history + legal cards and compute derived helpers correctly.

packages/contrai-core/tests/test_play_observation.py

test_play_state.pyAdd PlayState apply/derived-view tests +421/-0

Add PlayState apply/derived-view tests

• New tests for derived trick views, to_act, terminal detection, apply() enforcement, and with_hands determinization checks.

packages/contrai-core/tests/test_play_state.py

test_team.pyUpdate team tests +6/-0

Update team tests

• Small test updates aligning team behavior with updated engine usage.

packages/contrai-core/tests/test_team.py

test_trick.pyUpdate trick tests for current_winner extraction +72/-0

Update trick tests for current_winner extraction

• Updates tests to cover module-level current_winner and Trick winner behavior consistency.

packages/contrai-core/tests/test_trick.py

pyproject.tomlBump contrai-engine to v0.2.0 +1/-1

Bump contrai-engine to v0.2.0

• Lockstep workspace version bump to 0.2.0.

packages/contrai-engine/pyproject.toml

test_game.pyUpdate game tests for typed status and sudden death +262/-15

Update game tests for typed status and sudden death

• Updates/extends tests to cover the new check_game_over semantics (strict lead required) and typed GameOverStatus outputs.

packages/contrai-engine/tests/test_model/test_game.py

test_rule_based_cardplay.pyAdd/expand expert card-play strategy tests +1164/-0

Add/expand expert card-play strategy tests

• Adds extensive tests validating observation-derived tracking, trump-pulling rules, master preservation, and ruff anticipation behaviors.

packages/contrai-engine/tests/test_model/test_rule_based_cardplay.py

test_recap.pyAdd/expand view recap rendering tests +1009/-0

Add/expand view recap rendering tests

• Adds comprehensive rendering tests for recap panels including tiebreaker messaging and final-round variants.

packages/contrai-engine/tests/test_view/test_recap.py

pyproject.tomlBump contrai-scraper to v0.2.0 +1/-1

Bump contrai-scraper to v0.2.0

• Lockstep workspace version bump to 0.2.0.

packages/contrai-scraper/pyproject.toml

pyproject.tomlConfigure pylint docstring exemptions for tests +6/-0

Configure pylint docstring exemptions for tests

• Adds pylint config to exempt pytest test functions/classes from docstring requirements while preserving public-API docstring policy.

pyproject.toml

uv.lockRefresh uv lockfile for v0.2.0 bumps +4/-4

Refresh uv lockfile for v0.2.0 bumps

• Updates lockfile entries to match lockstep package version bumps and workspace metadata changes.

uv.lock

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Dealer advances on redeal 🐞 Bug ≡ Correctness
Description
When all players pass, Game.manage_round() returns after collecting cards; the CLI loop then calls
manage_round() again, which calls start_new_round() and advances the dealer via next_dealer(),
violating the documented rule that an all-pass redeal uses the same dealer.
Code

packages/contrai-engine/src/contrai_engine/model/game.py[R158-165]

+        # If no contract (all passed), handle failed contract (redistributes cards).
        if not contract:
-            round_scores = self.current_round.handle_failed_contract()
-            # Notify the view that the round will be redealt.
+            self.current_round.handle_failed_contract()
+            # Notify the view that the round will be redealt. The hook is a
+            # pure announcement — it carries no round payload.
            if view is not None and hasattr(view, 'on_all_pass_redeal'):
-                view.on_all_pass_redeal(self.current_round)
-            return {
-                'contract': None,
-                'scores': round_scores,
-                'total_scores': self.scores.copy(),
-                'message': 'All players passed. Cards redistributed.'
-            }
+                view.on_all_pass_redeal()
+            return
Evidence
The domain rules explicitly require the same dealer on an all-pass; the CLI calls manage_round()
repeatedly, and start_new_round() always calls next_dealer(), so returning from manage_round()
on all-pass causes the dealer to advance before the redeal.

contree-domain.md[166-175]
packages/contrai-engine/src/contrai_engine/cli.py[49-70]
packages/contrai-engine/src/contrai_engine/model/game.py[109-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
After an all-pass auction, the rules state the cards are collected and redealt **with the same dealer**. Currently `Game.manage_round()` returns on all-pass, and the CLI loop calls `manage_round()` again; the next call unconditionally runs `start_new_round()` which unconditionally calls `next_dealer()`, so the dealer advances before the redeal.

## Issue Context
- The CLI drives rounds in a loop and does not special-case all-pass.
- `start_new_round()` always advances the dealer.

## Fix Focus Areas
- packages/contrai-engine/src/contrai_engine/model/game.py[109-165]
- packages/contrai-engine/src/contrai_engine/cli.py[49-70]

## Suggested fix approach
- Add an internal flag on `Game` like `_pending_redeal_same_dealer: bool`.
- In `manage_round()`, when `contract` is falsy (all-pass), set the flag and return (or redeal inline).
- In `start_new_round()`, only call `next_dealer()` when the flag is not set; clear the flag once consumed.
- Ensure `players_order` is recomputed appropriately for the dealer who is re-dealing (same as current dealer).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. with_hands allows duplicates 🐞 Bug ≡ Correctness
Description
PlayState.with_hands() rejects already-played cards but does not enforce uniqueness across the
replacement hands, allowing the same card to exist in multiple hands and be played twice in a
determinized fork.
Code

packages/contrai-core/src/contrai_core/play.py[R432-456]

+        new_hands = tuple(tuple(hand) for hand in hands)
+        if len(new_hands) != len(self.players):
+            raise ValueError(
+                f"Expected {len(self.players)} hands, got {len(new_hands)}."
+            )
+        for seat, hand in enumerate(new_hands):
+            if len(hand) != len(self.hands[seat]):
+                raise ValueError(
+                    f"Seat {seat} must keep {len(self.hands[seat])} cards; "
+                    f"replacement has {len(hand)}."
+                )
+        played = {play.card for play in self.plays}
+        for hand in new_hands:
+            for card in hand:
+                if card in played:
+                    raise ValueError(
+                        f"Replacement hand contains an already-played card: "
+                        f"{card!r}."
+                    )
+        return PlayState(
+            contract=self.contract,
+            players=self.players,
+            hands=new_hands,
+            plays=self.plays,
+        )
Evidence
PlayState.start() explicitly enforces that the dealt cards are distinct across all hands;
with_hands() currently only checks for intersection with already-played cards and therefore can
accept duplicate cards across hands.

packages/contrai-core/src/contrai_core/play.py[121-160]
packages/contrai-core/src/contrai_core/play.py[409-456]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PlayState.with_hands()` validates per-seat card counts and prevents replacement cards that already appear in `self.plays`, but it does not prevent duplicates across the provided replacement hands (or within a hand). This can create an impossible play state (a single card owned by multiple seats).

## Issue Context
`PlayState.start()` enforces that all 32 dealt cards are distinct across hands; determinization forks should preserve the same invariant to avoid invalid downstream simulations.

## Fix Focus Areas
- packages/contrai-core/src/contrai_core/play.py[113-160]
- packages/contrai-core/src/contrai_core/play.py[409-456]

## Suggested fix approach
- In `with_hands()`, flatten all replacement cards into one list.
- Reject duplicates by checking `len(set(all_replacement_cards)) == len(all_replacement_cards)`.
- Keep the existing check that no replacement card appears in `self.plays` (can be folded into the same set-based validation).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +158 to +165
# If no contract (all passed), handle failed contract (redistributes cards).
if not contract:
round_scores = self.current_round.handle_failed_contract()
# Notify the view that the round will be redealt.
self.current_round.handle_failed_contract()
# Notify the view that the round will be redealt. The hook is a
# pure announcement — it carries no round payload.
if view is not None and hasattr(view, 'on_all_pass_redeal'):
view.on_all_pass_redeal(self.current_round)
return {
'contract': None,
'scores': round_scores,
'total_scores': self.scores.copy(),
'message': 'All players passed. Cards redistributed.'
}
view.on_all_pass_redeal()
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Dealer advances on redeal 🐞 Bug ≡ Correctness

When all players pass, Game.manage_round() returns after collecting cards; the CLI loop then calls
manage_round() again, which calls start_new_round() and advances the dealer via next_dealer(),
violating the documented rule that an all-pass redeal uses the same dealer.
Agent Prompt
## Issue description
After an all-pass auction, the rules state the cards are collected and redealt **with the same dealer**. Currently `Game.manage_round()` returns on all-pass, and the CLI loop calls `manage_round()` again; the next call unconditionally runs `start_new_round()` which unconditionally calls `next_dealer()`, so the dealer advances before the redeal.

## Issue Context
- The CLI drives rounds in a loop and does not special-case all-pass.
- `start_new_round()` always advances the dealer.

## Fix Focus Areas
- packages/contrai-engine/src/contrai_engine/model/game.py[109-165]
- packages/contrai-engine/src/contrai_engine/cli.py[49-70]

## Suggested fix approach
- Add an internal flag on `Game` like `_pending_redeal_same_dealer: bool`.
- In `manage_round()`, when `contract` is falsy (all-pass), set the flag and return (or redeal inline).
- In `start_new_round()`, only call `next_dealer()` when the flag is not set; clear the flag once consumed.
- Ensure `players_order` is recomputed appropriately for the dealer who is re-dealing (same as current dealer).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +432 to +456
new_hands = tuple(tuple(hand) for hand in hands)
if len(new_hands) != len(self.players):
raise ValueError(
f"Expected {len(self.players)} hands, got {len(new_hands)}."
)
for seat, hand in enumerate(new_hands):
if len(hand) != len(self.hands[seat]):
raise ValueError(
f"Seat {seat} must keep {len(self.hands[seat])} cards; "
f"replacement has {len(hand)}."
)
played = {play.card for play in self.plays}
for hand in new_hands:
for card in hand:
if card in played:
raise ValueError(
f"Replacement hand contains an already-played card: "
f"{card!r}."
)
return PlayState(
contract=self.contract,
players=self.players,
hands=new_hands,
plays=self.plays,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. With_hands allows duplicates 🐞 Bug ≡ Correctness

PlayState.with_hands() rejects already-played cards but does not enforce uniqueness across the
replacement hands, allowing the same card to exist in multiple hands and be played twice in a
determinized fork.
Agent Prompt
## Issue description
`PlayState.with_hands()` validates per-seat card counts and prevents replacement cards that already appear in `self.plays`, but it does not prevent duplicates across the provided replacement hands (or within a hand). This can create an impossible play state (a single card owned by multiple seats).

## Issue Context
`PlayState.start()` enforces that all 32 dealt cards are distinct across hands; determinization forks should preserve the same invariant to avoid invalid downstream simulations.

## Fix Focus Areas
- packages/contrai-core/src/contrai_core/play.py[113-160]
- packages/contrai-core/src/contrai_core/play.py[409-456]

## Suggested fix approach
- In `with_hands()`, flatten all replacement cards into one list.
- Reject duplicates by checking `len(set(all_replacement_cards)) == len(all_replacement_cards)`.
- Keep the existing check that no replacement card appears in `self.plays` (can be folded into the same set-based validation).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant