board-game-engine 0.0.7 → 0.0.9
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/dist/board-game-engine.js +61 -53
- package/dist/board-game-engine.min.js +1 -1
- package/e2e/bge.spec.js +267 -0
- package/e2e/bgio-minimal.spec.js +70 -0
- package/e2e/examples.spec.js +34 -0
- package/e2e/fixtures/bge-checkers.html +39 -0
- package/e2e/fixtures/bge-minimal.html +43 -0
- package/e2e/fixtures/bge-ttt.html +45 -0
- package/e2e/fixtures/bgio-minimal-debug.html +47 -0
- package/e2e/fixtures/bgio-minimal.html +44 -0
- package/e2e/fixtures/minimal-game.json +4 -0
- package/examples/checkers.json +793 -0
- package/examples/connect-four.json +126 -0
- package/examples/eights.json +653 -0
- package/examples/index.html +219 -0
- package/examples/reversi.json +215 -0
- package/examples/tic-tac-toe.json +53 -0
- package/package.json +8 -5
- package/playwright-report/index.html +85 -0
- package/playwright.config.cjs +23 -0
- package/src/client/client.js +38 -29
- package/src/game-factory/bank/bank.js +1 -1
- package/src/game-factory/condition/is-full-condition.js +1 -1
- package/src/game-factory/condition/would-condition.js +1 -1
- package/src/game-factory/expand-game-rules.js +7 -12
- package/src/game-factory/move/move-factory.js +1 -1
- package/src/game-factory/space-group/space-group.js +1 -1
- package/src/index.js +1 -1
- package/src/utils/get-steps.js +1 -1
- package/test-results/.last-run.json +4 -0
- package/babel.config.js +0 -6
- package/board-game-engine.test.js +0 -7
- package/jest.config.js +0 -21
- package/tic-tac-toe-verbose.json +0 -54
package/e2e/bge.spec.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoardGameEngine Client e2e tests.
|
|
3
|
+
*/
|
|
4
|
+
import { test, expect } from '@playwright/test'
|
|
5
|
+
import { readFileSync } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
|
|
8
|
+
const FIXTURES = {
|
|
9
|
+
minimal: '/e2e/fixtures/bge-minimal.html',
|
|
10
|
+
ttt: '/e2e/fixtures/bge-ttt.html',
|
|
11
|
+
checkers: '/e2e/fixtures/bge-checkers.html',
|
|
12
|
+
}
|
|
13
|
+
const MINIMAL_GAME_PATH = join(process.cwd(), 'e2e', 'fixtures', 'minimal-game.json')
|
|
14
|
+
const TTT_JSON_PATH = join(process.cwd(), 'examples', 'tic-tac-toe.json')
|
|
15
|
+
const CHECKERS_JSON_PATH = join(process.cwd(), 'examples', 'checkers.json')
|
|
16
|
+
|
|
17
|
+
test.describe('BoardGameEngine', () => {
|
|
18
|
+
test('trivial game loads and getState() returns state', async ({ page }) => {
|
|
19
|
+
test.setTimeout(20000)
|
|
20
|
+
const minimalJson = readFileSync(MINIMAL_GAME_PATH, 'utf8')
|
|
21
|
+
await page.route(/minimal-game\.json$/, (route) =>
|
|
22
|
+
route.fulfill({ contentType: 'application/json', body: minimalJson })
|
|
23
|
+
)
|
|
24
|
+
await page.goto(FIXTURES.minimal, { waitUntil: 'load' })
|
|
25
|
+
await expect(page.getByRole('heading', { name: /BoardGameEngine client – minimal game/i })).toBeVisible()
|
|
26
|
+
|
|
27
|
+
const result = await page.evaluate(async () => {
|
|
28
|
+
const client = window.__bgeMinimalClient
|
|
29
|
+
if (!client?.getState) return { error: 'no client', hasBGE: !!window.BoardGameEngine }
|
|
30
|
+
for (let i = 0; i < 50; i++) {
|
|
31
|
+
const s = client.getState()
|
|
32
|
+
const G = s?.state?.G ?? s?.G
|
|
33
|
+
if (G != null) return { gotState: true, G, currentPlayer: s?.state?.ctx?.currentPlayer ?? s?.ctx?.currentPlayer }
|
|
34
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
35
|
+
}
|
|
36
|
+
return { gotState: false, lastGetState: client.getState(), hasBGE: !!window.BoardGameEngine }
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
if (result.error) {
|
|
40
|
+
expect(result.hasBGE).toBe(true)
|
|
41
|
+
expect(result.error).toBeUndefined()
|
|
42
|
+
}
|
|
43
|
+
expect(result.gotState, result.gotState ? '' : `getState() never had .state.G`).toBe(true)
|
|
44
|
+
if (result.gotState) {
|
|
45
|
+
expect(result.G).toBeDefined()
|
|
46
|
+
expect(typeof result.G).toBe('object')
|
|
47
|
+
expect(result.currentPlayer).toBeDefined()
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('tic-tac-toe loads and getState() returns state', async ({ page }) => {
|
|
52
|
+
test.setTimeout(30000)
|
|
53
|
+
const tttJson = readFileSync(TTT_JSON_PATH, 'utf8')
|
|
54
|
+
await page.route(/tic-tac-toe\.json$/, (route) =>
|
|
55
|
+
route.fulfill({ contentType: 'application/json', body: tttJson })
|
|
56
|
+
)
|
|
57
|
+
await page.goto(FIXTURES.ttt, { waitUntil: 'load' })
|
|
58
|
+
await expect(page.getByRole('heading', { name: /BoardGameEngine – tic-tac-toe/i })).toBeVisible()
|
|
59
|
+
|
|
60
|
+
const result = await page.evaluate(async () => {
|
|
61
|
+
const client = window.__bgeTttClient
|
|
62
|
+
if (!client?.getState) return { error: 'no client' }
|
|
63
|
+
for (let i = 0; i < 100; i++) {
|
|
64
|
+
const s = client.getState()
|
|
65
|
+
if (s?.state?.G != null) return { gotState: true }
|
|
66
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
67
|
+
}
|
|
68
|
+
return { gotState: false }
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
expect(result.error).toBeUndefined()
|
|
72
|
+
expect(result.gotState).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('tic-tac-toe: getState().allClickable is a non-empty Set', async ({ page }) => {
|
|
76
|
+
test.setTimeout(20000)
|
|
77
|
+
const tttJson = readFileSync(TTT_JSON_PATH, 'utf8')
|
|
78
|
+
await page.route(/tic-tac-toe\.json$/, (route) =>
|
|
79
|
+
route.fulfill({ contentType: 'application/json', body: tttJson })
|
|
80
|
+
)
|
|
81
|
+
await page.goto(FIXTURES.ttt, { waitUntil: 'load' })
|
|
82
|
+
await expect(page.getByRole('heading', { name: /BoardGameEngine – tic-tac-toe/i })).toBeVisible()
|
|
83
|
+
|
|
84
|
+
const result = await page.evaluate(async () => {
|
|
85
|
+
const client = window.__bgeTttClient
|
|
86
|
+
if (!client?.getState) return { error: 'no client' }
|
|
87
|
+
let state = client.getState()
|
|
88
|
+
for (let i = 0; i < 60; i++) {
|
|
89
|
+
if (state?.state?.G) break
|
|
90
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
91
|
+
state = client.getState()
|
|
92
|
+
}
|
|
93
|
+
if (!state?.state?.G) return { error: 'no state' }
|
|
94
|
+
const allClickable = state.allClickable
|
|
95
|
+
return { isSet: allClickable instanceof Set, size: allClickable?.size ?? 0 }
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
expect(result.error).toBeUndefined()
|
|
99
|
+
expect(result.isSet).toBe(true)
|
|
100
|
+
expect(result.size).toBeGreaterThan(0)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('tic-tac-toe: make a move and assert state changed', async ({ page }) => {
|
|
104
|
+
test.setTimeout(20000)
|
|
105
|
+
const tttJson = readFileSync(TTT_JSON_PATH, 'utf8')
|
|
106
|
+
await page.route(/tic-tac-toe\.json$/, (route) =>
|
|
107
|
+
route.fulfill({ contentType: 'application/json', body: tttJson })
|
|
108
|
+
)
|
|
109
|
+
await page.goto(FIXTURES.ttt, { waitUntil: 'load' })
|
|
110
|
+
await expect(page.getByRole('heading', { name: /BoardGameEngine – tic-tac-toe/i })).toBeVisible()
|
|
111
|
+
|
|
112
|
+
const result = await page.evaluate(async () => {
|
|
113
|
+
const client = window.__bgeTttClient
|
|
114
|
+
if (!client?.getState) return { error: 'no client' }
|
|
115
|
+
let state = client.getState()
|
|
116
|
+
for (let i = 0; i < 60; i++) {
|
|
117
|
+
if (state?.state?.G) break
|
|
118
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
119
|
+
state = client.getState()
|
|
120
|
+
}
|
|
121
|
+
if (!state?.state?.G) return { error: 'no state' }
|
|
122
|
+
const G = state.state.G
|
|
123
|
+
const grid = G?.sharedBoard?.entities?.[0]
|
|
124
|
+
if (!grid?.spaces) return { error: 'no grid spaces' }
|
|
125
|
+
const placeIndex = grid.spaces.findIndex(s => (s.entities?.length ?? 0) === 0)
|
|
126
|
+
const emptySpace = placeIndex >= 0 ? grid.spaces[placeIndex] : null
|
|
127
|
+
if (!emptySpace) return { error: 'no empty space' }
|
|
128
|
+
const turnBefore = state.state.ctx.turn
|
|
129
|
+
const currentPlayerBefore = state.state.ctx.currentPlayer
|
|
130
|
+
if (!client.doStep) return { error: 'no doStep' }
|
|
131
|
+
client.doStep(emptySpace)
|
|
132
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
133
|
+
for (let i = 0; i < 40; i++) {
|
|
134
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
135
|
+
const next = client.getState()
|
|
136
|
+
const turnAfter = next?.state?.ctx?.turn
|
|
137
|
+
const currentPlayerAfter = next?.state?.ctx?.currentPlayer
|
|
138
|
+
if (turnAfter !== turnBefore || currentPlayerAfter !== currentPlayerBefore) {
|
|
139
|
+
const nextGrid = next?.state?.G?.sharedBoard?.entities?.[0]
|
|
140
|
+
const placedSpace = nextGrid?.spaces?.[placeIndex]
|
|
141
|
+
const spaceHasEntity = (placedSpace?.entities?.length ?? 0) >= 1
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
turnBefore,
|
|
145
|
+
turnAfter,
|
|
146
|
+
currentPlayerBefore,
|
|
147
|
+
currentPlayerAfter,
|
|
148
|
+
spaceHasEntity,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { error: 'state did not change after move', turnBefore, currentPlayerBefore }
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
expect(result.error).toBeUndefined()
|
|
156
|
+
expect(result.ok).toBe(true)
|
|
157
|
+
expect(result.turnAfter).toBeGreaterThan(result.turnBefore)
|
|
158
|
+
expect(result.currentPlayerBefore).toBe('0')
|
|
159
|
+
expect(result.currentPlayerAfter).toBe('1')
|
|
160
|
+
expect(result.spaceHasEntity).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('tic-tac-toe: play to game end and check for winner or draw', async ({ page }) => {
|
|
164
|
+
test.setTimeout(60000)
|
|
165
|
+
const tttJson = readFileSync(TTT_JSON_PATH, 'utf8')
|
|
166
|
+
await page.route(/tic-tac-toe\.json$/, (route) =>
|
|
167
|
+
route.fulfill({ contentType: 'application/json', body: tttJson })
|
|
168
|
+
)
|
|
169
|
+
await page.goto(FIXTURES.ttt, { waitUntil: 'load' })
|
|
170
|
+
await expect(page.getByRole('heading', { name: /BoardGameEngine – tic-tac-toe/i })).toBeVisible()
|
|
171
|
+
|
|
172
|
+
const result = await page.evaluate(async () => {
|
|
173
|
+
const countFilled = (grid) => grid?.spaces?.filter(s => (s?.entities?.length ?? 0) > 0).length ?? 0
|
|
174
|
+
const client = window.__bgeTttClient
|
|
175
|
+
if (!client?.getState) return { error: 'no client' }
|
|
176
|
+
let state = client.getState()
|
|
177
|
+
for (let i = 0; i < 60; i++) {
|
|
178
|
+
if (state?.state?.G) break
|
|
179
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
180
|
+
state = client.getState()
|
|
181
|
+
}
|
|
182
|
+
if (!state?.state?.G) return { error: 'no state' }
|
|
183
|
+
const grid = state.state.G?.sharedBoard?.entities?.[0]
|
|
184
|
+
if (!grid?.getSpace) return { error: 'no grid' }
|
|
185
|
+
if (!client.doStep) return { error: 'no doStep' }
|
|
186
|
+
const moveOrder = [
|
|
187
|
+
[0, 0], [1, 0], [2, 0], [0, 1], [1, 1], [0, 2], [2, 1], [2, 2], [1, 2]
|
|
188
|
+
]
|
|
189
|
+
for (let m = 0; m < moveOrder.length; m++) {
|
|
190
|
+
state = client.getState()
|
|
191
|
+
if (state?.gameover ?? state?.state?.ctx?.gameover) break
|
|
192
|
+
const G = state?.state?.G
|
|
193
|
+
const currentGrid = G?.sharedBoard?.entities?.[0]
|
|
194
|
+
if (!currentGrid?.getSpace) return { error: `no grid on move ${m + 1}` }
|
|
195
|
+
const [x, y] = moveOrder[m]
|
|
196
|
+
const space = currentGrid.getSpace([x, y])
|
|
197
|
+
if (!space) return { error: `no space at ${x},${y}` }
|
|
198
|
+
if ((space.entities?.length ?? 0) > 0) continue
|
|
199
|
+
const turnBefore = state.state.ctx.turn
|
|
200
|
+
const currentBefore = state.state.ctx.currentPlayer
|
|
201
|
+
client.doStep(space)
|
|
202
|
+
for (let poll = 0; poll < 50; poll++) {
|
|
203
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
204
|
+
state = client.getState()
|
|
205
|
+
const gameover = state?.gameover ?? state?.state?.ctx?.gameover
|
|
206
|
+
if (gameover) {
|
|
207
|
+
const g = state?.state?.G?.sharedBoard?.entities?.[0]
|
|
208
|
+
return { ok: true, gameover, winner: gameover?.winner ?? (typeof gameover === 'object' ? gameover.winner : undefined), draw: gameover?.draw, movesPlayed: m + 1, filledCount: countFilled(g) }
|
|
209
|
+
}
|
|
210
|
+
if (state?.state?.ctx?.turn !== turnBefore || state?.state?.ctx?.currentPlayer !== currentBefore) break
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (let i = 0; i < 25; i++) {
|
|
214
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
215
|
+
state = client.getState()
|
|
216
|
+
const gameover = state?.gameover ?? state?.state?.ctx?.gameover
|
|
217
|
+
if (gameover) {
|
|
218
|
+
const g = state?.state?.G?.sharedBoard?.entities?.[0]
|
|
219
|
+
return { ok: true, gameover, winner: gameover?.winner ?? (typeof gameover === 'object' ? gameover.winner : undefined), draw: gameover?.draw, movesPlayed: moveOrder.length, filledCount: countFilled(g) }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
state = client.getState()
|
|
223
|
+
const ctx = state?.state?.ctx
|
|
224
|
+
const gameover = state?.gameover ?? ctx?.gameover
|
|
225
|
+
const finalGrid = state?.state?.G?.sharedBoard?.entities?.[0]
|
|
226
|
+
return { ok: true, gameover, winner: gameover?.winner, draw: gameover?.draw, movesPlayed: moveOrder.length, lastTurn: ctx?.turn, filledCount: countFilled(finalGrid) }
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
expect(result.error).toBeUndefined()
|
|
230
|
+
expect(result.ok).toBe(true)
|
|
231
|
+
expect(result.movesPlayed).toBe(9)
|
|
232
|
+
expect(result.filledCount).toBe(9)
|
|
233
|
+
if (result.gameover) {
|
|
234
|
+
expect(result.gameover).toBeTruthy()
|
|
235
|
+
if (result.draw) expect(result.draw).toBe(true)
|
|
236
|
+
else if (result.winner != null) expect(['0', '1']).toContain(result.winner)
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('checkers: getState().allClickable is a non-empty Set', async ({ page }) => {
|
|
241
|
+
test.setTimeout(30000)
|
|
242
|
+
const checkersJson = readFileSync(CHECKERS_JSON_PATH, 'utf8')
|
|
243
|
+
await page.route(/checkers\.json$/, (route) =>
|
|
244
|
+
route.fulfill({ contentType: 'application/json', body: checkersJson })
|
|
245
|
+
)
|
|
246
|
+
await page.goto(FIXTURES.checkers, { waitUntil: 'load' })
|
|
247
|
+
await expect(page.getByRole('heading', { name: /BoardGameEngine – checkers/i })).toBeVisible()
|
|
248
|
+
|
|
249
|
+
const result = await page.evaluate(async () => {
|
|
250
|
+
const client = window.__bgeCheckersClient
|
|
251
|
+
if (!client?.getState) return { error: 'no client' }
|
|
252
|
+
let state = client.getState()
|
|
253
|
+
for (let i = 0; i < 100; i++) {
|
|
254
|
+
if (state?.state?.G) break
|
|
255
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
256
|
+
state = client.getState()
|
|
257
|
+
}
|
|
258
|
+
if (!state?.state?.G) return { error: 'no state' }
|
|
259
|
+
const allClickable = state.allClickable
|
|
260
|
+
return { isSet: allClickable instanceof Set, size: allClickable?.size ?? 0 }
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
expect(result.error).toBeUndefined()
|
|
264
|
+
expect(result.isSet).toBe(true)
|
|
265
|
+
expect(result.size).toBeGreaterThan(0)
|
|
266
|
+
})
|
|
267
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal boardgame.io client test (no BoardGameEngine).
|
|
3
|
+
* Loads a trivial game via boardgame.io only to narrow down why the client
|
|
4
|
+
* doesn't hydrate in headless Firefox.
|
|
5
|
+
*/
|
|
6
|
+
import { test, expect } from '@playwright/test'
|
|
7
|
+
|
|
8
|
+
const BGIO_MINIMAL_URL = '/e2e/fixtures/bgio-minimal.html'
|
|
9
|
+
const BGIO_MINIMAL_DEBUG_URL = '/e2e/fixtures/bgio-minimal-debug.html'
|
|
10
|
+
|
|
11
|
+
test.describe('boardgame.io minimal (no BoardGameEngine)', () => {
|
|
12
|
+
test('bgio client loads and getState() returns state in headless Firefox', async ({ page }) => {
|
|
13
|
+
test.setTimeout(20000)
|
|
14
|
+
await page.goto(BGIO_MINIMAL_URL, { waitUntil: 'load' })
|
|
15
|
+
await expect(page.getByRole('heading', { name: /boardgame\.io client only/i })).toBeVisible()
|
|
16
|
+
|
|
17
|
+
const result = await page.evaluate(async () => {
|
|
18
|
+
const client = window.__bgioMinimalClient
|
|
19
|
+
if (!client || !client.getState) return { error: 'no client', hasBoardgameIO: !!window.BoardgameIO }
|
|
20
|
+
let state = client.getState()
|
|
21
|
+
for (let i = 0; i < 50; i++) {
|
|
22
|
+
if (state && state.G !== undefined) {
|
|
23
|
+
return { gotState: true, G: state.G, currentPlayer: state.ctx?.currentPlayer }
|
|
24
|
+
}
|
|
25
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
26
|
+
state = client.getState()
|
|
27
|
+
}
|
|
28
|
+
return { gotState: false, lastGetState: state, hasBoardgameIO: !!window.BoardgameIO }
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
if (result.error) {
|
|
32
|
+
expect(result.hasBoardgameIO, 'BoardgameIO should be on window').toBe(true)
|
|
33
|
+
expect(result.error).toBeUndefined()
|
|
34
|
+
}
|
|
35
|
+
expect(result.gotState, result.gotState ? '' : `getState() never returned .G. lastGetState: ${JSON.stringify(result.lastGetState)}`).toBe(true)
|
|
36
|
+
if (result.gotState) {
|
|
37
|
+
expect(result.G).toEqual({ count: 0 })
|
|
38
|
+
expect(result.currentPlayer).toBeDefined()
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('bgio client with Debug panel loads and getState() returns state', async ({ page }) => {
|
|
43
|
+
test.setTimeout(25000)
|
|
44
|
+
await page.goto(BGIO_MINIMAL_DEBUG_URL, { waitUntil: 'load' })
|
|
45
|
+
await expect(page.getByRole('heading', { name: /boardgame\.io client only \(debug on\)/i })).toBeVisible()
|
|
46
|
+
|
|
47
|
+
const result = await page.evaluate(async () => {
|
|
48
|
+
const client = window.__bgioMinimalClient
|
|
49
|
+
if (!client || !client.getState) return { error: 'no client', hasBoardgameIO: !!window.BoardgameIO }
|
|
50
|
+
let state = client.getState()
|
|
51
|
+
for (let i = 0; i < 80; i++) {
|
|
52
|
+
if (state && state.G !== undefined) {
|
|
53
|
+
return { gotState: true, G: state.G, currentPlayer: state.ctx?.currentPlayer }
|
|
54
|
+
}
|
|
55
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
56
|
+
state = client.getState()
|
|
57
|
+
}
|
|
58
|
+
return { gotState: false, lastGetState: state, hasBoardgameIO: !!window.BoardgameIO }
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (result.error) {
|
|
62
|
+
expect(result.hasBoardgameIO, 'BoardgameIO should be on window').toBe(true)
|
|
63
|
+
expect(result.error).toBeUndefined()
|
|
64
|
+
}
|
|
65
|
+
expect(result.gotState, result.gotState ? '' : `With Debug: getState() never returned .G. lastGetState: ${JSON.stringify(result.lastGetState)}`).toBe(true)
|
|
66
|
+
if (result.gotState) {
|
|
67
|
+
expect(result.G).toEqual({ count: 0 })
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** E2E tests for examples/index.html (Board Game Engine). */
|
|
2
|
+
import { test, expect } from '@playwright/test'
|
|
3
|
+
import { readFileSync } from 'fs'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
|
|
6
|
+
const EXAMPLES_URL = '/examples/index.html'
|
|
7
|
+
const TTT_JSON_PATH = join(process.cwd(), 'examples', 'tic-tac-toe.json')
|
|
8
|
+
|
|
9
|
+
test.describe('Examples', () => {
|
|
10
|
+
test('examples page loads with tic-tac-toe selected and Start button', async ({ page }) => {
|
|
11
|
+
await page.goto(EXAMPLES_URL, { waitUntil: 'load' })
|
|
12
|
+
await expect(page.getByRole('heading', { name: /Board Game Engine/i })).toBeVisible()
|
|
13
|
+
await expect(page.locator('#game-select')).toHaveValue('tic-tac-toe')
|
|
14
|
+
await expect(page.getByRole('button', { name: /Start \/ Reset Game/i })).toBeVisible()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('can load tic-tac-toe and read game config in browser', async ({ page }) => {
|
|
18
|
+
const tttJson = readFileSync(TTT_JSON_PATH, 'utf8')
|
|
19
|
+
await page.route(/tic-tac-toe\.json$/, (route) =>
|
|
20
|
+
route.fulfill({ contentType: 'application/json', body: tttJson })
|
|
21
|
+
)
|
|
22
|
+
await page.goto(EXAMPLES_URL, { waitUntil: 'load' })
|
|
23
|
+
|
|
24
|
+
const config = await page.evaluate(async () => {
|
|
25
|
+
const res = await fetch('/examples/tic-tac-toe.json')
|
|
26
|
+
return res.json()
|
|
27
|
+
})
|
|
28
|
+
expect(config.entities).toBeDefined()
|
|
29
|
+
expect(Array.isArray(config.entities)).toBe(true)
|
|
30
|
+
expect(config.moves).toBeDefined()
|
|
31
|
+
expect(config.moves.placePlayerMarker).toBeDefined()
|
|
32
|
+
expect(config.numPlayers).toBe(2)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>BoardGameEngine – checkers (debug off)</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1>BoardGameEngine – checkers</h1>
|
|
9
|
+
<pre id="out">(waiting…)</pre>
|
|
10
|
+
<script src="/dist/board-game-engine.js"></script>
|
|
11
|
+
<script>
|
|
12
|
+
(async function () {
|
|
13
|
+
const out = document.getElementById('out');
|
|
14
|
+
if (!window.BoardGameEngine || !window.BoardGameEngine.Client) {
|
|
15
|
+
out.textContent = 'BoardGameEngine.Client not found';
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let gameRules;
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch('/examples/checkers.json');
|
|
21
|
+
const rules = await res.json();
|
|
22
|
+
gameRules = JSON.stringify(rules);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
out.textContent = 'Fetch failed: ' + (e && e.message);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const client = new window.BoardGameEngine.Client({
|
|
28
|
+
gameRules,
|
|
29
|
+
gameName: 'checkers',
|
|
30
|
+
numPlayers: 2,
|
|
31
|
+
debug: false,
|
|
32
|
+
});
|
|
33
|
+
window.__bgeCheckersClient = client;
|
|
34
|
+
const connected = client.connect();
|
|
35
|
+
out.textContent = connected ? 'Client connected.' : 'Connect returned falsy.';
|
|
36
|
+
})();
|
|
37
|
+
</script>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>BoardGameEngine client minimal (trivial game)</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1>BoardGameEngine client – minimal game</h1>
|
|
9
|
+
<pre id="out">(waiting…)</pre>
|
|
10
|
+
<script src="/dist/board-game-engine.js"></script>
|
|
11
|
+
<script>
|
|
12
|
+
(async function () {
|
|
13
|
+
const out = document.getElementById('out');
|
|
14
|
+
if (!window.BoardGameEngine || !window.BoardGameEngine.Client) {
|
|
15
|
+
out.textContent = 'BoardGameEngine.Client not found';
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let gameRules;
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch('/e2e/fixtures/minimal-game.json');
|
|
21
|
+
const rules = await res.json();
|
|
22
|
+
gameRules = JSON.stringify(rules);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
out.textContent = 'Fetch failed: ' + (e && e.message);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const client = new window.BoardGameEngine.Client({
|
|
28
|
+
gameRules,
|
|
29
|
+
gameName: 'minimal',
|
|
30
|
+
numPlayers: 2,
|
|
31
|
+
debug: false,
|
|
32
|
+
onClientUpdate: () => {
|
|
33
|
+
const s = client.getState();
|
|
34
|
+
if (s && s.state) window.__bgeMinimalState = s;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const connected = client.connect();
|
|
38
|
+
window.__bgeMinimalClient = connected || client;
|
|
39
|
+
out.textContent = connected ? 'Client connected.' : 'Connect returned falsy.';
|
|
40
|
+
})();
|
|
41
|
+
</script>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>BoardGameEngine – tic-tac-toe (debug off)</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1>BoardGameEngine – tic-tac-toe</h1>
|
|
9
|
+
<pre id="out">(waiting…)</pre>
|
|
10
|
+
<script src="/dist/board-game-engine.js"></script>
|
|
11
|
+
<script>
|
|
12
|
+
(async function () {
|
|
13
|
+
const out = document.getElementById('out');
|
|
14
|
+
if (!window.BoardGameEngine || !window.BoardGameEngine.Client) {
|
|
15
|
+
out.textContent = 'BoardGameEngine.Client not found';
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let gameRules;
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch('/examples/tic-tac-toe.json');
|
|
21
|
+
const rules = await res.json();
|
|
22
|
+
gameRules = JSON.stringify(rules);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
out.textContent = 'Fetch failed: ' + (e && e.message);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
window.__bgeTttGameRulesFetched = true;
|
|
28
|
+
const client = new window.BoardGameEngine.Client({
|
|
29
|
+
gameRules,
|
|
30
|
+
gameName: 'tic-tac-toe',
|
|
31
|
+
numPlayers: 2,
|
|
32
|
+
debug: false,
|
|
33
|
+
onClientUpdate: () => {
|
|
34
|
+
const s = client.getState();
|
|
35
|
+
if (s && s.state) window.__bgeTttState = s;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
window.__bgeTttClient = client;
|
|
39
|
+
const connected = client.connect();
|
|
40
|
+
window.__bgeTttConnected = !!connected;
|
|
41
|
+
out.textContent = connected ? 'Client connected.' : 'Connect returned falsy.';
|
|
42
|
+
})();
|
|
43
|
+
</script>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>boardgame.io minimal with Debug</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1>boardgame.io client only (debug on)</h1>
|
|
9
|
+
<pre id="out">(waiting…)</pre>
|
|
10
|
+
<div id="debug-root"></div>
|
|
11
|
+
<script src="/node_modules/@mnbroatch/boardgame.io/dist/boardgameio.min.js"></script>
|
|
12
|
+
<script>
|
|
13
|
+
(function () {
|
|
14
|
+
const out = document.getElementById('out');
|
|
15
|
+
if (!window.BoardgameIO || !window.BoardgameIO.Client) {
|
|
16
|
+
out.textContent = 'BoardgameIO.Client not found';
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const Client = window.BoardgameIO.Client;
|
|
20
|
+
const game = {
|
|
21
|
+
name: 'minimal',
|
|
22
|
+
setup: () => ({ count: 0 }),
|
|
23
|
+
moves: {
|
|
24
|
+
inc: (G) => ({ ...G, count: G.count + 1 }),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const debugRoot = document.getElementById('debug-root');
|
|
28
|
+
const client = Client({
|
|
29
|
+
game,
|
|
30
|
+
numPlayers: 2,
|
|
31
|
+
debug: window.BoardgameIO.Debug ? { impl: window.BoardgameIO.Debug, collapseOnLoad: true } : true,
|
|
32
|
+
root: debugRoot,
|
|
33
|
+
});
|
|
34
|
+
client.subscribe(() => {
|
|
35
|
+
const state = client.getState();
|
|
36
|
+
if (state && state.G !== undefined) {
|
|
37
|
+
window.__bgioMinimalState = state;
|
|
38
|
+
out.textContent = 'State: ' + JSON.stringify({ G: state.G, ctx: state.ctx });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
client.start();
|
|
42
|
+
window.__bgioMinimalClient = client;
|
|
43
|
+
out.textContent = 'Client started (debug on).';
|
|
44
|
+
})();
|
|
45
|
+
</script>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>boardgame.io minimal (no BoardGameEngine)</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1>boardgame.io client only</h1>
|
|
9
|
+
<pre id="out">(waiting…)</pre>
|
|
10
|
+
<script src="/node_modules/@mnbroatch/boardgame.io/dist/boardgameio.min.js"></script>
|
|
11
|
+
<script>
|
|
12
|
+
(function () {
|
|
13
|
+
const out = document.getElementById('out');
|
|
14
|
+
if (!window.BoardgameIO || !window.BoardgameIO.Client) {
|
|
15
|
+
out.textContent = 'BoardgameIO.Client not found';
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const Client = window.BoardgameIO.Client;
|
|
19
|
+
const game = {
|
|
20
|
+
name: 'minimal',
|
|
21
|
+
setup: () => ({ count: 0 }),
|
|
22
|
+
moves: {
|
|
23
|
+
inc: (G) => ({ ...G, count: G.count + 1 }),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const client = Client({
|
|
27
|
+
game,
|
|
28
|
+
numPlayers: 2,
|
|
29
|
+
debug: false,
|
|
30
|
+
});
|
|
31
|
+
client.subscribe(() => {
|
|
32
|
+
const state = client.getState();
|
|
33
|
+
if (state && state.G !== undefined) {
|
|
34
|
+
window.__bgioMinimalState = state;
|
|
35
|
+
out.textContent = 'State: ' + JSON.stringify({ G: state.G, ctx: state.ctx });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
client.start();
|
|
39
|
+
window.__bgioMinimalClient = client;
|
|
40
|
+
out.textContent = 'Client started. Poll getState() or wait for subscribe.';
|
|
41
|
+
})();
|
|
42
|
+
</script>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|