board-game-engine 0.0.11 → 1.0.3

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.
@@ -27062,7 +27062,7 @@ function createPayload(bgioState, moveRule, targets, context) {
27062
27062
  var Client2 = class {
27063
27063
  constructor(options) {
27064
27064
  this.options = options;
27065
- this.game = options.boardgameIOGame || gameFactory(JSON.parse(options.gameRules), options.gameName);
27065
+ this.game = options.boardgameIOGame || gameFactory(JSON.parse(options.gameRules));
27066
27066
  if (!options.boardgameIOGame) {
27067
27067
  this.moveBuilder = { targets: [], stepIndex: 0, eliminatedMoves: [] };
27068
27068
  this.optimisticWinner = null;
@@ -27076,18 +27076,18 @@ var Client2 = class {
27076
27076
  collapseOnLoad: true,
27077
27077
  impl: Debug
27078
27078
  },
27079
- gameId,
27080
- boardgamePlayerID,
27081
- clientToken,
27082
- singlePlayer = !clientToken
27079
+ matchID,
27080
+ playerID,
27081
+ credentials,
27082
+ multiplayer = SocketIO({ server, socketOpts: { transports: ["websocket", "polling"] } })
27083
27083
  } = this.options;
27084
27084
  try {
27085
- const clientOptions = singlePlayer ? { game: this.game, numPlayers, debug } : {
27085
+ const clientOptions = !credentials ? { game: this.game, numPlayers, debug } : {
27086
27086
  game: this.game,
27087
- multiplayer: SocketIO({ server, socketOpts: { transports: ["websocket", "polling"] } }),
27088
- matchID: gameId,
27089
- playerID: boardgamePlayerID,
27090
- credentials: clientToken,
27087
+ multiplayer,
27088
+ matchID,
27089
+ playerID,
27090
+ credentials,
27091
27091
  debug
27092
27092
  };
27093
27093
  this.client = Client(clientOptions);
@@ -27103,46 +27103,45 @@ var Client2 = class {
27103
27103
  this.options.onClientUpdate?.();
27104
27104
  }
27105
27105
  getState() {
27106
- const clientState = this.client?.getState();
27107
- if (!clientState) return {};
27106
+ const bgioState = this.client?.getState();
27107
+ if (!bgioState) return {};
27108
+ const state = this.options.boardgameIOGame ? bgioState : {
27109
+ ...bgioState,
27110
+ G: deserialize(JSON.stringify(bgioState.G), registry)
27111
+ };
27112
+ const gameover = this.optimisticWinner ?? state?.ctx?.gameover;
27113
+ const currentMoves = gameover ? [] : getCurrentMoves(state, this.client);
27108
27114
  if (this.options.boardgameIOGame) {
27109
27115
  return {
27110
- state: clientState,
27111
- gameover: clientState?.ctx?.gameover,
27112
- moves: this.client.moves
27116
+ state,
27117
+ gameover,
27118
+ moves: this.client.moves,
27119
+ currentMoves
27113
27120
  };
27114
27121
  }
27115
- const state = {
27116
- ...clientState,
27117
- G: deserialize(JSON.stringify(clientState.G), registry),
27118
- originalG: clientState.G
27119
- };
27120
- const gameover = state?.ctx?.gameover;
27121
- const moves = !gameover ? Object.entries(getCurrentMoves(state, this.client)).reduce((acc, [moveName, rawMove]) => {
27122
+ const _wrappedMoves = Object.entries(currentMoves).reduce((acc, [moveName, rawMove]) => {
27122
27123
  const move = (payload) => {
27123
27124
  this.client.moves[moveName](preparePayload(payload));
27124
27125
  };
27125
27126
  move.moveInstance = rawMove.moveInstance;
27126
27127
  return { ...acc, [moveName]: move };
27127
- }, {}) : [];
27128
- const possibleMoves = getPossibleMoves(state, moves, this.moveBuilder);
27129
- const allClickable = possibleMoves.allClickable;
27130
- const possibleMoveMeta = possibleMoves.possibleMoveMeta;
27131
- return { state, gameover, moves, allClickable, possibleMoveMeta };
27128
+ }, {});
27129
+ const { allClickable, _possibleMoveMeta } = getPossibleMoves(state, _wrappedMoves, this.moveBuilder);
27130
+ return { state, gameover, allClickable, _wrappedMoves, _possibleMoveMeta };
27132
27131
  }
27133
27132
  doStep(_target) {
27134
27133
  if (this.options.boardgameIOGame) return;
27135
- const { state, moves, possibleMoveMeta } = this.getState();
27134
+ const { state, _wrappedMoves, _possibleMoveMeta } = this.getState();
27136
27135
  const target = _target.abstract ? _target : state.G.bank.locate(_target.entityId);
27137
- const newEliminated = Object.entries(possibleMoveMeta).filter(([_2, meta]) => !hasTarget(meta.clickableForMove, target)).map(([name]) => name).concat(this.moveBuilder.eliminatedMoves);
27138
- if (newEliminated.length === Object.keys(moves).length) {
27136
+ const newEliminated = Object.entries(_possibleMoveMeta).filter(([_2, meta]) => !hasTarget(meta.clickableForMove, target)).map(([name]) => name).concat(this.moveBuilder.eliminatedMoves);
27137
+ if (newEliminated.length === Object.keys(_wrappedMoves).length) {
27139
27138
  console.error("invalid move with target:", target?.rule);
27140
27139
  return;
27141
27140
  }
27142
- const remainingMoveEntries = Object.entries(possibleMoveMeta).filter(([name]) => !newEliminated.includes(name));
27143
- if (isMoveCompleted(state, moves, remainingMoveEntries, this.moveBuilder.stepIndex)) {
27141
+ const remainingMoveEntries = Object.entries(_possibleMoveMeta).filter(([name]) => !newEliminated.includes(name));
27142
+ if (isMoveCompleted(state, _wrappedMoves, remainingMoveEntries, this.moveBuilder.stepIndex)) {
27144
27143
  const [moveName] = remainingMoveEntries[0];
27145
- const move = moves[moveName];
27144
+ const move = _wrappedMoves[moveName];
27146
27145
  const payload = createPayload(
27147
27146
  state,
27148
27147
  move.moveInstance.rule,
@@ -27185,7 +27184,7 @@ function hasTarget(clickableSet, target) {
27185
27184
  }
27186
27185
  function getPossibleMoves(bgioState, moves, moveBuilder) {
27187
27186
  const { eliminatedMoves, stepIndex } = moveBuilder;
27188
- const possibleMoveMeta = {};
27187
+ const _possibleMoveMeta = {};
27189
27188
  const allClickable = /* @__PURE__ */ new Set();
27190
27189
  Object.entries(moves).filter(([moveName]) => !eliminatedMoves.includes(moveName)).forEach(([moveName, move]) => {
27191
27190
  const moveRule = resolveProperties(bgioState, { ...move.moveInstance.rule, moveName });
@@ -27203,10 +27202,10 @@ function getPossibleMoves(bgioState, moves, moveBuilder) {
27203
27202
  const clickableForMove = new Set(
27204
27203
  moveIsAllowed && moveSteps?.[stepIndex]?.getClickable(context) || []
27205
27204
  );
27206
- possibleMoveMeta[moveName] = { clickableForMove };
27205
+ _possibleMoveMeta[moveName] = { clickableForMove };
27207
27206
  clickableForMove.forEach((entity) => allClickable.add(entity));
27208
27207
  });
27209
- return { possibleMoveMeta, allClickable };
27208
+ return { _possibleMoveMeta, allClickable };
27210
27209
  }
27211
27210
  function isMoveCompleted(state, moves, remainingMoveEntries, stepIndex) {
27212
27211
  return remainingMoveEntries.length === 1 && getSteps(state, moves[remainingMoveEntries[0][0]].moveInstance.rule).length === stepIndex + 1;
package/e2e/coverage.js CHANGED
@@ -7,8 +7,41 @@ import { test as base } from '@playwright/test'
7
7
  import { mkdirSync, writeFileSync } from 'fs'
8
8
  import { join } from 'path'
9
9
 
10
+ /** JSON.stringify with circular reference guard; returns a string safe to log. */
11
+ function safeStringify (value, seen = new WeakSet()) {
12
+ if (value === null) return 'null'
13
+ if (value === undefined) return 'undefined'
14
+ const t = typeof value
15
+ if (t !== 'object') return String(value)
16
+ if (seen.has(value)) return '[Circular]'
17
+ seen.add(value)
18
+ try {
19
+ if (Array.isArray(value)) {
20
+ return '[' + value.map((v) => safeStringify(v, seen)).join(', ') + ']'
21
+ }
22
+ const entries = Object.entries(value).map(([k, v]) => JSON.stringify(k) + ': ' + safeStringify(v, seen))
23
+ return '{ ' + entries.join(', ') + ' }'
24
+ } finally {
25
+ seen.delete(value)
26
+ }
27
+ }
28
+
10
29
  export const test = base.extend({
11
30
  page: async ({ page }, use, testInfo) => {
31
+ if (process.env.DEBUG_CONSOLE && page) {
32
+ page.on('console', async (msg) => {
33
+ const type = msg.type()
34
+ const args = msg.args()
35
+ const values = await Promise.all(
36
+ args.map((arg) => arg.jsonValue().catch(() => '[Unserializable]'))
37
+ )
38
+ const formatted = values.map((v) => {
39
+ if (v !== null && typeof v === 'object') return safeStringify(v)
40
+ return v
41
+ })
42
+ console.log(`[browser ${type}]`, ...formatted)
43
+ })
44
+ }
12
45
  await use(page)
13
46
  if (!process.env.COVERAGE || !page) return
14
47
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "board-game-engine",
3
- "version": "0.0.11",
3
+ "version": "1.0.3",
4
4
  "description": "wip board game engine",
5
5
  "main": "dist/board-game-engine.cjs",
6
6
  "module": "dist/board-game-engine.mjs",
@@ -15,6 +15,7 @@
15
15
  "test: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",
16
16
  "build:coverage": "cross-env COVERAGE=1 node scripts/build.mjs",
17
17
  "test:e2e": "playwright test",
18
+ "test:e2e:debug": "cross-env DEBUG_CONSOLE=1 playwright test",
18
19
  "test:e2e:install": "playwright install firefox",
19
20
  "serve": "http-server . -p 5174 -c-1",
20
21
  "lint": "eslint .",
@@ -82,4 +82,4 @@ Error generating stack: `+a.message+`
82
82
  <div id='root'></div>
83
83
  </body>
84
84
  </html>
85
- <script id="playwrightReportBase64" type="application/zip">data:application/zip;base64,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</script>
85
+ <script id="playwrightReportBase64" type="application/zip">data:application/zip;base64,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</script>
@@ -16,7 +16,7 @@ export class Client {
16
16
  constructor (options) {
17
17
  this.options = options
18
18
  this.game = options.boardgameIOGame
19
- || gameFactory(JSON.parse(options.gameRules), options.gameName)
19
+ || gameFactory(JSON.parse(options.gameRules))
20
20
 
21
21
  if (!options.boardgameIOGame) {
22
22
  this.moveBuilder = { targets: [], stepIndex: 0, eliminatedMoves: [] }
@@ -32,21 +32,21 @@ export class Client {
32
32
  collapseOnLoad: true,
33
33
  impl: Debug,
34
34
  },
35
- gameId,
36
- boardgamePlayerID,
37
- clientToken,
38
- singlePlayer = !clientToken,
35
+ matchID,
36
+ playerID,
37
+ credentials,
38
+ multiplayer = SocketIO({ server, socketOpts: { transports: ['websocket', 'polling'] } }),
39
39
  } = this.options
40
40
 
41
41
  try {
42
- const clientOptions = singlePlayer
42
+ const clientOptions = !credentials
43
43
  ? { game: this.game, numPlayers, debug }
44
44
  : {
45
45
  game: this.game,
46
- multiplayer: SocketIO({ server, socketOpts: { transports: ['websocket', 'polling'] } }),
47
- matchID: gameId,
48
- playerID: boardgamePlayerID,
49
- credentials: clientToken,
46
+ multiplayer,
47
+ matchID,
48
+ playerID,
49
+ credentials,
50
50
  debug,
51
51
  }
52
52
 
@@ -65,67 +65,70 @@ export class Client {
65
65
  }
66
66
 
67
67
  getState () {
68
- const clientState = this.client?.getState()
69
- if (!clientState) return {}
68
+ const bgioState = this.client?.getState()
69
+ if (!bgioState) return {}
70
+
71
+ const state = this.options.boardgameIOGame
72
+ ? bgioState
73
+ : {
74
+ ...bgioState,
75
+ G: deserialize(JSON.stringify(bgioState.G), registry),
76
+ }
77
+
78
+ const gameover = this.optimisticWinner ?? state?.ctx?.gameover
79
+
80
+ const currentMoves = gameover
81
+ ? []
82
+ : getCurrentMoves(state, this.client)
70
83
 
71
84
  if (this.options.boardgameIOGame) {
72
85
  return {
73
- state: clientState,
74
- gameover: clientState?.ctx?.gameover,
86
+ state,
87
+ gameover,
75
88
  moves: this.client.moves,
89
+ currentMoves
76
90
  }
77
91
  }
78
92
 
79
- const state = {
80
- ...clientState,
81
- G: deserialize(JSON.stringify(clientState.G), registry),
82
- originalG: clientState.G,
83
- }
84
-
85
- const gameover = state?.ctx?.gameover
86
-
87
- const moves = !gameover
88
- ? Object.entries(getCurrentMoves(state, this.client)).reduce((acc, [moveName, rawMove]) => {
93
+ const _wrappedMoves = Object.entries(currentMoves)
94
+ .reduce((acc, [moveName, rawMove]) => {
89
95
  const move = (payload) => {
90
96
  this.client.moves[moveName](preparePayload(payload))
91
97
  }
92
98
  move.moveInstance = rawMove.moveInstance
93
99
  return { ...acc, [moveName]: move }
94
100
  }, {})
95
- : []
96
101
 
97
- const possibleMoves = getPossibleMoves(state, moves, this.moveBuilder)
98
- const allClickable = possibleMoves.allClickable
99
- const possibleMoveMeta = possibleMoves.possibleMoveMeta
102
+ const { allClickable, _possibleMoveMeta } = getPossibleMoves(state, _wrappedMoves, this.moveBuilder)
100
103
 
101
- return { state, gameover, moves, allClickable, possibleMoveMeta }
104
+ return { state, gameover, allClickable, _wrappedMoves, _possibleMoveMeta }
102
105
  }
103
106
 
104
107
  doStep (_target) {
105
108
  if (this.options.boardgameIOGame) return
106
109
 
107
- const { state, moves, possibleMoveMeta } = this.getState()
110
+ const { state, _wrappedMoves, _possibleMoveMeta } = this.getState()
108
111
 
109
112
  const target = _target.abstract
110
113
  ? _target
111
114
  : state.G.bank.locate(_target.entityId)
112
115
 
113
- const newEliminated = Object.entries(possibleMoveMeta)
116
+ const newEliminated = Object.entries(_possibleMoveMeta)
114
117
  .filter(([_, meta]) => !hasTarget(meta.clickableForMove, target))
115
118
  .map(([name]) => name)
116
119
  .concat(this.moveBuilder.eliminatedMoves);
117
120
 
118
- if (newEliminated.length === Object.keys(moves).length) {
121
+ if (newEliminated.length === Object.keys(_wrappedMoves).length) {
119
122
  console.error('invalid move with target:', target?.rule);
120
123
  return;
121
124
  }
122
125
 
123
- const remainingMoveEntries = Object.entries(possibleMoveMeta)
126
+ const remainingMoveEntries = Object.entries(_possibleMoveMeta)
124
127
  .filter(([name]) => !newEliminated.includes(name))
125
128
 
126
- if (isMoveCompleted(state, moves, remainingMoveEntries, this.moveBuilder.stepIndex)) {
129
+ if (isMoveCompleted(state, _wrappedMoves, remainingMoveEntries, this.moveBuilder.stepIndex)) {
127
130
  const [moveName] = remainingMoveEntries[0]
128
- const move = moves[moveName]
131
+ const move = _wrappedMoves[moveName]
129
132
  const payload = createPayload(
130
133
  state,
131
134
  move.moveInstance.rule,
@@ -174,7 +177,7 @@ function hasTarget(clickableSet, target) {
174
177
 
175
178
  function getPossibleMoves(bgioState, moves, moveBuilder) {
176
179
  const { eliminatedMoves, stepIndex } = moveBuilder;
177
- const possibleMoveMeta = {};
180
+ const _possibleMoveMeta = {};
178
181
  const allClickable = new Set();
179
182
 
180
183
  Object.entries(moves)
@@ -202,11 +205,11 @@ function getPossibleMoves(bgioState, moves, moveBuilder) {
202
205
  (moveIsAllowed && moveSteps?.[stepIndex]?.getClickable(context)) || []
203
206
  );
204
207
 
205
- possibleMoveMeta[moveName] = { clickableForMove };
208
+ _possibleMoveMeta[moveName] = { clickableForMove };
206
209
  clickableForMove.forEach(entity => allClickable.add(entity));
207
210
  });
208
211
 
209
- return { possibleMoveMeta, allClickable };
212
+ return { _possibleMoveMeta, allClickable };
210
213
  }
211
214
 
212
215
  function isMoveCompleted(state, moves, remainingMoveEntries, stepIndex) {