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
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# bga-assist
|
|
2
|
+
|
|
3
|
+
## What This Is
|
|
4
|
+
This repository gives you two things for BGA development with Claude Code: a ready-to-use skill file and a local testing harness. The skill file (`SKILL.md`) teaches Claude Code the project conventions and generation patterns, while the harness provides PHP and JS test scaffolding so generated logic can be validated locally. Use the skill alone if you only want better code generation, or use the full harness if you also want runnable tests.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
### Skill Only (No Testing Infrastructure)
|
|
9
|
+
|
|
10
|
+
1. Clone or download this repository.
|
|
11
|
+
2. Add `SKILL.md` to your Claude Code workspace instructions.
|
|
12
|
+
|
|
13
|
+
### Full Harness
|
|
14
|
+
|
|
15
|
+
1. Clone this repo into your BGA game's development dependencies.
|
|
16
|
+
2. Install PHP support:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
composer require --dev thebgllc/bga-assist
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
3. Install JS support:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install --save-dev bga-assist
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
4. Extend `BgaGameTestCase` in your PHPUnit tests.
|
|
29
|
+
|
|
30
|
+
## Using with Claude Code
|
|
31
|
+
|
|
32
|
+
These prompts are designed to map directly to the tested patterns in `SampleGameTest.php`:
|
|
33
|
+
|
|
34
|
+
1. "What happens if the active player submits only 2 cards?"
|
|
35
|
+
2. "Write tests for the _checkValidSet function"
|
|
36
|
+
3. "Generate a states.inc.php for a game with a bidding phase and a resolution phase"
|
|
37
|
+
4. "I'm getting a PHP error in my action method - what's wrong?"
|
|
38
|
+
5. "How do I notify only the active player when they draw a card?"
|
|
39
|
+
|
|
40
|
+
## Running Tests
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# PHP
|
|
44
|
+
./vendor/bin/phpunit
|
|
45
|
+
|
|
46
|
+
# JS
|
|
47
|
+
npm test
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Scope Boundaries
|
|
51
|
+
|
|
52
|
+
This v1 repo intentionally does not include:
|
|
53
|
+
|
|
54
|
+
- MCP server setup
|
|
55
|
+
- SFTP deployment tooling
|
|
56
|
+
- Studio log tailing
|
|
57
|
+
- Playwright live Studio E2E tests
|
|
58
|
+
- BGA Studio API integration
|
|
59
|
+
- Game-specific production implementations
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bga-dev-skill
|
|
3
|
+
description: Instructions for Claude Code to generate BGA-compatible server code and tests using this repository's implemented harness and patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# BGA Development Instructions
|
|
7
|
+
|
|
8
|
+
## 1) Your Role
|
|
9
|
+
Act like a senior BGA gameplay engineer who writes production-style server logic and runnable tests. Use only APIs that exist in this repository's harness and tested examples, prefer deterministic game logic, and generate tests in the fluent style used by the provided base test case.
|
|
10
|
+
|
|
11
|
+
## 2) Framework Version Detection
|
|
12
|
+
Before writing code, detect project style and stay consistent:
|
|
13
|
+
|
|
14
|
+
- Legacy-style indicators: root-level game files, Dojo module frontend, notifyAllPlayers and notifyPlayer usage.
|
|
15
|
+
- Modern-style indicators: namespaced PHP classes and different notification/action APIs.
|
|
16
|
+
|
|
17
|
+
If the project is mixed or unclear, stop and ask which framework version to target. Do not mix APIs from different versions in one patch.
|
|
18
|
+
|
|
19
|
+
## 3) PHP Server - Critical Rules
|
|
20
|
+
These are enforced by implemented harness behavior and passing example tests:
|
|
21
|
+
|
|
22
|
+
- Use only harness-supported DB methods in game logic:
|
|
23
|
+
- DbQuery
|
|
24
|
+
- getCollectionFromDB
|
|
25
|
+
- getObjectFromDB
|
|
26
|
+
- getUniqueValueFromDB
|
|
27
|
+
- getIntFromDB
|
|
28
|
+
- Validate player actions with checkAction before mutating state.
|
|
29
|
+
- Use throwUserError for gameplay validation failures.
|
|
30
|
+
- Use throwVisibleSystemError for server/system failures.
|
|
31
|
+
- Drive state changes through:
|
|
32
|
+
- gamestate_nextState
|
|
33
|
+
- gamestate_changeActivePlayer
|
|
34
|
+
- gamestate_setAllPlayersMultiactive
|
|
35
|
+
- gamestate_setPlayerNonMultiactive
|
|
36
|
+
- Keep notification payload keys stable and explicit; use the same key names end-to-end.
|
|
37
|
+
- Do not use raw PDO directly in game classes.
|
|
38
|
+
- Do not use superglobals in action logic.
|
|
39
|
+
- Do not invent framework methods that are not present in the harness.
|
|
40
|
+
|
|
41
|
+
## 4) PHP Server - Common Patterns
|
|
42
|
+
Use these concrete patterns from the implemented sample game and harness:
|
|
43
|
+
|
|
44
|
+
- Setup/deal flow:
|
|
45
|
+
- Create required tables if missing.
|
|
46
|
+
- Seed baseline data if empty.
|
|
47
|
+
- Deal cards by moving records between locations.
|
|
48
|
+
- Set initial state and game-state values.
|
|
49
|
+
- Action flow:
|
|
50
|
+
- checkAction
|
|
51
|
+
- validate input
|
|
52
|
+
- load from DB
|
|
53
|
+
- run pure validation logic
|
|
54
|
+
- write DB changes
|
|
55
|
+
- send notifications
|
|
56
|
+
- transition state
|
|
57
|
+
- Scoring flow:
|
|
58
|
+
- Update score in DB.
|
|
59
|
+
- Update card locations in DB.
|
|
60
|
+
- Notify with result payload.
|
|
61
|
+
- Multi-active flow:
|
|
62
|
+
- mark all players multiactive.
|
|
63
|
+
- mark each player non-multiactive as they complete.
|
|
64
|
+
- transition when all are done.
|
|
65
|
+
|
|
66
|
+
## 5) Testing - Generating Tests from Natural Language
|
|
67
|
+
When asked "what happens when...", generate PHPUnit tests using BgaGameTestCase fluent helpers and preserve this pattern:
|
|
68
|
+
|
|
69
|
+
- Given:
|
|
70
|
+
- givenActivePlayer
|
|
71
|
+
- givenCurrentPlayer when actor mismatch matters
|
|
72
|
+
- givenState
|
|
73
|
+
- givenDatabaseRows
|
|
74
|
+
- givenGameStateValue
|
|
75
|
+
- When:
|
|
76
|
+
- whenAction(method, args)
|
|
77
|
+
- Then:
|
|
78
|
+
- result->assertSucceeded() or result->assertFailedWith(code)
|
|
79
|
+
- thenStateShouldBe
|
|
80
|
+
- thenNotificationSent or thenNotificationNotSent
|
|
81
|
+
- thenPlayerNotifiedWith when target-specific behavior matters
|
|
82
|
+
- thenDatabaseHas or thenDatabaseCount
|
|
83
|
+
|
|
84
|
+
Use these scenario templates from the sample tests:
|
|
85
|
+
|
|
86
|
+
- happy path
|
|
87
|
+
- invalid input with explicit error code
|
|
88
|
+
- wrong player acting
|
|
89
|
+
- endgame transition condition
|
|
90
|
+
- pure logic function test with direct assertions
|
|
91
|
+
|
|
92
|
+
Keep the CC PATTERN comment style in generated example-heavy tests because it is intentional teaching content.
|
|
93
|
+
|
|
94
|
+
## 6) Testing - What Can and Cannot Be Tested
|
|
95
|
+
Can test locally with this harness:
|
|
96
|
+
|
|
97
|
+
- server action validation
|
|
98
|
+
- state transitions
|
|
99
|
+
- DB writes/reads
|
|
100
|
+
- notification emission and payload subsets
|
|
101
|
+
- pure gameplay logic methods
|
|
102
|
+
|
|
103
|
+
Cannot test with this harness alone:
|
|
104
|
+
|
|
105
|
+
- Studio rendering behavior
|
|
106
|
+
- animation timing in live client
|
|
107
|
+
- real network transport behavior
|
|
108
|
+
- full end-to-end Studio integration
|
|
109
|
+
|
|
110
|
+
## 7) Sub-Skill References
|
|
111
|
+
When work touches these topics, consult these files and follow their guidance:
|
|
112
|
+
|
|
113
|
+
- State machine patterns: skills/state-machine.md
|
|
114
|
+
- Database patterns: skills/database-patterns.md
|
|
115
|
+
- JS Dojo patterns: skills/js-dojo-patterns.md
|
|
116
|
+
- Notifications contract: skills/notifications.md
|
|
117
|
+
- Scaffold templates: skills/scaffold-templates.md
|
package/composer.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "your-handle/bga-dev-skill",
|
|
3
|
+
"description": "CC skill and testing harness for BoardGameArena game development",
|
|
4
|
+
"require-dev": {
|
|
5
|
+
"phpunit/phpunit": "^10.0"
|
|
6
|
+
},
|
|
7
|
+
"autoload": {
|
|
8
|
+
"psr-4": {
|
|
9
|
+
"BgaHarness\\": "harness/php/",
|
|
10
|
+
"BgaExample\\": "harness/example/"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace BgaExample;
|
|
6
|
+
|
|
7
|
+
use BgaHarness\BgaStubs;
|
|
8
|
+
|
|
9
|
+
class SampleGame extends BgaStubs
|
|
10
|
+
{
|
|
11
|
+
public function __construct()
|
|
12
|
+
{
|
|
13
|
+
parent::__construct();
|
|
14
|
+
|
|
15
|
+
$this->_setAllowedActions([
|
|
16
|
+
'playerTurn' => ['action_playSet', 'action_pass'],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
$this->_setTransitions([
|
|
20
|
+
'playerTurn' => [
|
|
21
|
+
'nextPlayer' => 'playerTurn',
|
|
22
|
+
'endGame' => 'endGame',
|
|
23
|
+
],
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public function setupNewGame(): void
|
|
28
|
+
{
|
|
29
|
+
$this->DbQuery(
|
|
30
|
+
"CREATE TABLE IF NOT EXISTS card (" .
|
|
31
|
+
"card_id INTEGER PRIMARY KEY, " .
|
|
32
|
+
"card_type TEXT, " .
|
|
33
|
+
"card_number INTEGER, " .
|
|
34
|
+
"card_shading TEXT, " .
|
|
35
|
+
"card_location TEXT, " .
|
|
36
|
+
"card_location_arg INTEGER)"
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
$existingCards = $this->getIntFromDB('SELECT COUNT(*) FROM card');
|
|
40
|
+
if ($existingCards === 0) {
|
|
41
|
+
$types = ['red', 'green', 'blue'];
|
|
42
|
+
$numbers = [1, 2, 3];
|
|
43
|
+
$shadings = ['solid', 'striped', 'open'];
|
|
44
|
+
$cardId = 1;
|
|
45
|
+
|
|
46
|
+
for ($i = 0; $i < 3; $i++) {
|
|
47
|
+
foreach ($types as $type) {
|
|
48
|
+
foreach ($numbers as $number) {
|
|
49
|
+
foreach ($shadings as $shading) {
|
|
50
|
+
$this->DbQuery(sprintf(
|
|
51
|
+
"INSERT INTO card (card_id, card_type, card_number, card_shading, card_location, card_location_arg) VALUES (%d, '%s', %d, '%s', 'deck', 0)",
|
|
52
|
+
$cardId,
|
|
53
|
+
$type,
|
|
54
|
+
$number,
|
|
55
|
+
$shading
|
|
56
|
+
));
|
|
57
|
+
$cardId++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
$players = $this->getCollectionFromDB('SELECT player_id FROM player');
|
|
65
|
+
foreach ($players as $player) {
|
|
66
|
+
$playerId = (int) ($player['player_id'] ?? 0);
|
|
67
|
+
if ($playerId <= 0) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
$cardsToDeal = $this->getCollectionFromDB('SELECT card_id FROM card WHERE card_location = "deck" ORDER BY card_id LIMIT 3');
|
|
72
|
+
foreach ($cardsToDeal as $card) {
|
|
73
|
+
$this->DbQuery(sprintf(
|
|
74
|
+
"UPDATE card SET card_location = 'hand', card_location_arg = %d WHERE card_id = %d",
|
|
75
|
+
$playerId,
|
|
76
|
+
(int) $card['card_id']
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
$this->_setState('playerTurn');
|
|
82
|
+
$this->_setGameStateValue('cards_remaining', $this->getIntFromDB("SELECT COUNT(*) FROM card WHERE card_location = 'deck'"));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public function action_playSet(array $cardIds): void
|
|
86
|
+
{
|
|
87
|
+
$this->checkAction('action_playSet');
|
|
88
|
+
|
|
89
|
+
if (count($cardIds) !== 3) {
|
|
90
|
+
$this->throwUserError('invalidSet');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
$cards = [];
|
|
94
|
+
foreach ($cardIds as $cardId) {
|
|
95
|
+
$row = $this->getObjectFromDB('SELECT * FROM card WHERE card_id = ' . (int) $cardId);
|
|
96
|
+
if ($row === null) {
|
|
97
|
+
$this->throwUserError('invalidSet');
|
|
98
|
+
}
|
|
99
|
+
$cards[] = [
|
|
100
|
+
'type' => (string) ($row['card_type'] ?? ''),
|
|
101
|
+
'number' => (int) ($row['card_number'] ?? 1),
|
|
102
|
+
'shading' => (string) ($row['card_shading'] ?? 'solid'),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!$this->_checkValidSet($cards)) {
|
|
107
|
+
$this->throwUserError('invalidSet');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
$this->_scoreSet($this->getActivePlayerId(), $cardIds);
|
|
111
|
+
|
|
112
|
+
$remaining = $this->incGameStateValue('cards_remaining', -3);
|
|
113
|
+
if ($remaining <= 0) {
|
|
114
|
+
$this->gamestate_nextState('endGame');
|
|
115
|
+
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
$this->gamestate_nextState('nextPlayer');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public function action_pass(): void
|
|
123
|
+
{
|
|
124
|
+
$this->checkAction('action_pass');
|
|
125
|
+
$this->notifyAllPlayers('playerPassed', '', ['player_id' => $this->getActivePlayerId()]);
|
|
126
|
+
$this->gamestate_nextState('nextPlayer');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public function _checkValidSet(array $cards): bool
|
|
130
|
+
{
|
|
131
|
+
if (count($cards) !== 3) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
foreach (['type', 'number', 'shading'] as $field) {
|
|
136
|
+
$values = array_map(static fn (array $card): mixed => $card[$field] ?? null, $cards);
|
|
137
|
+
$unique = count(array_unique($values));
|
|
138
|
+
if ($unique !== 1 && $unique !== 3) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public function _scoreSet(int $playerId, array $cards): void
|
|
147
|
+
{
|
|
148
|
+
$this->DbQuery('UPDATE player SET player_score = player_score + 1 WHERE player_id = ' . $playerId);
|
|
149
|
+
foreach ($cards as $cardId) {
|
|
150
|
+
$this->DbQuery("UPDATE card SET card_location = 'discard' WHERE card_id = " . (int) $cardId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
$score = (int) $this->getUniqueValueFromDB('SELECT player_score FROM player WHERE player_id = ' . $playerId);
|
|
154
|
+
$this->notifyAllPlayers('setPlayed', '', [
|
|
155
|
+
'player_id' => $playerId,
|
|
156
|
+
'score' => $score,
|
|
157
|
+
'card_ids' => $cards,
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace BgaExample;
|
|
6
|
+
|
|
7
|
+
use BgaHarness\BgaGameTestCase;
|
|
8
|
+
use BgaHarness\BgaStubs;
|
|
9
|
+
|
|
10
|
+
class SampleGameTest extends BgaGameTestCase
|
|
11
|
+
{
|
|
12
|
+
protected function createGame(): BgaStubs
|
|
13
|
+
{
|
|
14
|
+
$game = new SampleGame();
|
|
15
|
+
$game->_setState('playerTurn');
|
|
16
|
+
$game->_setGameStateValue('cards_remaining', 12);
|
|
17
|
+
$game->_getDb()->seedTable('player', [
|
|
18
|
+
['player_id' => 1, 'player_name' => 'Alice', 'player_score' => 0],
|
|
19
|
+
['player_id' => 2, 'player_name' => 'Bob', 'player_score' => 0],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
return $game;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public function test_valid_set_scores_and_advances_state(): void
|
|
26
|
+
{
|
|
27
|
+
// CC PATTERN: happy path — given setup -> when action -> then state + notification
|
|
28
|
+
$this->givenActivePlayer(1)
|
|
29
|
+
->givenState('playerTurn')
|
|
30
|
+
->givenDatabaseRows('card', [
|
|
31
|
+
['card_id' => 1, 'card_type' => 'red', 'card_number' => 1, 'card_shading' => 'solid', 'card_location' => 'hand', 'card_location_arg' => 1],
|
|
32
|
+
['card_id' => 2, 'card_type' => 'green', 'card_number' => 2, 'card_shading' => 'striped', 'card_location' => 'hand', 'card_location_arg' => 1],
|
|
33
|
+
['card_id' => 3, 'card_type' => 'blue', 'card_number' => 3, 'card_shading' => 'open', 'card_location' => 'hand', 'card_location_arg' => 1],
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
$result = $this->whenAction('action_playSet', ['card_ids' => [1, 2, 3]]);
|
|
37
|
+
|
|
38
|
+
$result->assertSucceeded();
|
|
39
|
+
$this->thenNotificationSent('setPlayed', ['player_id' => 1, 'score' => 1]);
|
|
40
|
+
$this->thenDatabaseHas('card', ['card_location' => 'discard', 'card_id' => 1]);
|
|
41
|
+
$this->thenStateShouldBe('playerTurn');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public function test_invalid_set_rejected_with_error(): void
|
|
45
|
+
{
|
|
46
|
+
// CC PATTERN: invalid action — assert specific BGA error code thrown
|
|
47
|
+
$this->givenActivePlayer(1)
|
|
48
|
+
->givenState('playerTurn')
|
|
49
|
+
->givenDatabaseRows('card', [
|
|
50
|
+
['card_id' => 1, 'card_type' => 'red', 'card_number' => 1, 'card_shading' => 'solid'],
|
|
51
|
+
['card_id' => 2, 'card_type' => 'red', 'card_number' => 2, 'card_shading' => 'striped'],
|
|
52
|
+
['card_id' => 3, 'card_type' => 'blue', 'card_number' => 3, 'card_shading' => 'open'],
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
$result = $this->whenAction('action_playSet', ['card_ids' => [1, 2, 3]]);
|
|
56
|
+
|
|
57
|
+
$result->assertFailedWith('invalidSet');
|
|
58
|
+
$this->thenNotificationNotSent('setPlayed');
|
|
59
|
+
$this->thenStateShouldBe('playerTurn');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public function test_inactive_player_cannot_play(): void
|
|
63
|
+
{
|
|
64
|
+
// CC PATTERN: wrong actor — checkAction should block this
|
|
65
|
+
$this->givenActivePlayer(1)
|
|
66
|
+
->givenCurrentPlayer(2)
|
|
67
|
+
->givenState('playerTurn');
|
|
68
|
+
|
|
69
|
+
$result = $this->whenAction('action_playSet', ['card_ids' => [4, 5, 6]]);
|
|
70
|
+
|
|
71
|
+
$result->assertFailedWith('notYourTurn');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public function test_last_set_triggers_endgame(): void
|
|
75
|
+
{
|
|
76
|
+
// CC PATTERN: state transition — verify the right transition fires
|
|
77
|
+
$this->givenGameStateValue('cards_remaining', 3)
|
|
78
|
+
->givenActivePlayer(1)
|
|
79
|
+
->givenDatabaseRows('card', [
|
|
80
|
+
['card_id' => 1, 'card_type' => 'red', 'card_number' => 1, 'card_shading' => 'solid', 'card_location' => 'hand', 'card_location_arg' => 1],
|
|
81
|
+
['card_id' => 2, 'card_type' => 'green', 'card_number' => 2, 'card_shading' => 'striped', 'card_location' => 'hand', 'card_location_arg' => 1],
|
|
82
|
+
['card_id' => 3, 'card_type' => 'blue', 'card_number' => 3, 'card_shading' => 'open', 'card_location' => 'hand', 'card_location_arg' => 1],
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
$this->whenAction('action_playSet', ['card_ids' => [1, 2, 3]]);
|
|
86
|
+
|
|
87
|
+
$this->thenStateShouldBe('endGame');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public function test_set_validation_logic(): void
|
|
91
|
+
{
|
|
92
|
+
// CC PATTERN: pure function — no given/when/then needed, just call directly
|
|
93
|
+
$validSet = [
|
|
94
|
+
['type' => 'red', 'number' => 1, 'shading' => 'solid'],
|
|
95
|
+
['type' => 'green', 'number' => 2, 'shading' => 'striped'],
|
|
96
|
+
['type' => 'blue', 'number' => 3, 'shading' => 'open'],
|
|
97
|
+
];
|
|
98
|
+
$this->assertTrue($this->game->_checkValidSet($validSet));
|
|
99
|
+
|
|
100
|
+
$invalidSet = $validSet;
|
|
101
|
+
$invalidSet[2]['type'] = 'red';
|
|
102
|
+
$this->assertFalse($this->game->_checkValidSet($invalidSet));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { isValidSet, calculateScore, getLegalMoves } = require('../../src/setgame.utils.js');
|
|
2
|
+
|
|
3
|
+
describe('isValidSet', () => {
|
|
4
|
+
test('all same color is valid', () => {
|
|
5
|
+
const cards = [
|
|
6
|
+
{ color: 'red', number: 1, shading: 'solid' },
|
|
7
|
+
{ color: 'red', number: 2, shading: 'striped' },
|
|
8
|
+
{ color: 'red', number: 3, shading: 'open' }
|
|
9
|
+
];
|
|
10
|
+
expect(isValidSet(cards)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('all different colors is valid', () => {
|
|
14
|
+
const cards = [
|
|
15
|
+
{ color: 'red', number: 1, shading: 'solid' },
|
|
16
|
+
{ color: 'green', number: 2, shading: 'striped' },
|
|
17
|
+
{ color: 'blue', number: 3, shading: 'open' }
|
|
18
|
+
];
|
|
19
|
+
expect(isValidSet(cards)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('two same one different is invalid', () => {
|
|
23
|
+
const cards = [
|
|
24
|
+
{ color: 'red', number: 1, shading: 'solid' },
|
|
25
|
+
{ color: 'red', number: 2, shading: 'striped' },
|
|
26
|
+
{ color: 'blue', number: 3, shading: 'open' }
|
|
27
|
+
];
|
|
28
|
+
expect(isValidSet(cards)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('calculateScore', () => {
|
|
33
|
+
test('base score for standard set', () => {
|
|
34
|
+
expect(calculateScore(1, 0)).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('bonus multiplier for 3-in-a-row', () => {
|
|
38
|
+
expect(calculateScore(2, 3)).toBe(4);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('getLegalMoves', () => {
|
|
43
|
+
test('finds at least one legal set', () => {
|
|
44
|
+
const cards = [
|
|
45
|
+
{ color: 'red', number: 1, shading: 'solid' },
|
|
46
|
+
{ color: 'green', number: 2, shading: 'striped' },
|
|
47
|
+
{ color: 'blue', number: 3, shading: 'open' },
|
|
48
|
+
{ color: 'red', number: 1, shading: 'solid' }
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
expect(getLegalMoves(cards).length).toBeGreaterThan(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
global.dojo = {
|
|
2
|
+
place: jest.fn(),
|
|
3
|
+
query: jest.fn(() => ({ forEach: jest.fn(), length: 0 })),
|
|
4
|
+
style: jest.fn(),
|
|
5
|
+
addClass: jest.fn(),
|
|
6
|
+
removeClass: jest.fn(),
|
|
7
|
+
connect: jest.fn(),
|
|
8
|
+
on: jest.fn(),
|
|
9
|
+
animateProperty: jest.fn(() => ({ play: jest.fn() })),
|
|
10
|
+
require: jest.fn()
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
global.gameui = {
|
|
14
|
+
player_id: 1,
|
|
15
|
+
gamedatas: {},
|
|
16
|
+
addTooltip: jest.fn(),
|
|
17
|
+
ajaxcall: jest.fn(),
|
|
18
|
+
showMessage: jest.fn()
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const notificationHandlers = {};
|
|
22
|
+
|
|
23
|
+
global.bgaNotification = {
|
|
24
|
+
register: (type, handler) => {
|
|
25
|
+
notificationHandlers[type] = handler;
|
|
26
|
+
},
|
|
27
|
+
trigger: (type, args) => {
|
|
28
|
+
if (notificationHandlers[type]) {
|
|
29
|
+
notificationHandlers[type]({ args });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|