Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
51d626a
style(engine): type manage_round via RoundResult TypedDict
valmathieu Jun 20, 2026
be6dfa2
style(engine): type check_game_over via GameOverStatus TypedDict
valmathieu Jun 20, 2026
99c0e1e
chore(engine): drop useless comments
valmathieu Jun 21, 2026
268f204
refactor(engine): extract view design tokens into theme module
valmathieu Jun 21, 2026
1bb0d8d
refactor(engine): split out view formatting helpers
valmathieu Jun 21, 2026
4f49ecf
refactor(engine): split out human-input parsers
valmathieu Jun 21, 2026
8f60223
refactor(engine): split out auction-legality helpers
valmathieu Jun 21, 2026
2f6ce9f
refactor(engine): split out view state-reader helpers
valmathieu Jun 22, 2026
8a8a9a0
refactor(engine): extract shared view layout helpers
valmathieu Jun 23, 2026
44d9ac5
refactor(engine): extract round-recap screen rendering
valmathieu Jun 23, 2026
3b29647
refactor(engine): extract bidding screen rendering
valmathieu Jun 24, 2026
57d199d
refactor(engine): extract trick screen rendering
valmathieu Jun 24, 2026
86a4c4e
refactor(engine): extract landing screen rendering
valmathieu Jun 25, 2026
867238f
refactor(engine): extract end-game screen rendering
valmathieu Jun 26, 2026
13813a0
refactor(engine): re-export RichView from view package
valmathieu Jun 26, 2026
6db96e0
docs(engine): document the view package split
valmathieu Jun 27, 2026
18a37b6
docs(engine): redraw engine class diagram for the view package split
valmathieu Jun 27, 2026
2aa2ad2
refactor(engine): carve player.py into a player/ subpackage
valmathieu Jun 27, 2026
ba0f325
refactor(engine): extract pluggable rule-based bidding strategy
valmathieu Jun 27, 2026
45953b2
refactor(engine): extract card-play strategy + AI level registry
valmathieu Jun 28, 2026
3ac8c68
docs(engine): document pluggable AI strategy architecture
valmathieu Jun 28, 2026
c6def70
refactor(engine): carve round.py into a round/ subpackage
valmathieu Jun 28, 2026
961e1c2
refactor(engine): extract round scoring into a scoring module
valmathieu Jun 28, 2026
b704646
refactor(engine): extract card-legality rules into a legality module
valmathieu Jun 29, 2026
437e4d8
docs(engine): document the round package split
valmathieu Jun 29, 2026
c0a0e1a
test(engine): cover Game.manage_round and player ordering
valmathieu Jun 30, 2026
ada367f
refactor(engine): drop the unused RoundResult return from manage_round
valmathieu Jun 30, 2026
a469dcf
refactor(engine): model the game-over verdict as a frozen dataclass
valmathieu Jul 1, 2026
0384942
docs(engine): drop the unused GameController from diagrams and docs
valmathieu Jul 2, 2026
d7d2864
docs(engine): correct seq_trick illegal-play to the IllegalPlayError …
valmathieu Jul 3, 2026
cf84991
fix(engine): label the AI seats expert, not medium
valmathieu Jul 3, 2026
972af09
docs(engine): fix the AI difficulty label in diagrams and docs
valmathieu Jul 4, 2026
edac653
docs(engine): add the CLI game-loop sequence and scoring flowchart
valmathieu Jul 4, 2026
fc4a03c
test(engine): focus player test on the is_human property
valmathieu Jul 4, 2026
95ef5e9
docs(engine): document the choose_card defer-to-view contract
valmathieu Jul 4, 2026
0243b15
refactor(engine): skip choose_card for humans in the trick loop
valmathieu Jul 5, 2026
c86dd13
refactor(engine): make the rule-based bidding strategy Bid-native
valmathieu Jul 5, 2026
9d7c821
refactor(engine): reuse auction.last_contract_bid in the bidding stra…
valmathieu Jul 5, 2026
270e5ae
refactor(engine): render the bidding view from Bid objects
valmathieu Jul 5, 2026
2576c7b
refactor(engine): remove the wire-format bid bridge
valmathieu Jul 6, 2026
4a9f603
refactor(engine): rename _check_double_redouble to _check_double
valmathieu Jul 6, 2026
0717da6
docs(engine): update bidding docs and diagrams for the Bid-native flow
valmathieu Jul 7, 2026
00b5c1c
refactor(engine): resolve the expert AI's best contract and suit in o…
valmathieu Jul 7, 2026
82d5443
fix(engine): stop the partner-support bid escalation loop
valmathieu Jul 8, 2026
3f57b6a
test(engine): pin the partner-support ceiling behaviour
valmathieu Jul 9, 2026
2316888
feat(engine): wire AI card tracking into the round lifecycle
valmathieu Jul 10, 2026
d05943e
feat(engine): stop the trump pull when both opponents are void
valmathieu Jul 10, 2026
d6bdb92
refactor(core): extract the trick-winner rule into a current_winner f…
valmathieu Jul 11, 2026
f81384f
feat(core): add the PlayState play-phase state machine
valmathieu Jul 11, 2026
b738997
feat(core): add the PlayObservation imperfect-information view
valmathieu Jul 11, 2026
04a8435
refactor(engine): drive the trick loop from the core PlayState
valmathieu Jul 11, 2026
8d418e8
refactor(engine): hand card-play strategies a frozen observation
valmathieu Jul 12, 2026
5b1b1db
test(engine): backfill the deterministic round lifecycle test
valmathieu Jul 12, 2026
24be382
docs(core): document the PlayState play phase
valmathieu Jul 12, 2026
a7be57e
docs(engine): update the trick-flow docs for the PlayState loop
valmathieu Jul 12, 2026
3b70d1f
docs: align the observation trust-boundary wording
valmathieu Jul 13, 2026
5e4027e
test(engine): cover the bidding and trick screen renderers
valmathieu Jul 13, 2026
ee1a036
fix(core): name the acting seat in illegal-play errors
valmathieu Jul 14, 2026
28edf87
refactor(engine): move the Game score panel to the cross-screen layou…
valmathieu Jul 14, 2026
078d6b1
docs(diagrams): re-render the scoring-flow and CLI-screens PNGs at hi…
valmathieu Jul 14, 2026
a3c52d1
refactor(engine): use English domain vocabulary for the last-trick bonus
valmathieu Jul 14, 2026
0297533
refactor(engine): rename unannounced_capot to unannounced_slam
valmathieu Jul 15, 2026
c3fafa2
docs(engine): complete docstring coverage across the view module
valmathieu Jul 15, 2026
e749eb2
fix(engine): make the bid-rejection hint track the cheapest legal raise
valmathieu Jul 16, 2026
2ab5c5f
docs(engine): use double/redouble wording in view formatting
valmathieu Jul 17, 2026
3417bd3
test(engine): align view test wording with double/redouble
valmathieu Jul 17, 2026
c3729cf
docs(engine): fix Game/Round activation lifetimes in seq_round diagram
valmathieu Jul 18, 2026
d8c1d57
refactor(engine): tidy signatures, annotations and scoring comments
valmathieu Jul 18, 2026
4233552
refactor(engine): rename _PlayerStrategy mix-in to PlayerStateMixin
valmathieu Jul 18, 2026
18b47e0
chore: exempt pytest tests from pylint docstring checks
valmathieu Jul 18, 2026
d9810e6
test: fill in module, fixture and stub docstrings across test suites
valmathieu Jul 19, 2026
58ff118
docs: promote module header comments to docstrings
valmathieu Jul 19, 2026
68d6fd1
refactor(engine): split rule_based into a package and clear pylint fi…
valmathieu Jul 19, 2026
b84736c
feat(engine): preserve master cards when following a winning partner
valmathieu Jul 19, 2026
22a1788
chore: git-ignore superpowers planning documents
valmathieu Jul 20, 2026
37bd760
feat(engine): anticipate opponent ruffs in expert card play
valmathieu Jul 20, 2026
fc88c7c
fix(engine): play tiebreaker rounds when teams tie at the target
valmathieu Jul 21, 2026
50f3549
style(engine): rule off the trick-count row in the recap outcome table
valmathieu Jul 22, 2026
d7e3273
style(engine): add trump glyph to contract log line, drop trump-line …
valmathieu Jul 22, 2026
d2cfbf4
doc(domain): add the end game tie rule
valmathieu Jul 23, 2026
3f054f3
doc(domain): add unannounced capot rule
valmathieu Jul 23, 2026
ee184ff
doc(changelog); summarize long paragraphs
valmathieu Jul 24, 2026
77b26bc
docs(changelog): backfill two dropped Fixed bullets
valmathieu Jul 25, 2026
ccaec9a
chore: release v0.2.0
valmathieu Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ Thumbs.db
# commands, hooks) but NOT per-user local overrides (machine-specific paths,
# personal preferences).
.claude/settings.local.json
CLAUDE.md
CLAUDE.md

