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 ADDED
@@ -0,0 +1,781 @@
1
+ # BGA Dev Skill & Testing Harness — Claude Code Build Plan
2
+
3
+ ## What This Repo Is
4
+
5
+ A publishable GitHub repo that makes Claude Code (CC) immediately useful for
6
+ BoardGameArena game development. Two deliverables in one repo:
7
+
8
+ 1. **SKILL.md** — A CC skill file encoding BGA conventions, API patterns, and
9
+ code generation rules. Users drop this into their own workspace and CC
10
+ becomes BGA-aware without any infrastructure.
11
+
12
+ 2. **Testing Harness** — A PHP stub library (PHPUnit) and JS utility layer
13
+ (Jest) that let CC generate and run meaningful tests against real game logic,
14
+ locally, without a live Studio slot.
15
+
16
+ An optional third layer — Playwright integration for E2E tests against a live
17
+ Studio slot — is scoped as a future v2 and should NOT be built in v1.
18
+
19
+ ---
20
+
21
+ ## Repo Structure to Create
22
+
23
+ ```
24
+ bga-dev-skill/
25
+
26
+ ├── SKILL.md # Primary skill file — CC reads this
27
+
28
+ ├── skills/ # Sub-skills referenced from SKILL.md
29
+ │ ├── state-machine.md # State machine patterns + worked examples
30
+ │ ├── database-patterns.md # DbQuery, getCollectionFromDB, SQL schema
31
+ │ ├── js-dojo-patterns.md # Dojo module patterns, ajaxcall, notifications
32
+ │ ├── notifications.md # notifyAllPlayers / notifyPlayer contracts
33
+ │ └── scaffold-templates.md # Boilerplate for each required BGA file
34
+
35
+ ├── harness/
36
+ │ ├── php/
37
+ │ │ ├── BgaStubs.php # Mock BGA framework methods
38
+ │ │ ├── BgaGameTestCase.php # Base test class with fluent helpers
39
+ │ │ ├── BgaNotificationSpy.php # Captures and asserts notifications
40
+ │ │ ├── BgaDatabaseFake.php # In-memory DB stand-in for DbQuery etc.
41
+ │ │ └── BgaExceptionTypes.php # BGA exception class stubs
42
+ │ ├── js/
43
+ │ │ ├── bgaStubs.js # Mock dojo, gameui, BGA globals
44
+ │ │ └── testHelpers.js # Assertion helpers for game state
45
+ │ └── example/
46
+ │ ├── SampleGame.php # Minimal example game using harness
47
+ │ ├── SampleGameTest.php # Annotated tests CC can learn from
48
+ │ └── sampleUtils.test.js # Example Jest tests for JS logic
49
+
50
+ ├── composer.json # PHPUnit dependency
51
+ ├── package.json # Jest dependency
52
+ ├── phpunit.xml # PHPUnit config pointing at harness + example
53
+ ├── jest.config.js # Jest config
54
+ └── README.md # Install instructions for end users
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Build Order
60
+
61
+ Work through these phases in sequence. Each phase should be fully working and
62
+ committed before starting the next. CC should not jump ahead.
63
+
64
+ ### Phase 1 — Repo Scaffolding
65
+ ### Phase 2 — PHP Stub Library
66
+ ### Phase 3 — Base Test Case (fluent helpers)
67
+ ### Phase 4 — SKILL.md (main skill file)
68
+ ### Phase 5 — Sub-skill files
69
+ ### Phase 6 — Example game + annotated tests
70
+ ### Phase 7 — JS stub layer + Jest config
71
+ ### Phase 8 — README
72
+
73
+ ---
74
+
75
+ ## Phase 1: Repo Scaffolding
76
+
77
+ Create the directory structure above. Create all files as empty stubs with a
78
+ single comment indicating what they will contain. This gives CC the full file
79
+ tree to reason about from the start.
80
+
81
+ Create `composer.json`:
82
+ ```json
83
+ {
84
+ "name": "your-handle/bga-dev-skill",
85
+ "description": "CC skill and testing harness for BoardGameArena game development",
86
+ "require-dev": {
87
+ "phpunit/phpunit": "^10.0"
88
+ },
89
+ "autoload": {
90
+ "psr-4": {
91
+ "BgaHarness\\": "harness/php/",
92
+ "BgaExample\\": "harness/example/"
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ Create `package.json`:
99
+ ```json
100
+ {
101
+ "name": "bga-dev-skill",
102
+ "scripts": {
103
+ "test": "jest"
104
+ },
105
+ "devDependencies": {
106
+ "jest": "^29.0.0"
107
+ }
108
+ }
109
+ ```
110
+
111
+ Create `phpunit.xml` pointing at `harness/` with colors enabled.
112
+
113
+ ---
114
+
115
+ ## Phase 2: PHP Stub Library
116
+
117
+ This is the most critical phase. The stubs must accurately reflect BGA's actual
118
+ API so CC doesn't hallucinate method signatures when generating tests.
119
+
120
+ ### `BgaDatabaseFake.php`
121
+
122
+ An in-memory SQLite-backed (or array-backed) fake for BGA's DB layer. Must
123
+ implement these methods with identical signatures to BGA's real framework:
124
+
125
+ ```php
126
+ namespace BgaHarness;
127
+
128
+ class BgaDatabaseFake {
129
+ private array $tables = [];
130
+
131
+ // Executes a query string — for INSERT, UPDATE, DELETE
132
+ public function DbQuery(string $sql): void {}
133
+
134
+ // Returns array of rows — equivalent to BGA's getCollectionFromDB
135
+ public function getCollectionFromDB(string $sql, bool $bUniqueValue = false): array {}
136
+
137
+ // Returns single row
138
+ public function getObjectFromDB(string $sql): ?array {}
139
+
140
+ // Returns single value
141
+ public function getUniqueValueFromDB(string $sql): mixed {}
142
+
143
+ // Returns count
144
+ public function getIntFromDB(string $sql): int {}
145
+
146
+ // Seed the fake with table data for test setup
147
+ public function seedTable(string $table, array $rows): void {}
148
+
149
+ // Assert a row exists matching conditions
150
+ public function assertRowExists(string $table, array $conditions): void {}
151
+
152
+ // Assert row count
153
+ public function assertRowCount(string $table, int $expected, array $conditions = []): void {}
154
+ }
155
+ ```
156
+
157
+ Implementation note: Use PHP's PDO with SQLite `:memory:` as the backing store.
158
+ This lets real SQL run against the fake, catching SQL errors in tests.
159
+
160
+ ### `BgaNotificationSpy.php`
161
+
162
+ Captures all notifications emitted during a test for later assertion:
163
+
164
+ ```php
165
+ namespace BgaHarness;
166
+
167
+ class BgaNotificationSpy {
168
+ private array $notifications = [];
169
+
170
+ public function notifyAllPlayers(string $type, string $message, array $data): void {
171
+ $this->notifications[] = [
172
+ 'target' => 'all',
173
+ 'type' => $type,
174
+ 'message' => $message,
175
+ 'data' => $data,
176
+ ];
177
+ }
178
+
179
+ public function notifyPlayer(int $playerId, string $type, string $message, array $data): void {
180
+ $this->notifications[] = [
181
+ 'target' => $playerId,
182
+ 'type' => $type,
183
+ 'message' => $message,
184
+ 'data' => $data,
185
+ ];
186
+ }
187
+
188
+ // Assertions
189
+ public function assertNotified(string $type, array $dataSubset = []): void {}
190
+ public function assertNotifiedPlayer(int $playerId, string $type, array $dataSubset = []): void {}
191
+ public function assertNotNotified(string $type): void {}
192
+ public function assertNotificationCount(int $expected): void {}
193
+ public function getNotifications(): array { return $this->notifications; }
194
+ public function reset(): void { $this->notifications = []; }
195
+ }
196
+ ```
197
+
198
+ ### `BgaStubs.php`
199
+
200
+ The main framework stub. Game classes extend `Table` in BGA — this stub
201
+ replaces that base class for testing:
202
+
203
+ ```php
204
+ namespace BgaHarness;
205
+
206
+ abstract class BgaStubs {
207
+
208
+ protected BgaDatabaseFake $db;
209
+ protected BgaNotificationSpy $notifications;
210
+
211
+ // Player state
212
+ private int $activePlayerId;
213
+ private array $players = [];
214
+ private string $currentState = 'gameSetup';
215
+
216
+ public function __construct() {
217
+ $this->db = new BgaDatabaseFake();
218
+ $this->notifications = new BgaNotificationSpy();
219
+ }
220
+
221
+ // ── Player management ──────────────────────────────────────────
222
+
223
+ protected function getActivePlayerId(): int {}
224
+ protected function getActivePlayerName(): string {}
225
+ protected function getPlayerNameById(int $playerId): string {}
226
+ protected function getCurrentPlayerId(): int {}
227
+
228
+ // ── State machine ──────────────────────────────────────────────
229
+
230
+ protected function gamestate_nextState(string $transition): void {}
231
+ protected function gamestate_changeActivePlayer(int $playerId): void {}
232
+ protected function gamestate_setAllPlayersMultiactive(): void {}
233
+ protected function gamestate_setPlayerNonMultiactive(int $playerId, string $nextState): void {}
234
+ protected function checkAction(string $actionName): bool {} // validates vs state machine
235
+
236
+ // ── Database pass-throughs ─────────────────────────────────────
237
+
238
+ protected function DbQuery(string $sql): void {
239
+ $this->db->DbQuery($sql);
240
+ }
241
+ protected function getCollectionFromDB(string $sql, bool $bUniqueValue = false): array {
242
+ return $this->db->getCollectionFromDB($sql, $bUniqueValue);
243
+ }
244
+ protected function getObjectFromDB(string $sql): ?array {
245
+ return $this->db->getObjectFromDB($sql);
246
+ }
247
+ protected function getUniqueValueFromDB(string $sql): mixed {
248
+ return $this->db->getUniqueValueFromDB($sql);
249
+ }
250
+
251
+ // ── Notifications pass-throughs ────────────────────────────────
252
+
253
+ protected function notifyAllPlayers(string $type, string $msg, array $data): void {
254
+ $this->notifications->notifyAllPlayers($type, $msg, $data);
255
+ }
256
+ protected function notifyPlayer(int $id, string $type, string $msg, array $data): void {
257
+ $this->notifications->notifyPlayer($id, $type, $msg, $data);
258
+ }
259
+
260
+ // ── BGA utility methods ────────────────────────────────────────
261
+
262
+ protected function getGameStateValue(string $name): int {}
263
+ protected function setGameStateValue(string $name, int $value): void {}
264
+ protected function incGameStateValue(string $name, int $increment): int {}
265
+
266
+ // ── Cards (BGA Deck component stub) ───────────────────────────
267
+
268
+ protected function createDeck(string $deckId): BgaDeckStub {}
269
+
270
+ // ── Error handling ─────────────────────────────────────────────
271
+
272
+ // BGA uses these — tests can assert they are (or aren't) thrown
273
+ protected function throwUserError(string $errorCode): never {
274
+ throw new BgaUserException($errorCode);
275
+ }
276
+ protected function throwVisibleSystemError(string $message): never {
277
+ throw new BgaVisibleSystemException($message);
278
+ }
279
+
280
+ // ── Test setup helpers (not in real BGA — test-only) ──────────
281
+
282
+ public function _setActivePlayer(int $playerId): void {}
283
+ public function _setPlayers(array $players): void {}
284
+ public function _setState(string $stateName): void {}
285
+ public function _getDb(): BgaDatabaseFake { return $this->db; }
286
+ public function _getNotifications(): BgaNotificationSpy { return $this->notifications; }
287
+ }
288
+ ```
289
+
290
+ ### `BgaExceptionTypes.php`
291
+
292
+ ```php
293
+ namespace BgaHarness;
294
+
295
+ // BGA throws these — stubs so test code can catch them
296
+ class BgaUserException extends \Exception {}
297
+ class BgaVisibleSystemException extends \Exception {}
298
+ class BgaSystemException extends \Exception {}
299
+ ```
300
+
301
+ ---
302
+
303
+ ## Phase 3: Base Test Case
304
+
305
+ A fluent test case base class that CC should use as the template for every
306
+ generated test. The fluent style is important — it makes CC's output from
307
+ natural language queries readable and self-documenting.
308
+
309
+ ```php
310
+ namespace BgaHarness;
311
+
312
+ use PHPUnit\Framework\TestCase;
313
+
314
+ abstract class BgaGameTestCase extends TestCase {
315
+
316
+ protected BgaStubs $game; // subclasses set this to their game instance
317
+
318
+ protected function setUp(): void {
319
+ parent::setUp();
320
+ $this->game = $this->createGame();
321
+ $this->game->_setPlayers($this->defaultPlayers());
322
+ }
323
+
324
+ // Subclass implements this to return their game instance
325
+ abstract protected function createGame(): BgaStubs;
326
+
327
+ // ── Given (setup) ──────────────────────────────────────────────
328
+
329
+ protected function givenActivePlayer(int $playerId): static {
330
+ $this->game->_setActivePlayer($playerId);
331
+ return $this;
332
+ }
333
+
334
+ protected function givenState(string $stateName): static {
335
+ $this->game->_setState($stateName);
336
+ return $this;
337
+ }
338
+
339
+ protected function givenDatabaseRows(string $table, array $rows): static {
340
+ $this->game->_getDb()->seedTable($table, $rows);
341
+ return $this;
342
+ }
343
+
344
+ protected function givenGameStateValue(string $name, int $value): static {
345
+ // seeds a game state variable
346
+ return $this;
347
+ }
348
+
349
+ // ── When (action) ──────────────────────────────────────────────
350
+
351
+ // Call a player action method — wraps in try/catch for error assertions
352
+ protected function whenAction(string $method, array $args = []): ActionResult {
353
+ try {
354
+ $result = $this->game->$method(...array_values($args));
355
+ return ActionResult::success($result);
356
+ } catch (BgaUserException $e) {
357
+ return ActionResult::userError($e->getMessage());
358
+ } catch (BgaVisibleSystemException $e) {
359
+ return ActionResult::systemError($e->getMessage());
360
+ }
361
+ }
362
+
363
+ // ── Then (assertion) ───────────────────────────────────────────
364
+
365
+ protected function thenStateShouldBe(string $expected): void {
366
+ // read current state from game and assert
367
+ }
368
+
369
+ protected function thenNotificationSent(string $type, array $dataSubset = []): void {
370
+ $this->game->_getNotifications()->assertNotified($type, $dataSubset);
371
+ }
372
+
373
+ protected function thenNotificationNotSent(string $type): void {
374
+ $this->game->_getNotifications()->assertNotNotified($type);
375
+ }
376
+
377
+ protected function thenPlayerNotifiedWith(int $playerId, string $type, array $dataSubset = []): void {
378
+ $this->game->_getNotifications()->assertNotifiedPlayer($playerId, $type, $dataSubset);
379
+ }
380
+
381
+ protected function thenDatabaseHas(string $table, array $conditions): void {
382
+ $this->game->_getDb()->assertRowExists($table, $conditions);
383
+ }
384
+
385
+ protected function thenDatabaseCount(string $table, int $count, array $conditions = []): void {
386
+ $this->game->_getDb()->assertRowCount($table, $count, $conditions);
387
+ }
388
+
389
+ // ── Defaults ───────────────────────────────────────────────────
390
+
391
+ protected function defaultPlayers(): array {
392
+ return [
393
+ 1 => ['player_id' => 1, 'player_name' => 'Alice', 'player_score' => 0],
394
+ 2 => ['player_id' => 2, 'player_name' => 'Bob', 'player_score' => 0],
395
+ ];
396
+ }
397
+ }
398
+
399
+ // Wraps action results so tests can assert on success or specific error type
400
+ class ActionResult {
401
+ public bool $succeeded;
402
+ public ?string $errorCode;
403
+ public mixed $returnValue;
404
+
405
+ public static function success(mixed $value): self {}
406
+ public static function userError(string $code): self {}
407
+ public static function systemError(string $message): self {}
408
+
409
+ public function assertSucceeded(): void {}
410
+ public function assertFailedWith(string $errorCode): void {}
411
+ }
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Phase 4: SKILL.md
417
+
418
+ This is what CC reads when working in a BGA game project. It should be opinionated
419
+ and direct — written as instructions to CC, not as documentation for humans.
420
+
421
+ Structure:
422
+
423
+ ```markdown
424
+ ---
425
+ name: bga-dev-skill
426
+ description: BGA game development patterns, API reference, and testing conventions
427
+ for BoardGameArena games. Provides correct method signatures, state machine
428
+ patterns, and test generation for PHP game logic and JS frontend code.
429
+ ---
430
+
431
+ # BGA Development
432
+
433
+ ## Your Role
434
+ When working in a BGA project, you are a BGA-experienced developer who knows
435
+ the framework's constraints and idioms. Never suggest raw PDO, never use
436
+ superglobals, never call BGA methods that don't exist. When generating tests,
437
+ always use BgaGameTestCase and the fluent pattern.
438
+
439
+ ## File Structure (every BGA game has exactly these files)
440
+ [list all required files with their purpose]
441
+
442
+ ## PHP Server — Critical Rules
443
+ [BGA DB layer — only these methods exist]
444
+ [State machine — how transitions work]
445
+ [Error throwing — only these two methods]
446
+ [What self:: calls are valid]
447
+
448
+ ## PHP Server — Common Patterns
449
+ [Card dealing from Deck component]
450
+ [Multi-active player handling]
451
+ [Scoring patterns]
452
+ [Undo-safe action patterns]
453
+
454
+ ## JS Frontend — Critical Rules
455
+ [Dojo module declaration pattern]
456
+ [ajaxcall signature and error handling]
457
+ [Notification handler registration]
458
+ [Never use document.querySelector — use dojo.query]
459
+
460
+ ## State Machine
461
+ [states.inc.php structure]
462
+ [Transition naming conventions]
463
+ [Multi-active state patterns]
464
+ [When to use activeplayer vs multipleactiveplayer]
465
+
466
+ ## Testing — Generating Tests from Natural Language
467
+ When asked "what happens when [scenario]", generate a PHPUnit test using
468
+ BgaGameTestCase with this exact pattern:
469
+ [worked example]
470
+
471
+ When asked to test a JS utility function, generate a Jest test with this pattern:
472
+ [worked example]
473
+
474
+ ## Testing — What Can and Cannot Be Tested
475
+ CAN test: game logic, state transitions, DB writes, notification contents
476
+ CANNOT test without a live Studio slot: rendering, animations, ajaxcall network
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Phase 5: Sub-skill Files
482
+
483
+ ### `skills/state-machine.md`
484
+
485
+ Full worked example of a states.inc.php for a 3-state game with:
486
+ - `gameSetup` → `playerTurn` → `endGame`
487
+ - Multi-active voting state example
488
+ - All required keys documented with correct types
489
+ - Common mistake: forgetting `possibleactions` in non-game-end states
490
+
491
+ ### `skills/database-patterns.md`
492
+
493
+ Correct SQL and PHP for the 6 most common BGA database operations:
494
+ - Dealing cards from deck to hand
495
+ - Moving cards between locations
496
+ - Incrementing player score
497
+ - Getting all cards in a location
498
+ - Getting a specific player's hand count
499
+ - Checking if a player has a specific card
500
+
501
+ Each pattern shown as both raw SQL (for `DbQuery`) and as the recommended
502
+ `getCollectionFromDB` / `getObjectFromDB` call.
503
+
504
+ ### `skills/js-dojo-patterns.md`
505
+
506
+ - Correct `define([...], function(...) { ... })` module pattern
507
+ - `ajaxcall` with action, args, handler, and error handler
508
+ - `addTooltip` and `addTooltipToClass`
509
+ - `dojo.place`, `dojo.query`, `dojo.connect` vs `dojo.on`
510
+ - How to register a notification handler in `setupNotifications()`
511
+ - Animation with `dojo.animateProperty`
512
+
513
+ ### `skills/notifications.md`
514
+
515
+ The notification contract between PHP and JS — this is where most BGA bugs live.
516
+
517
+ For each notification type:
518
+ - PHP: which method to call, data shape
519
+ - JS: how to register the handler, what `notif.args` contains
520
+ - Common mistake: PHP sends `card_id`, JS expects `cardId` (casing mismatch)
521
+
522
+ ### `skills/scaffold-templates.md`
523
+
524
+ Minimal correct boilerplate for each required file. CC should use these as
525
+ starting points, not invent its own structure. Include:
526
+ - `gamename.game.php` — correct class extension, required method stubs
527
+ - `gamename.action.php` — correct action method pattern with sanitization
528
+ - `gamename.view.php` — correct view method
529
+ - `gamename.js` — correct Dojo define wrapper with required methods
530
+ - `states.inc.php` — minimal 2-state example
531
+ - `dbmodel.sql` — correct format with player table already included
532
+
533
+ ---
534
+
535
+ ## Phase 6: Example Game + Annotated Tests
536
+
537
+ A minimal but complete game called `setgame` (a simplified Set card game).
538
+ This is the reference implementation CC learns test patterns from.
539
+
540
+ ### `SampleGame.php`
541
+
542
+ Implements exactly:
543
+ - `setupNewGame()` — deals cards to players
544
+ - `action_playSet(array $cardIds)` — validates and plays a set
545
+ - `action_pass()` — player passes their turn
546
+ - `_checkValidSet(array $cards): bool` — pure logic, easily testable
547
+ - `_scoreSet(int $playerId, array $cards): void` — updates DB and notifies
548
+
549
+ ### `SampleGameTest.php`
550
+
551
+ Must include annotated tests for all of these scenarios, with comments
552
+ explaining the pattern so CC can adapt them:
553
+
554
+ ```php
555
+ // ── Happy path ─────────────────────────────────────────────────────
556
+
557
+ public function test_valid_set_scores_and_advances_state(): void {
558
+ // CC PATTERN: happy path — given setup → when action → then state + notification
559
+ $this->givenActivePlayer(1)
560
+ ->givenState('playerTurn')
561
+ ->givenDatabaseRows('card', [
562
+ ['card_id' => 1, 'card_type' => 'red', 'card_location' => 'hand', 'card_location_arg' => 1],
563
+ ['card_id' => 2, 'card_type' => 'green', 'card_location' => 'hand', 'card_location_arg' => 1],
564
+ ['card_id' => 3, 'card_type' => 'blue', 'card_location' => 'hand', 'card_location_arg' => 1],
565
+ ]);
566
+
567
+ $result = $this->whenAction('action_playSet', ['card_ids' => [1, 2, 3]]);
568
+
569
+ $result->assertSucceeded();
570
+ $this->thenNotificationSent('setPlayed', ['player_id' => 1, 'score' => 1]);
571
+ $this->thenDatabaseHas('card', ['card_location' => 'discard', 'card_id' => 1]);
572
+ $this->thenStateShouldBe('playerTurn'); // next player's turn
573
+ }
574
+
575
+ // ── Invalid input ──────────────────────────────────────────────────
576
+
577
+ public function test_invalid_set_rejected_with_error(): void {
578
+ // CC PATTERN: invalid action — assert specific BGA error code thrown
579
+ $this->givenActivePlayer(1)
580
+ ->givenState('playerTurn')
581
+ ->givenDatabaseRows('card', [/* two matching, one not */]);
582
+
583
+ $result = $this->whenAction('action_playSet', ['card_ids' => [1, 2, 3]]);
584
+
585
+ $result->assertFailedWith('invalidSet');
586
+ $this->thenNotificationNotSent('setPlayed');
587
+ $this->thenStateShouldBe('playerTurn'); // state unchanged
588
+ }
589
+
590
+ // ── Wrong player ───────────────────────────────────────────────────
591
+
592
+ public function test_inactive_player_cannot_play(): void {
593
+ // CC PATTERN: wrong actor — checkAction should block this
594
+ $this->givenActivePlayer(1)
595
+ ->givenState('playerTurn');
596
+
597
+ // Player 2 tries to act when it's player 1's turn
598
+ $result = $this->whenAction('action_playSet', ['card_ids' => [4, 5, 6]]);
599
+
600
+ $result->assertFailedWith('notYourTurn');
601
+ }
602
+
603
+ // ── State transition ───────────────────────────────────────────────
604
+
605
+ public function test_last_set_triggers_endgame(): void {
606
+ // CC PATTERN: state transition — verify the right transition fires
607
+ $this->givenGameStateValue('cards_remaining', 3)
608
+ ->givenActivePlayer(1);
609
+
610
+ $this->whenAction('action_playSet', ['card_ids' => [1, 2, 3]]);
611
+
612
+ $this->thenStateShouldBe('endGame');
613
+ }
614
+
615
+ // ── Pure logic ─────────────────────────────────────────────────────
616
+
617
+ public function test_set_validation_logic(): void {
618
+ // CC PATTERN: pure function — no given/when/then needed, just call directly
619
+ $validSet = [
620
+ ['type' => 'red', 'number' => 1, 'shading' => 'solid'],
621
+ ['type' => 'green', 'number' => 2, 'shading' => 'striped'],
622
+ ['type' => 'blue', 'number' => 3, 'shading' => 'open'],
623
+ ];
624
+ $this->assertTrue($this->game->_checkValidSet($validSet));
625
+
626
+ $invalidSet = $validSet;
627
+ $invalidSet[2]['type'] = 'red'; // two reds, not all different or all same
628
+ $this->assertFalse($this->game->_checkValidSet($invalidSet));
629
+ }
630
+ ```
631
+
632
+ ---
633
+
634
+ ## Phase 7: JS Stub Layer
635
+
636
+ ### `harness/js/bgaStubs.js`
637
+
638
+ Minimal Jest-compatible mocks for BGA globals:
639
+
640
+ ```javascript
641
+ // Mocks the dojo global that BGA's JS assumes exists
642
+ global.dojo = {
643
+ place: jest.fn(),
644
+ query: jest.fn(() => ({ forEach: jest.fn(), length: 0 })),
645
+ style: jest.fn(),
646
+ addClass: jest.fn(),
647
+ removeClass: jest.fn(),
648
+ connect: jest.fn(),
649
+ on: jest.fn(),
650
+ animateProperty: jest.fn(() => ({ play: jest.fn() })),
651
+ require: jest.fn(),
652
+ };
653
+
654
+ // Mocks the gameui global
655
+ global.gameui = {
656
+ player_id: 1,
657
+ gamedatas: {},
658
+ addTooltip: jest.fn(),
659
+ ajaxcall: jest.fn(),
660
+ showMessage: jest.fn(),
661
+ };
662
+
663
+ // BGA notification mock — call triggerNotification in tests
664
+ const notificationHandlers = {};
665
+ global.bgaNotification = {
666
+ register: (type, handler) => { notificationHandlers[type] = handler; },
667
+ trigger: (type, args) => {
668
+ if (notificationHandlers[type]) notificationHandlers[type]({ args });
669
+ },
670
+ };
671
+ ```
672
+
673
+ ### `jest.config.js`
674
+
675
+ ```javascript
676
+ module.exports = {
677
+ testEnvironment: 'node',
678
+ setupFiles: ['./harness/js/bgaStubs.js'],
679
+ testMatch: ['**/*.test.js'],
680
+ };
681
+ ```
682
+
683
+ ### `harness/example/sampleUtils.test.js`
684
+
685
+ Example Jest tests for extractable game logic:
686
+
687
+ ```javascript
688
+ import { isValidSet, calculateScore, getLegalMoves } from '../../src/setgame.utils.js';
689
+
690
+ describe('isValidSet', () => {
691
+ test('all same color is valid', () => { /* ... */ });
692
+ test('all different colors is valid', () => { /* ... */ });
693
+ test('two same one different is invalid', () => { /* ... */ });
694
+ });
695
+
696
+ describe('calculateScore', () => {
697
+ test('base score for standard set', () => { /* ... */ });
698
+ test('bonus multiplier for 3-in-a-row', () => { /* ... */ });
699
+ });
700
+ ```
701
+
702
+ ---
703
+
704
+ ## Phase 8: README
705
+
706
+ Structure:
707
+
708
+ ### Installation (two paths)
709
+
710
+ **Skill only** (no testing infrastructure needed):
711
+ ```
712
+ 1. Clone this repo
713
+ 2. Add to your Claude Code workspace
714
+ 3. Reference SKILL.md in your workspace settings
715
+ ```
716
+
717
+ **Full harness**:
718
+ ```
719
+ 1. Clone this repo into your BGA game's dev dependencies
720
+ 2. composer require --dev your-handle/bga-dev-skill
721
+ 3. npm install --save-dev bga-dev-skill
722
+ 4. Extend BgaGameTestCase in your tests
723
+ ```
724
+
725
+ ### Using with Claude Code
726
+
727
+ Show 5 example prompts that now work correctly:
728
+
729
+ ```
730
+ "What happens if the active player submits an empty card selection?"
731
+ → CC generates a PHPUnit test using BgaGameTestCase
732
+
733
+ "Write tests for the _calculateScore function"
734
+ → CC generates both a PHP unit test and a Jest test
735
+
736
+ "Generate the states.inc.php for a game with a bidding phase and a resolution phase"
737
+ → CC uses the state machine skill to produce correct structure
738
+
739
+ "I'm getting a PHP error on line 47 of my action file — what's wrong?"
740
+ → CC has context about valid BGA action method patterns
741
+
742
+ "How do I notify only the active player when they draw a card?"
743
+ → CC uses the notifications skill to give the exact notifyPlayer call
744
+ ```
745
+
746
+ ### Running Tests
747
+
748
+ ```bash
749
+ # PHP
750
+ ./vendor/bin/phpunit
751
+
752
+ # JS
753
+ npm test
754
+ ```
755
+
756
+ ---
757
+
758
+ ## What CC Should NOT Build (Scope Boundaries)
759
+
760
+ - No MCP server (CC handles code gen natively)
761
+ - No SFTP deployment tooling (out of scope for v1)
762
+ - No log tailing (requires live Studio infrastructure)
763
+ - No Playwright tests (v2 — note in README as planned)
764
+ - No BGA Studio API integration of any kind
765
+ - No game-specific logic (harness is game-agnostic)
766
+
767
+ ---
768
+
769
+ ## Definition of Done
770
+
771
+ Each phase is done when:
772
+ - [ ] Files exist and contain real implementation (not stubs)
773
+ - [ ] `composer install && ./vendor/bin/phpunit` passes with no errors
774
+ - [ ] `npm install && npm test` passes with no errors
775
+ - [ ] The example game tests all pass against the example game
776
+ - [ ] CC can answer "what happens when a player plays an invalid set" by
777
+ generating a test that actually runs and fails correctly before the
778
+ fix, then passes after
779
+
780
+ The repo is ready to publish when all phases are complete and the README
781
+ install instructions work on a clean machine.