bga-dev-skill 0.1.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/CC_PLAN.md +781 -0
- package/README.md +59 -0
- package/SKILL.md +117 -0
- package/composer.json +13 -0
- package/harness/example/SampleGame.php +160 -0
- package/harness/example/SampleGameTest.php +104 -0
- package/harness/example/sampleUtils.test.js +53 -0
- package/harness/js/bgaStubs.js +32 -0
- package/harness/js/testHelpers.js +9 -0
- package/harness/php/BgaDatabaseFake.php +234 -0
- package/harness/php/BgaExceptionTypes.php +17 -0
- package/harness/php/BgaGameTestCase.php +160 -0
- package/harness/php/BgaNotificationSpy.php +116 -0
- package/harness/php/BgaStubs.php +244 -0
- package/jest.config.js +5 -0
- package/package.json +11 -0
- package/skills/database-patterns.md +143 -0
- package/skills/js-dojo-patterns.md +89 -0
- package/skills/notifications.md +321 -0
- package/skills/scaffold-templates.md +82 -0
- package/skills/state-machine.md +126 -0
- package/src/setgame.utils.js +47 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# BGA Notification Patterns
|
|
2
|
+
|
|
3
|
+
This skill is derived from real code in the rage project. Every example is
|
|
4
|
+
pulled directly from working PHP and JS — not documentation speculation.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## The Contract That Must Match Exactly
|
|
9
|
+
|
|
10
|
+
A notification is a named data packet PHP sends to all clients (or one client).
|
|
11
|
+
The name on the PHP side and the handler method name on the JS side must match
|
|
12
|
+
by a specific convention. A mismatch produces no error — the notification is
|
|
13
|
+
silently dropped and your UI does not update.
|
|
14
|
+
|
|
15
|
+
### PHP side — modern framework
|
|
16
|
+
|
|
17
|
+
```php
|
|
18
|
+
// $this->notify->all(type, message, data)
|
|
19
|
+
$this->notify->all("trump_change", clienttranslate('Trump is now ${trump_html}'), [
|
|
20
|
+
'trump' => $trump, // the card object
|
|
21
|
+
'trump_html' => $html, // pre-rendered HTML for the log message
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
// $this->notify->player(playerId, type, message, data)
|
|
25
|
+
$this->notify->player($playerId, "dealtCards", '', [
|
|
26
|
+
'cards' => $cards,
|
|
27
|
+
]);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### JS side — modern framework (ES module)
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// Method name = "notif_" + type — must match exactly, including case
|
|
34
|
+
async notif_trump_change(args) {
|
|
35
|
+
this.gamedatas.trump = args.trump; // args.trump, not args.card
|
|
36
|
+
this.renderTrump(args.trump);
|
|
37
|
+
// args.trump_html exists but is only used in the log — JS doesn't need it
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async notif_dealtCards(args) {
|
|
41
|
+
this.gamedatas.hand = args.cards; // args.cards, matches PHP 'cards' key
|
|
42
|
+
this.renderHand();
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### The naming rules
|
|
47
|
+
|
|
48
|
+
| PHP type string | JS handler name |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `"trump_change"` | `notif_trump_change` |
|
|
51
|
+
| `"dealtCards"` | `notif_dealtCards` |
|
|
52
|
+
| `"actionBid"` | `notif_actionBid` |
|
|
53
|
+
| `"winningCardNotif"` | `notif_winningCardNotif` |
|
|
54
|
+
| `"scoringNotif"` | `notif_scoringNotif` |
|
|
55
|
+
|
|
56
|
+
The prefix is always `notif_`. After that, the string is copied verbatim —
|
|
57
|
+
`trump_change` stays `trump_change`, not `trumpChange`.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## The Casing Gotcha (Most Common Silent Bug)
|
|
62
|
+
|
|
63
|
+
PHP array keys are sent as-is to JS. `notif.args` keys must match what PHP
|
|
64
|
+
put in the data array — character for character.
|
|
65
|
+
|
|
66
|
+
```php
|
|
67
|
+
// PHP sends snake_case
|
|
68
|
+
$this->notify->all("winningCardNotif", '...', [
|
|
69
|
+
'player_id' => $pid, // snake_case
|
|
70
|
+
'plus5' => $plus5, // no underscore
|
|
71
|
+
'minus5' => $minus5,
|
|
72
|
+
]);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// JS must read the same keys
|
|
77
|
+
async notif_winningCardNotif(args) {
|
|
78
|
+
const pid = args.player_id; // ✅ snake_case matches
|
|
79
|
+
this.gamedatas.plus5 = args.plus5;
|
|
80
|
+
this.gamedatas.minus5 = args.minus5;
|
|
81
|
+
// args.playerId would silently be undefined
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Never camelCase a key on the JS side that PHP sent as snake_case. There is no
|
|
86
|
+
error. The value will be undefined and your handler will silently misrender.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Data Shapes from rage (Reference)
|
|
91
|
+
|
|
92
|
+
These are the exact payloads for each notification type in rage.
|
|
93
|
+
When generating tests or writing new notifications, use these as the canonical
|
|
94
|
+
shape.
|
|
95
|
+
|
|
96
|
+
### `trump_change`
|
|
97
|
+
```php
|
|
98
|
+
// PHP
|
|
99
|
+
$this->notify->all("trump_change", clienttranslate('Trump is now ${trump_html}'), [
|
|
100
|
+
'trump' => $trump, // full card row from DB: {id, type, type_arg, location, location_arg}
|
|
101
|
+
'trump_html' => $html, // pre-rendered HTML string for log only
|
|
102
|
+
]);
|
|
103
|
+
```
|
|
104
|
+
```javascript
|
|
105
|
+
// JS — only trump is used; trump_html is consumed by the log renderer automatically
|
|
106
|
+
async notif_trump_change(args) {
|
|
107
|
+
this.gamedatas.trump = args.trump;
|
|
108
|
+
this.renderTrump(args.trump);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `dealtCards`
|
|
113
|
+
```php
|
|
114
|
+
// PHP — sent only to the receiving player (notify->player, not ->all)
|
|
115
|
+
$this->notify->player($playerId, "dealtCards", '', [
|
|
116
|
+
'cards' => $this->cards->getPlayerHand($playerId),
|
|
117
|
+
]);
|
|
118
|
+
```
|
|
119
|
+
```javascript
|
|
120
|
+
async notif_dealtCards(args) {
|
|
121
|
+
this.gamedatas.hand = args.cards; // keyed by card id
|
|
122
|
+
this.renderHand();
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `actionBid`
|
|
127
|
+
```php
|
|
128
|
+
$this->notify->all("actionBid", clienttranslate('${player_name} bids ${bid}'), [
|
|
129
|
+
'player_id' => $playerId,
|
|
130
|
+
'player_name' => $this->getPlayerNameById($playerId),
|
|
131
|
+
'bid' => $bid,
|
|
132
|
+
]);
|
|
133
|
+
```
|
|
134
|
+
```javascript
|
|
135
|
+
async notif_actionBid(args) {
|
|
136
|
+
const player_id = args.player_id;
|
|
137
|
+
this.gamedatas.players[player_id].bid = args.bid;
|
|
138
|
+
document.getElementById('tricks_' + player_id).innerText = args.bid;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### `actionPlayCard`
|
|
143
|
+
```php
|
|
144
|
+
// card has an extra 'color' key when a joker was played
|
|
145
|
+
$this->notify->all("actionPlayCard", clienttranslate('${player_name} plays ${card_html}'), [
|
|
146
|
+
'player_id' => $playerId,
|
|
147
|
+
'player_name' => $this->getPlayerNameById($playerId),
|
|
148
|
+
'card' => $card, // full card object, including location_arg = player_id
|
|
149
|
+
'card_html' => $cardHtml,
|
|
150
|
+
]);
|
|
151
|
+
```
|
|
152
|
+
```javascript
|
|
153
|
+
async notif_actionPlayCard(args) {
|
|
154
|
+
const card = args.card; // args.card, not args.card_id
|
|
155
|
+
// card.location_arg is the player_id who played it
|
|
156
|
+
if (card.type === 'joker') {
|
|
157
|
+
this.gamedatas.players[card.location_arg].joker_color = card.color;
|
|
158
|
+
}
|
|
159
|
+
this.gamedatas.table.push(card);
|
|
160
|
+
this.addCardTo(document.getElementById('table_cards'), card, true);
|
|
161
|
+
await this.bga.gameui.wait(600);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `winningCardNotif`
|
|
166
|
+
```php
|
|
167
|
+
$this->notify->all("winningCardNotif", clienttranslate('${player_name} wins the trick'), [
|
|
168
|
+
'player_id' => $winnerId,
|
|
169
|
+
'player_name' => $this->getPlayerNameById($winnerId),
|
|
170
|
+
'plus5' => $plus5CountByPlayer, // array keyed by player_id
|
|
171
|
+
'minus5' => $minus5CountByPlayer,
|
|
172
|
+
]);
|
|
173
|
+
```
|
|
174
|
+
```javascript
|
|
175
|
+
async notif_winningCardNotif(args) {
|
|
176
|
+
const pid = args.player_id;
|
|
177
|
+
this.gamedatas.plus5 = args.plus5;
|
|
178
|
+
this.gamedatas.minus5 = args.minus5;
|
|
179
|
+
await this.bga.gameui.wait(2000);
|
|
180
|
+
// clear table, increment tricks_taken, update UI
|
|
181
|
+
this.gamedatas.players[pid].tricks_taken++;
|
|
182
|
+
this.updateTricks(pid);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `revealBids`
|
|
187
|
+
```php
|
|
188
|
+
// Only sent for hidden-bidding variants
|
|
189
|
+
$this->notify->all("revealBids", clienttranslate('Revealed bids: ${html}'), [
|
|
190
|
+
'bids' => $bids, // {player_id: bid_amount, ...}
|
|
191
|
+
'html' => $html, // rendered string for log
|
|
192
|
+
]);
|
|
193
|
+
```
|
|
194
|
+
```javascript
|
|
195
|
+
async notif_revealBids(args) {
|
|
196
|
+
for (const player_id in args.bids) {
|
|
197
|
+
this.gamedatas.players[player_id].bid = args.bids[player_id];
|
|
198
|
+
this.updateTricks(player_id);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Notification Setup — Modern Framework
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
setupNotifications() {
|
|
209
|
+
// One-liner in the modern framework — registers all notif_* methods automatically
|
|
210
|
+
this.bga.notifications.setupPromiseNotifications();
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This replaces the old Dojo pattern of manually calling
|
|
215
|
+
`this.notifqueue.subscribe()` for each type. All `async notif_*` methods on
|
|
216
|
+
the Game class are registered automatically.
|
|
217
|
+
|
|
218
|
+
**Do not** call `setSynchronous()` — the modern promise-based notifications
|
|
219
|
+
handle sequencing via `await`.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Pre-rendered HTML in Notification Messages
|
|
224
|
+
|
|
225
|
+
PHP frequently builds an HTML string to embed in the log message:
|
|
226
|
+
|
|
227
|
+
```php
|
|
228
|
+
public function getCardHtml($card): string
|
|
229
|
+
{
|
|
230
|
+
$special = isset($this->special_cards[$card["type"]]) ? "special" : "";
|
|
231
|
+
$val = $special
|
|
232
|
+
? $this->special_cards[$card["type"]]["short"]
|
|
233
|
+
: $card["type_arg"];
|
|
234
|
+
$color = isset($card["color"]) ? $card["color"] : "";
|
|
235
|
+
return "<span class=\"card_type {$card['type']} {$color} {$special}\">{$val}</span>";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Used in a notification message template:
|
|
239
|
+
$this->notify->all("trump_change", clienttranslate('Trump is now ${trump_html}'), [
|
|
240
|
+
'trump' => $trump,
|
|
241
|
+
'trump_html' => $this->getCardHtml($trump),
|
|
242
|
+
]);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The `${trump_html}` in the message template is substituted by BGA's log
|
|
246
|
+
renderer using the `trump_html` value from the data array. The JS handler does
|
|
247
|
+
not need to use `args.trump_html` at all — it reads `args.trump` (the card
|
|
248
|
+
object) to update the UI.
|
|
249
|
+
|
|
250
|
+
**Pattern:** always name pre-rendered HTML keys with a `_html` suffix so it's
|
|
251
|
+
clear which keys are for the log vs which are for UI updates.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## What notify->all vs notify->player Sends
|
|
256
|
+
|
|
257
|
+
```php
|
|
258
|
+
// All players receive this (including spectators)
|
|
259
|
+
$this->notify->all("trump_change", $msg, $data);
|
|
260
|
+
|
|
261
|
+
// Only this specific player receives it (used for private hand info)
|
|
262
|
+
$this->notify->player($playerId, "dealtCards", '', $data);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
When a player reloads, `getAllDatas()` rebuilds their state. The
|
|
266
|
+
`notify->player` call is for real-time updates during play only. If a player
|
|
267
|
+
disconnects and reconnects, they get `getAllDatas()` — not replayed
|
|
268
|
+
notifications.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Common Bugs
|
|
273
|
+
|
|
274
|
+
**Bug 1: JS key doesn't match PHP key**
|
|
275
|
+
```php
|
|
276
|
+
$this->notify->all("bid", '...', ['player_id' => $id, 'bid_amount' => $bid]);
|
|
277
|
+
```
|
|
278
|
+
```javascript
|
|
279
|
+
async notif_bid(args) {
|
|
280
|
+
const bid = args.bid; // ❌ undefined — PHP key is 'bid_amount'
|
|
281
|
+
const bid = args.bid_amount; // ✅
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Bug 2: Forgetting notify->player for private data**
|
|
286
|
+
```php
|
|
287
|
+
// ❌ Sends all players' hands to everyone
|
|
288
|
+
$this->notify->all("dealtCards", '', ['cards' => $allCards]);
|
|
289
|
+
|
|
290
|
+
// ✅ Each player only sees their own hand
|
|
291
|
+
foreach ($players as $playerId => $player) {
|
|
292
|
+
$this->notify->player($playerId, "dealtCards", '', [
|
|
293
|
+
'cards' => $this->cards->getPlayerHand($playerId),
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Bug 3: Using old notify API in modern framework**
|
|
299
|
+
```php
|
|
300
|
+
// ❌ Old API — works but inconsistent with modern codebase
|
|
301
|
+
$this->notifyAllPlayers("trump_change", $msg, $data);
|
|
302
|
+
$this->notifyPlayer($playerId, "dealtCards", '', $data);
|
|
303
|
+
|
|
304
|
+
// ✅ Modern API
|
|
305
|
+
$this->notify->all("trump_change", $msg, $data);
|
|
306
|
+
$this->notify->player($playerId, "dealtCards", '', $data);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Bug 4: Synchronous notification handler when awaiting animation**
|
|
310
|
+
```javascript
|
|
311
|
+
// ❌ Returns before animation completes — next notification fires immediately
|
|
312
|
+
notif_winningCardNotif(args) {
|
|
313
|
+
setTimeout(() => { /* clear table */ }, 2000);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ✅ Promise-based — next notification waits for this to resolve
|
|
317
|
+
async notif_winningCardNotif(args) {
|
|
318
|
+
await this.bga.gameui.wait(2000);
|
|
319
|
+
document.getElementById('table_cards').innerHTML = '';
|
|
320
|
+
}
|
|
321
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Scaffold Templates (Modern Framework)
|
|
2
|
+
|
|
3
|
+
Generate modern-framework structure first. Do not scaffold legacy layout by default.
|
|
4
|
+
|
|
5
|
+
Legacy framework exists, but its file shape differs; consult official BGA docs when targeting legacy projects.
|
|
6
|
+
|
|
7
|
+
## Required Structure
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
modules/
|
|
11
|
+
php/
|
|
12
|
+
Game.php
|
|
13
|
+
States/
|
|
14
|
+
<StateName>.php
|
|
15
|
+
Managers/
|
|
16
|
+
Models/
|
|
17
|
+
js/
|
|
18
|
+
Game.js
|
|
19
|
+
states/
|
|
20
|
+
<StateName>.js
|
|
21
|
+
templates/
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Required Files and Responsibilities
|
|
25
|
+
|
|
26
|
+
- `modules/php/Game.php`
|
|
27
|
+
- Main game class
|
|
28
|
+
- wiring of managers/models/states
|
|
29
|
+
- high-level actions and state entry points
|
|
30
|
+
- `modules/php/States/*.php`
|
|
31
|
+
- one class per complex state
|
|
32
|
+
- state-specific action validation and transitions
|
|
33
|
+
- `modules/php/Managers/*.php`
|
|
34
|
+
- data access and domain operations
|
|
35
|
+
- `modules/php/Models/*.php`
|
|
36
|
+
- value objects/DTO-style helpers
|
|
37
|
+
- `modules/js/Game.js`
|
|
38
|
+
- export class Game
|
|
39
|
+
- state registration
|
|
40
|
+
- notification setup
|
|
41
|
+
- UI orchestration
|
|
42
|
+
- `modules/js/states/*.js`
|
|
43
|
+
- frontend behavior per game state
|
|
44
|
+
- `onEnteringState`, `onLeavingState`, `onPlayerActivationChange`
|
|
45
|
+
|
|
46
|
+
## Modern Game.js Shape
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
export class Game {
|
|
50
|
+
constructor(bga) {
|
|
51
|
+
this.bga = bga;
|
|
52
|
+
// register state handlers
|
|
53
|
+
// bga.states.register('stateName', new SomeState(this, bga));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setup(gamedatas) {
|
|
57
|
+
this.gamedatas = gamedatas;
|
|
58
|
+
this.bga.notifications.setupPromiseNotifications();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## PHP Action Flow Template
|
|
64
|
+
|
|
65
|
+
Use this order in server action methods:
|
|
66
|
+
|
|
67
|
+
1. `checkAction(...)`
|
|
68
|
+
2. validate arguments
|
|
69
|
+
3. read DB state
|
|
70
|
+
4. apply domain rule
|
|
71
|
+
5. write DB
|
|
72
|
+
6. notify
|
|
73
|
+
7. transition
|
|
74
|
+
|
|
75
|
+
## Test Scaffold Requirement
|
|
76
|
+
|
|
77
|
+
For each gameplay action, create corresponding PHPUnit tests using `BgaGameTestCase` fluent style:
|
|
78
|
+
|
|
79
|
+
- happy path
|
|
80
|
+
- invalid input path
|
|
81
|
+
- wrong-player path
|
|
82
|
+
- transition path
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# State Machine Patterns
|
|
2
|
+
|
|
3
|
+
Write state machine code so transitions are explicit, action permissions are enforced, and game states never stall.
|
|
4
|
+
|
|
5
|
+
## Required State Types
|
|
6
|
+
|
|
7
|
+
Use only the four supported state types:
|
|
8
|
+
|
|
9
|
+
- `manager`
|
|
10
|
+
- `activeplayer`
|
|
11
|
+
- `multipleactiveplayer`
|
|
12
|
+
- `game`
|
|
13
|
+
|
|
14
|
+
## 12-State Example (Auction Cycle + Final Buying)
|
|
15
|
+
|
|
16
|
+
Use this as a structural template:
|
|
17
|
+
|
|
18
|
+
```php
|
|
19
|
+
$machinestates = [
|
|
20
|
+
1 => [
|
|
21
|
+
'name' => 'gameSetup',
|
|
22
|
+
'type' => 'manager',
|
|
23
|
+
'action' => 'stGameSetup',
|
|
24
|
+
'transitions' => ['next' => 10],
|
|
25
|
+
],
|
|
26
|
+
|
|
27
|
+
10 => [
|
|
28
|
+
'name' => 'roundStart',
|
|
29
|
+
'type' => 'game',
|
|
30
|
+
'action' => 'stRoundStart',
|
|
31
|
+
'transitions' => ['toAuction' => 20],
|
|
32
|
+
],
|
|
33
|
+
20 => [
|
|
34
|
+
'name' => 'auctionBid',
|
|
35
|
+
'type' => 'activeplayer',
|
|
36
|
+
'description' => clienttranslate('${actplayer} must bid or pass'),
|
|
37
|
+
'possibleactions' => ['actBid', 'actPassBid'],
|
|
38
|
+
'transitions' => ['nextBidder' => 21, 'auctionClosed' => 30],
|
|
39
|
+
],
|
|
40
|
+
21 => [
|
|
41
|
+
'name' => 'auctionAdvance',
|
|
42
|
+
'type' => 'game',
|
|
43
|
+
'action' => 'stAuctionAdvance',
|
|
44
|
+
'transitions' => ['continueAuction' => 20, 'auctionClosed' => 30],
|
|
45
|
+
],
|
|
46
|
+
|
|
47
|
+
30 => [
|
|
48
|
+
'name' => 'resolveAuction',
|
|
49
|
+
'type' => 'game',
|
|
50
|
+
'action' => 'stResolveAuction',
|
|
51
|
+
'transitions' => ['toFinalBuying' => 40],
|
|
52
|
+
],
|
|
53
|
+
40 => [
|
|
54
|
+
'name' => 'finalBuying',
|
|
55
|
+
'type' => 'multipleactiveplayer',
|
|
56
|
+
'description' => clienttranslate('All players select final purchases'),
|
|
57
|
+
'possibleactions' => ['actFinalBuy'],
|
|
58
|
+
'transitions' => ['allBought' => 50],
|
|
59
|
+
],
|
|
60
|
+
|
|
61
|
+
50 => [
|
|
62
|
+
'name' => 'finalBuyingResolve',
|
|
63
|
+
'type' => 'game',
|
|
64
|
+
'action' => 'stFinalBuyingResolve',
|
|
65
|
+
'transitions' => ['toAction' => 60],
|
|
66
|
+
],
|
|
67
|
+
60 => [
|
|
68
|
+
'name' => 'playerAction',
|
|
69
|
+
'type' => 'activeplayer',
|
|
70
|
+
'description' => clienttranslate('${actplayer} must play'),
|
|
71
|
+
'possibleactions' => ['actPlay', 'actPass'],
|
|
72
|
+
'transitions' => ['nextPlayer' => 61, 'endRound' => 70],
|
|
73
|
+
],
|
|
74
|
+
61 => [
|
|
75
|
+
'name' => 'advancePlayer',
|
|
76
|
+
'type' => 'game',
|
|
77
|
+
'action' => 'stAdvancePlayer',
|
|
78
|
+
'transitions' => ['continueRound' => 60, 'endRound' => 70],
|
|
79
|
+
],
|
|
80
|
+
|
|
81
|
+
70 => [
|
|
82
|
+
'name' => 'scoreRound',
|
|
83
|
+
'type' => 'game',
|
|
84
|
+
'action' => 'stScoreRound',
|
|
85
|
+
'transitions' => ['nextRound' => 80, 'endGame' => 99],
|
|
86
|
+
],
|
|
87
|
+
80 => [
|
|
88
|
+
'name' => 'prepareNextRound',
|
|
89
|
+
'type' => 'game',
|
|
90
|
+
'action' => 'stPrepareNextRound',
|
|
91
|
+
'transitions' => ['roundStart' => 10, 'endGame' => 99],
|
|
92
|
+
],
|
|
93
|
+
|
|
94
|
+
99 => [
|
|
95
|
+
'name' => 'gameEnd',
|
|
96
|
+
'type' => 'manager',
|
|
97
|
+
'action' => 'stGameEnd',
|
|
98
|
+
'args' => 'argGameEnd',
|
|
99
|
+
],
|
|
100
|
+
];
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Rules You Must Enforce
|
|
104
|
+
|
|
105
|
+
- Every `activeplayer` and `multipleactiveplayer` state must define `possibleactions`.
|
|
106
|
+
- Every transition called in PHP must exist in the state's `transitions` map.
|
|
107
|
+
- `game` states must always call `nextState` (or equivalent transition logic) before returning.
|
|
108
|
+
|
|
109
|
+
## multipleactiveplayer Completion Rule
|
|
110
|
+
|
|
111
|
+
When a player resolves action in a `multipleactiveplayer` state, mark them done:
|
|
112
|
+
|
|
113
|
+
```php
|
|
114
|
+
$this->gamestate_setPlayerNonMultiactive($playerId, 'allBought');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Pattern:
|
|
118
|
+
- Enter state and activate all relevant players.
|
|
119
|
+
- Each player action calls `setPlayerNonMultiactive`.
|
|
120
|
+
- Transition fires only after all active players are done.
|
|
121
|
+
|
|
122
|
+
## Common Failure Modes
|
|
123
|
+
|
|
124
|
+
- Missing `possibleactions` causes `checkAction` to reject valid actions.
|
|
125
|
+
- Calling a transition name that is not declared stalls gameplay.
|
|
126
|
+
- Returning from a `game` state action without transition leaves the game stuck.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function allSameOrAllDifferent(values) {
|
|
2
|
+
const unique = new Set(values);
|
|
3
|
+
return unique.size === 1 || unique.size === 3;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isValidSet(cards) {
|
|
7
|
+
if (!Array.isArray(cards) || cards.length !== 3) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const colors = cards.map((c) => c.color);
|
|
12
|
+
const numbers = cards.map((c) => c.number);
|
|
13
|
+
const shadings = cards.map((c) => c.shading);
|
|
14
|
+
|
|
15
|
+
return allSameOrAllDifferent(colors) && allSameOrAllDifferent(numbers) && allSameOrAllDifferent(shadings);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function calculateScore(setCount, streak = 0) {
|
|
19
|
+
const base = Number(setCount) || 0;
|
|
20
|
+
return streak >= 3 ? base * 2 : base;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getLegalMoves(cards) {
|
|
24
|
+
if (!Array.isArray(cards)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const moves = [];
|
|
29
|
+
for (let i = 0; i < cards.length; i += 1) {
|
|
30
|
+
for (let j = i + 1; j < cards.length; j += 1) {
|
|
31
|
+
for (let k = j + 1; k < cards.length; k += 1) {
|
|
32
|
+
const combo = [cards[i], cards[j], cards[k]];
|
|
33
|
+
if (isValidSet(combo)) {
|
|
34
|
+
moves.push(combo);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return moves;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
isValidSet,
|
|
45
|
+
calculateScore,
|
|
46
|
+
getLegalMoves
|
|
47
|
+
};
|