# Superpowers planning documents (brainstorm specs, implementation plans) are
# session artifacts, not repo docs - keep them local.
docs/superpowers/
docs/plans/
62 changes: 57 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,68 @@

All notable changes to the ContrAI workspace are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
All four workspace packages (`contrai-core`, `contrai-engine`, `contrai-analyzer`,
`contrai-scraper`) are versioned in lockstep — a single version covers the whole workspace.

## [Unreleased]

## [0.2.0] - 2026-07-25

Hardening release: the play phase moves into the core as an immutable `PlayState` state machine with an imperfect-information `PlayObservation` for AI strategies, the expert AI bids and plays from sounder rules, and the whole TUI took a review pass.

### Added

- (core) `PlayState` play-phase state machine, the play-side sibling of `Auction`: an
immutable, trick-by-trick state that owns the follow/trump legality rules, enforces turn order and the new `OUT_OF_TURN`/`CARD_NOT_IN_HAND` violation reasons through `apply`, and can be forked onto replacement hands via `with_hands` for future search-based AIs.
- (core) `PlayObservation` imperfect-information view: the projection of a `PlayState` that a single player is allowed to see (own hand, public trick history, legal cards), the input surface for AI card-play strategies.
- (engine) The expert AI's card play now reasons from sound card tracking: fallen cards and
per-seat voids (trump and plain suits alike) are derived from the public trick history, with
careful inference — any failure to follow records a void, but a seat discarding behind its
master partner is not read as trump-void. On that foundation the AI stops pulling trumps once
both opponents are known void in trump (remaining unseen trumps can only sit in partner's
hand), preserves master cards behind a winning partner by giving the next-highest card instead
(partner's Ace promotes its Ten to suit master — the Ten is kept for a later trick, trump-led
tricks included), and anticipates opponent ruffs — when an opponent still to play is proven
void in the led suit and may still hold trump, it stops piling points behind a winning partner
and contests a losing trick with the smallest card that beats the current best rather than the
fattest.

