board-game-engine-react 0.0.15 → 0.0.17

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.15",
3
+ "version": "0.0.17",
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,10 +86,12 @@
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
- "board-game-engine": "^0.0.8",
94
+ "board-game-engine": "^0.0.9",
83
95
  "react": "^18.2.0",
84
96
  "react-error-boundary": "^3.1.4",
85
97
  "react-transition-group": "^4.4.5"
@@ -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
+ }
@@ -7,8 +7,9 @@ export default function Entity ({ entity }) {
7
7
  const { clickTarget, allClickable } = useGame()
8
8
  const isClickable = allClickable.has(entity)
9
9
  const attributes = entity.attributes
10
+ const entityType = attributes.entityType ?? attributes.type
10
11
 
11
- switch (attributes.type) {
12
+ switch (entityType) {
12
13
  case 'Grid':
13
14
  return <Grid grid={entity} isClickable={isClickable} />
14
15
  case 'Space':
@@ -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 />)