board-game-engine-react 0.0.16 → 0.0.18

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.
@@ -0,0 +1,215 @@
1
+ {
2
+ "entities": [
3
+ {
4
+ "name": "mainGrid",
5
+ "entityType": "Grid",
6
+ "width": 8,
7
+ "height": 8
8
+ },
9
+ {
10
+ "name": "disc",
11
+ "state": { "player": "0" },
12
+ "count": "Infinity"
13
+ }
14
+ ],
15
+ "sharedBoard": [{ "name": "mainGrid" }],
16
+ "initialPlacements": [
17
+ {
18
+ "entity": {
19
+ "name": "disc",
20
+ "state": { "player": "0" }
21
+ },
22
+ "destination": { "index": 27 }
23
+ },
24
+ {
25
+ "entity": {
26
+ "name": "disc",
27
+ "state": { "player": "1" }
28
+ },
29
+ "destination": { "index": 28 }
30
+ },
31
+ {
32
+ "entity": {
33
+ "name": "disc",
34
+ "state": { "player": "1" }
35
+ },
36
+ "destination": { "index": 35 }
37
+ },
38
+ {
39
+ "entity": {
40
+ "name": "disc",
41
+ "state": { "player": "0" }
42
+ },
43
+ "destination": { "index": 36 }
44
+ }
45
+ ],
46
+ "minPlayers": 2,
47
+ "maxPlayers": 2,
48
+ "turn": {
49
+ "minMoves": 1,
50
+ "maxMoves": 1,
51
+ "initialMoves": [{
52
+ "moveType": "PassTurn",
53
+ "conditions": [{ "conditionType": "NoPossibleMoves" }]
54
+ }]
55
+ },
56
+ "moves": {
57
+ "placeDisc": {
58
+ "moveType": "PlaceNew",
59
+ "entity": {
60
+ "state": {
61
+ "player": {
62
+ "type": "ctxPath",
63
+ "path": ["currentPlayer"]
64
+ }
65
+ },
66
+ "conditions": [{
67
+ "conditionType": "Is",
68
+ "matcher": { "name": "disc" }
69
+ }]
70
+ },
71
+ "arguments": {
72
+ "destination": {
73
+ "playerChoice": true,
74
+ "conditions": [
75
+ {
76
+ "conditionType": "Is",
77
+ "matcher": { "entityType": "Space" }
78
+ },
79
+ {
80
+ "conditionType": "Not",
81
+ "conditions": [{ "conditionType": "Contains" }]
82
+ },
83
+ {
84
+ "conditionType": "InLine",
85
+ "sequence": [
86
+ {
87
+ "conditions": [{
88
+ "conditionType": "Is",
89
+ "entity": {
90
+ "type": "contextPath",
91
+ "path": ["originalTarget"]
92
+ }
93
+ }]
94
+ },
95
+ {
96
+ "minCount": 1,
97
+ "conditions": [
98
+ {
99
+ "conditionType": "Not",
100
+ "conditions": [{
101
+ "conditionType": "Contains",
102
+ "conditions": [{
103
+ "conditionType": "Is",
104
+ "matcher": {
105
+ "player": {
106
+ "type": "ctxPath",
107
+ "path": ["currentPlayer"]
108
+ }
109
+ }
110
+ }]
111
+ }]
112
+ },
113
+ {
114
+ "conditionType": "ContainsSame",
115
+ "properties": ["player"]
116
+ }
117
+ ]
118
+ },
119
+ {
120
+ "conditions": [{
121
+ "conditionType": "Contains",
122
+ "conditions": [{
123
+ "conditionType": "Is",
124
+ "matcher": {
125
+ "player": {
126
+ "type": "ctxPath",
127
+ "path": ["currentPlayer"]
128
+ }
129
+ }
130
+ }]
131
+ }]
132
+ }
133
+ ]
134
+ }
135
+ ]
136
+ }
137
+ },
138
+ "then": [{
139
+ "moveType": "ForEach",
140
+ "arguments": {
141
+ "targets": {
142
+ "type": "contextPath",
143
+ "path": [
144
+ "moveConditionResults",
145
+ 0,
146
+ "conditionResults",
147
+ "argumentResults",
148
+ "destination",
149
+ "results",
150
+ 0,
151
+ "results",
152
+ 2,
153
+ "matches",
154
+ { "flatten": true, "map": ["entities"] },
155
+ { "flatten": true }
156
+ ]
157
+ }
158
+ },
159
+ "move": {
160
+ "moveType": "SetState",
161
+ "arguments": {
162
+ "entity": {
163
+ "type": "contextPath",
164
+ "path": [ "loopTarget" ]
165
+ },
166
+ "state": {
167
+ "property": "player",
168
+ "value": {
169
+ "type": "ctxPath",
170
+ "path": ["currentPlayer"]
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }]
176
+ }
177
+ },
178
+ "endIf": [{
179
+ "conditions": [{
180
+ "conditionType": "Evaluate",
181
+ "expression": "passCount == numPlayers",
182
+ "arguments": {
183
+ "passCount": {
184
+ "type": "gamePath",
185
+ "path": ["_meta", "passedPlayers", "length"]
186
+ },
187
+ "numPlayers": {
188
+ "type": "ctxPath",
189
+ "path": ["playOrder", "length"]
190
+ }
191
+ }
192
+ }],
193
+ "result": {
194
+ "winners": {
195
+ "type": "mapMax",
196
+ "targets": {
197
+ "type": "ctxPath",
198
+ "path": ["playOrder"]
199
+ },
200
+ "mapping": {
201
+ "type": "count",
202
+ "conditions": [{
203
+ "conditionType": "Is",
204
+ "matcher": {
205
+ "player": {
206
+ "type": "contextPath",
207
+ "path": [ "loopTarget" ]
208
+ }
209
+ }
210
+ }]
211
+ }
212
+ }
213
+ }
214
+ }]
215
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "entities": [
3
+ {
4
+ "name": "mainGrid",
5
+ "entityType": "Grid",
6
+ "width": 3,
7
+ "height": 3
8
+ }
9
+ ],
10
+ "numPlayers": 2,
11
+ "moves": {
12
+ "placePlayerMarker": {
13
+ "moveType": "PlaceNew",
14
+ "entity": {
15
+ "conditions": "isCurrentPlayer"
16
+ },
17
+ "arguments": {
18
+ "destination": {
19
+ "conditions": [
20
+ { "entityType": "Space" },
21
+ "isEmpty"
22
+ ],
23
+ "playerChoice": true
24
+ }
25
+ }
26
+ }
27
+ },
28
+ "endIf": [
29
+ {
30
+ "conditions": [{
31
+ "conditionType": "HasLine",
32
+ "target": "mainGrid",
33
+ "sequence": [
34
+ {
35
+ "minCount": 3,
36
+ "conditions": [{
37
+ "conditionType": "ContainsSame",
38
+ "properties": ["player"]
39
+ }]
40
+ }
41
+ ]
42
+ }],
43
+ "result": { "winner": "ownerOfFirstResultEntity" }
44
+ },
45
+ {
46
+ "conditions": [{
47
+ "conditionType": "IsFull",
48
+ "target": "mainGrid"
49
+ }],
50
+ "result": { "draw": true }
51
+ }
52
+ ]
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "board-game-engine-react",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "React library for using board-game-engine",
5
5
  "main": "dist/board-game-engine-react.js",
6
6
  "scripts": {
@@ -8,9 +8,15 @@
8
8
  "test": "jest --verbose false",
9
9
  "test:coverage": "jest --coverage --verbose false",
10
10
  "build": "rollup -c",
11
+ "build:coverage": "cross-env COVERAGE=1 rollup -c",
11
12
  "storybook": "storybook dev -p 6006",
12
13
  "prepare": "npm run build",
13
- "build-storybook": "storybook build"
14
+ "build-storybook": "storybook build",
15
+ "test:e2e": "playwright test",
16
+ "test:e2e:install": "playwright install firefox",
17
+ "test:e2e:coverage": "npm run build:coverage && cross-env COVERAGE=1 playwright test && nyc merge .nyc_output/run .nyc_output/out.json && nyc report --reporter=html --reporter=text",
18
+ "serve:harness": "vite --config vite.harness.config.js",
19
+ "copy-games": "node -e \"const fs=require('fs');const path=require('path');const src=path.join('node_modules','board-game-engine','examples');const dest=path.join('test-app','public','games');fs.mkdirSync(dest,{recursive:true});fs.readdirSync(src).filter(f=>f.endsWith('.json')).forEach(f=>fs.copyFileSync(path.join(src,f),path.join(dest,f)))\""
14
20
  },
15
21
  "repository": {
16
22
  "type": "git",
@@ -52,6 +58,8 @@
52
58
  "@testing-library/user-event": "^13.5.0",
53
59
  "assert": "^2.0.0",
54
60
  "babel-jest": "^27.4.4",
61
+ "babel-plugin-istanbul": "^6.1.1",
62
+ "cross-env": "^7.0.3",
55
63
  "babel-loader": "^8.3.0",
56
64
  "eslint": "",
57
65
  "eslint-config-standard": "",
@@ -63,8 +71,10 @@
63
71
  "install": "^0.13.0",
64
72
  "jest": "^27.4.3",
65
73
  "markdown-to-jsx": "",
74
+ "nyc": "^15.1.0",
66
75
  "node-sass": "^7.0.1",
67
76
  "npm": "^8.3.1",
77
+ "@playwright/test": "^1.49.0",
68
78
  "prop-types": "^15.8.1",
69
79
  "raw-loader": "",
70
80
  "react-dom": "^18.2.0",
@@ -76,7 +86,9 @@
76
86
  "rollup-plugin-terser": "",
77
87
  "sass-loader": "^12.4.0",
78
88
  "source-loader": "",
79
- "storybook": "^8.4.6"
89
+ "storybook": "^8.4.6",
90
+ "vite": "^5.0.0",
91
+ "@vitejs/plugin-react": "^4.2.1"
80
92
  },
81
93
  "dependencies": {
82
94
  "board-game-engine": "^0.0.9",
@@ -0,0 +1,23 @@
1
+ const { devices } = require('@playwright/test')
2
+
3
+ const PORT = 5174
4
+
5
+ module.exports = {
6
+ testDir: 'e2e',
7
+ fullyParallel: false,
8
+ forbidOnly: !!process.env.CI,
9
+ retries: process.env.CI ? 2 : 0,
10
+ workers: 1,
11
+ reporter: 'html',
12
+ use: {
13
+ baseURL: `http://localhost:${PORT}`,
14
+ trace: 'on-first-retry',
15
+ viewport: { width: 800, height: 600 },
16
+ },
17
+ projects: [{ name: 'firefox', use: { ...devices['Desktop Firefox'] } }],
18
+ webServer: {
19
+ command: `npx vite --config vite.harness.config.js`,
20
+ url: `http://localhost:${PORT}`,
21
+ reuseExistingServer: !process.env.CI,
22
+ },
23
+ }
@@ -3,12 +3,19 @@ import { useGame } from "../../contexts/game-context.js";
3
3
  import Grid from '../board/grid.js'
4
4
  import Space from "../space/space.js";
5
5
 
6
+ function sameEntity (a, b) {
7
+ const idA = a.entityId ?? a.attributes?.entityId
8
+ const idB = b.entityId ?? b.attributes?.entityId
9
+ return idA != null && idB != null && idA === idB
10
+ }
11
+
6
12
  export default function Entity ({ entity }) {
7
13
  const { clickTarget, allClickable } = useGame()
8
- const isClickable = allClickable.has(entity)
14
+ const isClickable = [...allClickable].some(c => sameEntity(c, entity))
9
15
  const attributes = entity.attributes
16
+ const entityType = attributes.entityType ?? attributes.type
10
17
 
11
- switch (attributes.type) {
18
+ switch (entityType) {
12
19
  case 'Grid':
13
20
  return <Grid grid={entity} isClickable={isClickable} />
14
21
  case 'Space':
@@ -24,7 +31,7 @@ export default function Entity ({ entity }) {
24
31
  className={[
25
32
  'entity',
26
33
  attributes.player && `player-${attributes.player}`,
27
- allClickable.has(entity) && 'entity--clickable',
34
+ isClickable && 'entity--clickable',
28
35
  ].filter(Boolean).join(' ')}
29
36
  >
30
37
  {entity.rule.displayProperties?.map((property, i) => (
@@ -5,6 +5,7 @@ import GameStatus from '../game-status/game-status.js'
5
5
  import { GameProvider } from "../../contexts/game-context.js";
6
6
 
7
7
  export default function Game ({ gameConnection, loading, isSpectator }) {
8
+ console.log('gameConnection?.getState?.()', gameConnection?.getState?.())
8
9
  const G = gameConnection?.getState?.().state.G
9
10
 
10
11
  return G
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Board Game Engine React – E2E harness</title>
7
+ <style>
8
+ html, body { margin: 0; min-height: 100vh; font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e5e7eb; }
9
+ #root { padding: 1rem 1.5rem 2rem; }
10
+ .harness__controls { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; margin-bottom: 1rem; }
11
+ .harness__controls label { font-size: 0.9rem; }
12
+ .harness__controls select, .harness__controls input, .harness__controls button {
13
+ background: #1e293b; color: #e5e7eb; border: 1px solid #334155; border-radius: 6px; padding: 0.4rem 0.75rem; font-size: 0.9rem;
14
+ }
15
+ .harness__controls button { cursor: pointer; background: #16a34a; color: #fff; border: none; font-weight: 600; }
16
+ .harness__controls button:hover { filter: brightness(1.1); }
17
+ .harness__controls button:disabled { opacity: 0.6; cursor: not-allowed; }
18
+ .harness__game { min-height: 200px; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <div id="root"></div>
23
+ <script type="module" src="./main.jsx"></script>
24
+ </body>
25
+ </html>
@@ -0,0 +1,101 @@
1
+ import React, { useState, useCallback } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import '../dist/board-game-engine-react.css'
4
+ import { Game, useGameserverConnection } from 'board-game-engine-react'
5
+
6
+ const GAME_OPTIONS = [
7
+ { value: 'tic-tac-toe', label: 'Tic Tac Toe' },
8
+ { value: 'connect-four', label: 'Connect Four' },
9
+ { value: 'reversi', label: 'Reversi' },
10
+ { value: 'eights', label: 'Crazy Eights' },
11
+ { value: 'checkers', label: 'Checkers' },
12
+ ]
13
+
14
+ function Harness () {
15
+ const [gameKey, setGameKey] = useState('tic-tac-toe')
16
+ const [numPlayers, setNumPlayers] = useState(2)
17
+ const [gameRules, setGameRules] = useState(null)
18
+ const [loading, setLoading] = useState(false)
19
+ const [error, setError] = useState(null)
20
+
21
+ const startGame = useCallback(async () => {
22
+ setError(null)
23
+ setLoading(true)
24
+ try {
25
+ const res = await fetch(`/games/${gameKey}.json`)
26
+ if (!res.ok) throw new Error(res.statusText)
27
+ const rules = await res.json()
28
+ const n = Number(numPlayers) || rules.numPlayers || rules.maxPlayers || 2
29
+ setGameRules(JSON.stringify(rules))
30
+ setNumPlayers(n)
31
+ } catch (err) {
32
+ setError(err.message || String(err))
33
+ setGameRules(null)
34
+ } finally {
35
+ setLoading(false)
36
+ }
37
+ }, [gameKey, numPlayers])
38
+
39
+ const connection = useGameserverConnection({
40
+ singlePlayer: true,
41
+ gameRules: gameRules || '',
42
+ gameName: gameKey,
43
+ numPlayers,
44
+ enabled: !!gameRules,
45
+ })
46
+
47
+ const hasState = connection?.getState?.()
48
+
49
+ return (
50
+ <div className="harness" data-testid="harness">
51
+ <h1 data-testid="harness-title">Board Game Engine React</h1>
52
+ <div className="harness__controls">
53
+ <label>
54
+ Game
55
+ <select
56
+ id="game-select"
57
+ data-testid="game-select"
58
+ value={gameKey}
59
+ onChange={(e) => { setGameKey(e.target.value); setGameRules(null) }}
60
+ >
61
+ {GAME_OPTIONS.map((opt) => (
62
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
63
+ ))}
64
+ </select>
65
+ </label>
66
+ <label>
67
+ Players
68
+ <input
69
+ id="players-input"
70
+ data-testid="players-input"
71
+ type="number"
72
+ min={1}
73
+ max={6}
74
+ value={numPlayers}
75
+ onChange={(e) => setNumPlayers(Number(e.target.value) || 2)}
76
+ />
77
+ </label>
78
+ <button
79
+ id="start-btn"
80
+ data-testid="start-btn"
81
+ type="button"
82
+ onClick={startGame}
83
+ disabled={loading}
84
+ >
85
+ {loading ? 'Loading…' : 'Start / Reset Game'}
86
+ </button>
87
+ </div>
88
+ {error && <p data-testid="harness-error" role="alert">{error}</p>}
89
+ <div className="harness__game" data-testid="harness-game">
90
+ <Game
91
+ gameConnection={hasState ? connection : null}
92
+ loading={gameRules && !hasState ? 'Starting…' : null}
93
+ isSpectator={false}
94
+ />
95
+ </div>
96
+ </div>
97
+ )
98
+ }
99
+
100
+ const root = document.getElementById('root')
101
+ createRoot(root).render(<Harness />)