### Changed

- (engine) Card-play strategies now receive a single frozen `PlayObservation` — the
seat's own hand, the public trick history, and its legal cards — and derive any card
tracking from it, so a strategy is sealed off from another seat's hand by construction through what it is handed; sealing the live player refs on `Play` is a noted follow-up.
- (engine) The trick loop is now driven by the core `PlayState`: `Round` seeds an
immutable play-phase state at the start of play, derives the legal cards from it, and
mirrors the players' hands and the current trick from it each play; card-play legality
moved to `contrai-core` wholesale. An absent or illegal card now raises
`IllegalPlayError` (`CARD_NOT_IN_HAND`) instead of being silently skipped.
- (engine) Unify the internal bid representation on core `Bid`/`Auction` objects and
remove the legacy wire-format bridge — the rule-based AI and the bidding view now operate directly on typed `Bid` objects. Behaviour is unchanged.
- (engine) The expert AI now resolves its best (contract, suit) pair once per bidding
turn — the duplicated best-contract scans in the overbid check and the initial-bid
builder are folded into a single helper that also owns the suit tie-break.

### Removed

- (engine) The `initialize_card_tracking` / `update_card_tracking` hooks on
`CardPlayStrategy` / `AiPlayer` (public since 0.1.0) — card tracking is now derived
from the observation rather than pushed to per-strategy state each play.

