Skip to content

Commit 7c9624d

Browse files
committed
feat(engine): G9 accumulated multi-round scoring — Hearts plays to 100 points. Add 4 new builtins (get_cumulative_score, max_cumulative_score, min_cumulative_score, accumulate_scores), 1 new interpreter effect (accumulate_scores), modify reset_round to preserve cumulative_score_* variables, update Hearts ruleset v2.0.0 with game_over phase and conditional round/end transitions, and expand §11 Scoring + §17 Trick-Taking documentation. 28 new tests (15 builtin unit + 13 multi-round integration). 1031 tests green (922 shared + 15 schema + 94 host).
1 parent 6303845 commit 7c9624d

6 files changed

Lines changed: 1136 additions & 18 deletions

File tree

docs/ruleset-authoring.md

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,47 @@ the `computeHandValue()` function, which starts all dual-value cards (Aces) at
844844
their high value and downgrades them one at a time until the total is at or
845845
below the target.
846846

847+
### Multi-Round Scoring
848+
849+
For games played over multiple rounds (like Hearts), the engine supports
850+
accumulated scoring through cumulative score variables:
851+
852+
| Builtin | Args | Returns | Description |
853+
|---------|------|---------|-------------|
854+
| `accumulate_scores()` | 0 | effect | Adds each player's `player_score:{i}` to `variables["cumulative_score_{i}"]` |
855+
| `get_cumulative_score(i)` | 1 | number | Returns `variables["cumulative_score_{i}"]`, defaults to 0 |
856+
| `max_cumulative_score()` | 0 | number | Highest cumulative score across all human players |
857+
| `min_cumulative_score()` | 0 | number | Lowest cumulative score across all human players |
858+
859+
**Pattern**: After `calculate_scores()` computes round scores, call
860+
`accumulate_scores()` to persist them. Use `max_cumulative_score()` in
861+
transition conditions to determine whether the game should end or continue.
862+
863+
```json
864+
{
865+
"name": "scoring",
866+
"kind": "automatic",
867+
"automaticSequence": [
868+
"calculate_scores()",
869+
"accumulate_scores()"
870+
],
871+
"transitions": [
872+
{ "to": "game_over", "when": "max_cumulative_score() >= 100" },
873+
{ "to": "round_end", "when": "max_cumulative_score() < 100" }
874+
]
875+
}
876+
```
877+
878+
The `reset_round()` effect automatically preserves all `cumulative_score_*`
879+
variables while resetting everything else to `initialVariables`.
880+
881+
For final winner determination with cumulative scores, reference the cumulative
882+
builtins directly in the `winCondition`:
883+
884+
```json
885+
"winCondition": "get_cumulative_score(current_player_index) == min_cumulative_score()"
886+
```
887+
847888
---
848889

849890
## 12. Visibility
@@ -1475,37 +1516,80 @@ wins the trick instead of the highest led-suit card.
14751516

14761517
### Scoring
14771518

1478-
For avoidance games like Hearts, score penalty points:
1519+
For avoidance games like Hearts, score penalty points per round:
14791520

14801521
```json
14811522
"scoring": {
14821523
"method": "count_cards_by_suit(concat(\"won:\", current_player_index), \"hearts\") + if(has_card_with(concat(\"won:\", current_player_index), \"Q\", \"spades\"), 13, 0)",
1483-
"winCondition": "my_score == 0"
1524+
"winCondition": "get_cumulative_score(current_player_index) == min_cumulative_score()"
14841525
}
14851526
```
14861527

1528+
The `method` computes each round's penalty (hearts taken + 13 for Queen of
1529+
Spades). The `winCondition` uses cumulative scores so the player with the lowest
1530+
total penalty wins at game end.
1531+
14871532
For point-based games like Spades, score positively based on tricks won.
14881533

14891534
### Ending the Game
14901535

1491-
Call `end_game()` in the scoring phase to properly terminate the game:
1536+
For multi-round games, separate scoring from game termination. The scoring phase
1537+
accumulates round scores and branches based on whether the target has been
1538+
reached:
14921539

14931540
```json
14941541
{
14951542
"name": "scoring",
14961543
"kind": "automatic",
1497-
"automaticSequence": [
1498-
"calculate_scores()",
1499-
"determine_winners()",
1500-
"end_game()"
1544+
"automaticSequence": ["calculate_scores()", "accumulate_scores()"],
1545+
"transitions": [
1546+
{ "to": "game_over", "when": "max_cumulative_score() >= 100" },
1547+
{ "to": "round_end", "when": "max_cumulative_score() < 100" }
15011548
]
15021549
}
15031550
```
15041551

1552+
A dedicated `game_over` phase handles final winner determination:
1553+
1554+
```json
1555+
{
1556+
"name": "game_over",
1557+
"kind": "automatic",
1558+
"automaticSequence": ["determine_winners()", "end_game()"],
1559+
"transitions": []
1560+
}
1561+
```
1562+
1563+
The `round_end` phase lets players start the next round. The `reset_round()`
1564+
effect clears round state but preserves `cumulative_score_*` variables:
1565+
1566+
```json
1567+
{
1568+
"name": "round_end",
1569+
"kind": "all_players",
1570+
"actions": [{
1571+
"name": "play_again",
1572+
"label": "Play Again",
1573+
"effect": ["collect_all_to(draw_pile)", "reset_round()"]
1574+
}],
1575+
"transitions": [{ "to": "setup", "when": "continue_game" }]
1576+
}
1577+
```
1578+
15051579
The `end_game()` builtin transitions the game status to `finished` and derives
15061580
the winner from `scores[result:N]`. The `ResultScreen` on the client
15071581
automatically displays when the game ends.
15081582

1583+
For single-round games, combine everything in one scoring phase:
1584+
1585+
```json
1586+
{
1587+
"name": "scoring",
1588+
"kind": "automatic",
1589+
"automaticSequence": ["calculate_scores()", "determine_winners()", "end_game()"]
1590+
}
1591+
```
1592+
15091593
### Limitations
15101594

15111595
- **Follow-suit enforcement**: The engine does not yet validate individual card
@@ -1514,11 +1598,10 @@ automatically displays when the game ends.
15141598
the UI level or via future per-card conditions.
15151599
- **Card passing**: Pre-game card passing (e.g., Hearts pass phase) is not yet
15161600
supported. Requires a new action type for selecting multiple cards to pass.
1517-
- **Multi-round scoring**: Accumulated scoring across rounds (e.g., Hearts to
1518-
100) is not yet supported (G9). Currently each game is a single round.
15191601

15201602
### Complete Hearts Example
15211603

15221604
See [`rulesets/hearts.cardgame.json`](../rulesets/hearts.cardgame.json) for a
15231605
full working implementation. Hearts exercises: 4-player trick-taking, penalty
1524-
scoring, hearts-broken tracking, and the `end_game()` lifecycle.
1606+
scoring, hearts-broken tracking, multi-round accumulated scoring to 100 points,
1607+
and the `game_over``end_game()` lifecycle.

0 commit comments

Comments
 (0)