### Fixed

- (engine) Expert AI partner-support escalation: every supporting turn re-added the seat's full complement (+10 per external ace, +10 for the trump complement) on top of the *standing* contract, so partners alternately raised each other far past their combined strength (an 80 opening ratcheting to 160+, at which point the opponents' double heuristic armed itself on the inflated value), and a seat could even "support" its own bid after an opponent overbid. Support is now capped at a team ceiling — partner's opening bid plus the supporter's complement, announced once — and a seat never supports a suit it opened itself.
- (engine) Landing screen now labels the three AI seats `AI · expert` instead of `AI · medium` — the bots play the expert strategy, which is the only level wired today.
- (engine) Expert AI suit tie-break: with several suits tied for the best contract, the
AI always fell back to Spades — even when Spades never met the bidding table — and ignored the belote preference when more than one tied suit held a belote. It now picks among the tied suits only, preferring belote holders.
- (engine) The "Unrecognized bid" notice now suggests the cheapest raise the auction still allows (e.g. `'100 h'` once 90 stands) instead of a fixed `'80 h'`, dropping the numeric example entirely once only Slam-family raises remain.
- (engine) A game no longer ends without a winner when both teams finish a round level at or above the target score: the tie is sudden death — tiebreaker rounds are dealt until one team leads, the round recap announcing each one — so the game-over banner always names a winning team.
- (core) Illegal-play errors now name the acting seat: `PlayState.apply` attaches a `<position> card play` context to every `IllegalPlayError`, so rejection diagnostics say who misplayed as well as which card and why.

## [0.1.0] - 2026-06-21

First playable release: a complete CLI Contrée engine backed by a shared domain model,
plus the standalone hand analyzer and the spectator-mode scraper.
First playable release: a complete CLI Contrée engine backed by a shared domain model, plus the standalone hand analyzer and the spectator-mode scraper.

### Added

Expand All @@ -32,5 +83,6 @@ plus the standalone hand analyzer and the spectator-mode scraper.
Online → Spectator → Contree → Tournament navigation, seat identification, and `#tour`
round polling.

[Unreleased]: https://github.com/valmathieu/ContrAI/compare/v0.1.0...HEAD
[Unreleased]: https://github.com/valmathieu/ContrAI/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/valmathieu/ContrAI/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/valmathieu/ContrAI/releases/tag/v0.1.0
28 changes: 27 additions & 1 deletion contree-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,28 @@ Slam grid below:
always credited to the team **holding** K + Q of trump (not whoever captures
those cards in a trick — see §6.5), on top of everything else, win or lose.

#### Unannounced capot

If the declaring team wins **all 8 tricks** on a numeric contract *without
having bid a Slam*, the trick pile (152 cards + 10 *dix de der* = 162) is
replaced by a flat **250** substitute: the declarer scores `C + 250`, the
defense scores nothing, and the contract is necessarily **made** (sweeping
every trick cannot fail). This mirrors the announced-Slam shape but keeps the
numeric contract value `C` as the base.

- Whether the contracting player took all 8 tricks **personally** (a *grand
slam*) or the team split them (a plain *slam*), the substitute is the same
250 — the distinction is only descriptive.
- **Un-doubled only.** A doubled / redoubled sweep keeps the winner-takes-all
`160 + C × M` shape above; the 250 substitute does **not** apply.
- **Declaring team only.** If the *defense* takes all 8 tricks the declarer
has simply *chuté* — score it as an ordinary failed contract (`160 + C`),
not as a capot.
- **Belote (+20)** still layers on top for the holding team, as everywhere.

> Worked example: contract `100 ♠`, declarer sweeps all 8 → declarer
> 350 (`100 + 250`), defense 0.

#### Slam and Solo Slam

Slam-family contracts keep the same shape as numeric contracts — the at-risk
Expand Down Expand Up @@ -342,6 +364,9 @@ defense.
- A target score is agreed before the game (typical: **1500** or **2000**).
- The first team to reach or exceed the target at the end of a round wins.
- If both teams cross the target in the same round, the higher score wins.
- If both teams sit at the **same score** at or above the target, nobody has
won yet: play continues with additional rounds (sudden death) until one
team leads.

---

Expand Down Expand Up @@ -487,7 +512,8 @@ threshold; details live alongside the AI implementation.*
[Scoring] → sum cards + dix de der (+ belote if applicable)
apply contract success/failure + multiplier
[Check] → if any team ≥ target (1500/2000): end game
[Check] → if one team strictly leads at ≥ target (1500/2000): end game
tie at ≥ target: sudden death, keep playing until one team leads
else next round, dealer rotates right
```

Expand Down
6 changes: 4 additions & 2 deletions docs/ai-ladder/rule_based.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Rule-based AI

Hand-coded expert strategies. Currently implemented as `AiPlayer` in `contrai-engine` (expert bidding table + card-play heuristics; specs SF-09, SF-10).
Hand-coded expert strategies (expert bidding table + card-play heuristics; specs SF-09, SF-10). They are the **first concrete rung** of the AI ladder, implemented in `contrai-engine` as the `RuleBasedBiddingStrategy` / `RuleBasedCardPlayStrategy` pair — concrete implementations of the `BiddingStrategy` / `CardPlayStrategy` interfaces. They are injected into `AiPlayer` (the default strategies) and registered as `AI_LEVELS["expert"]`, so a future MCTS or learned level is a new strategy class rather than an edit to `AiPlayer`. See the [engine docs](../engine/index.md#ai-players) for the injection seam.

**Explainability:** the rule trace is the rationale — log which rule fired for each bid/play.
`RuleBasedCardPlayStrategy` is stateless between calls: every decision is a pure function of the single frozen `PlayObservation` it is handed for that turn. Whatever card tracking the heuristics need — which cards have fallen, which seats are known void in trump — is derived fresh each turn by replaying the observation's public trick history, never carried across calls or rounds. Because the observation is the only input a strategy ever receives, it is sealed off from another seat's hand by construction — through what it is handed; sealing the live player refs on `Play` is a noted follow-up.

**Explainability:** the rule trace is the rationale — log which rule fired for each bid/play. The strategy object is the natural home for that trace (AI roadmap §6.1).

> TODO: rule catalogue; planned extensions (deeper card counting, partner inference, signal-based bidding).
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Overview of how the four ContrAI packages fit together.
The repository is a [uv workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/) with four members under `packages/`:

- **`contrai-core`** — shared domain model. Owns `Suit`/`Rank`/`CARD_SUITS`, `Card`, `Deck`, `Hand`, `Team`, `BasePlayer`, the frozen `Bid` sum type, the `Auction` state-and-rule oracle, `Contract`, `Trick`, and the model-level exceptions (a `ContraiError` base, plus `IllegalBidError` / `IllegalPlayError` and friends). Pure data and invariants, no orchestration.
- **`contrai-engine`** — game engine on top of `contrai-core`. Extends `BasePlayer` with `Player` / `HumanPlayer` / `AiPlayer`, owns `Game` and `Round` orchestration, and ships the Rich-based `contrai` terminal UI (`view/rich_view.py` + `cli.py`). See [Engine — CLI](engine/index.md#cli).
- **`contrai-engine`** — game engine on top of `contrai-core`. Extends `BasePlayer` with `Player` / `HumanPlayer` / `AiPlayer`, owns `Game` and `Round` orchestration, and ships the Rich-based `contrai` terminal UI (the `view/` package — `RichView` orchestrator plus per-screen builders — wired in `cli.py`). See [Engine — CLI](engine/index.md#cli).
- **`contrai-analyzer`** — Streamlit dashboard for opening-hand strength (hypergeometric distribution + bidding truth-table). Deliberately independent of `contrai-core`; see [`analyzer/index.md`](analyzer/index.md) for the rationale behind the `SuitSlot` abstraction.
- **`contrai-scraper`** — Playwright spectator-mode scraper for online Coinche games. v1 ships login + table navigation + per-round polling; bidding/play observation and persistence are still to be wired up.

Expand Down
Loading
Loading