board-game-engine 0.0.11 → 1.0.4

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,19 @@ 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
+ numPlayers,
27091
27092
  debug
27092
27093
  };
27093
27094
  this.client = Client(clientOptions);
@@ -27103,46 +27104,45 @@ var Client2 = class {
27103
27104
  this.options.onClientUpdate?.();
27104
27105
  }
27105
27106
  getState() {
27106
- const clientState = this.client?.getState();
27107
- if (!clientState) return {};
27107
+ const bgioState = this.client?.getState();
27108
+ if (!bgioState) return {};
27109
+ const state = this.options.boardgameIOGame ? bgioState : {
27110
+ ...bgioState,
27111
+ G: deserialize(JSON.stringify(bgioState.G), registry)
27112
+ };
27113
+ const gameover = this.optimisticWinner ?? state?.ctx?.gameover;
27114
+ const currentMoves = gameover ? [] : getCurrentMoves(state, this.client);
27108
27115
  if (this.options.boardgameIOGame) {
27109
27116
  return {
27110
- state: clientState,
27111
- gameover: clientState?.ctx?.gameover,
27112
- moves: this.client.moves
27117
+ state,
27118
+ gameover,
27119
+ moves: this.client.moves,
27120
+ currentMoves
27113
27121
  };
27114
27122
  }
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]) => {
27123
+ const _wrappedMoves = Object.entries(currentMoves).reduce((acc, [moveName, rawMove]) => {
27122
27124
  const move = (payload) => {
27123
27125
  this.client.moves[moveName](preparePayload(payload));
27124
27126
  };
27125
27127
  move.moveInstance = rawMove.moveInstance;
27126
27128
  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 };
27129
+ }, {});
27130
+ const { allClickable, _possibleMoveMeta } = getPossibleMoves(state, _wrappedMoves, this.moveBuilder);
27131
+ return { state, gameover, allClickable, _wrappedMoves, _possibleMoveMeta };
27132
27132
  }
27133
27133
  doStep(_target) {
27134
27134
  if (this.options.boardgameIOGame) return;
27135
- const { state, moves, possibleMoveMeta } = this.getState();
27135
+ const { state, _wrappedMoves, _possibleMoveMeta } = this.getState();
27136
27136
  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) {
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(_wrappedMoves).length) {
27139
27139
  console.error("invalid move with target:", target?.rule);
27140
27140
  return;
27141
27141
  }
27142
- const remainingMoveEntries = Object.entries(possibleMoveMeta).filter(([name]) => !newEliminated.includes(name));
27143
- if (isMoveCompleted(state, moves, remainingMoveEntries, this.moveBuilder.stepIndex)) {
27142
+ const remainingMoveEntries = Object.entries(_possibleMoveMeta).filter(([name]) => !newEliminated.includes(name));
27143
+ if (isMoveCompleted(state, _wrappedMoves, remainingMoveEntries, this.moveBuilder.stepIndex)) {
27144
27144
  const [moveName] = remainingMoveEntries[0];
27145
- const move = moves[moveName];
27145
+ const move = _wrappedMoves[moveName];
27146
27146
  const payload = createPayload(
27147
27147
  state,
27148
27148
  move.moveInstance.rule,
@@ -27185,7 +27185,7 @@ function hasTarget(clickableSet, target) {
27185
27185
  }
27186
27186
  function getPossibleMoves(bgioState, moves, moveBuilder) {
27187
27187
  const { eliminatedMoves, stepIndex } = moveBuilder;
27188
- const possibleMoveMeta = {};
27188
+ const _possibleMoveMeta = {};
27189
27189
  const allClickable = /* @__PURE__ */ new Set();
27190
27190
  Object.entries(moves).filter(([moveName]) => !eliminatedMoves.includes(moveName)).forEach(([moveName, move]) => {
27191
27191
  const moveRule = resolveProperties(bgioState, { ...move.moveInstance.rule, moveName });
@@ -27203,10 +27203,10 @@ function getPossibleMoves(bgioState, moves, moveBuilder) {
27203
27203
  const clickableForMove = new Set(
27204
27204
  moveIsAllowed && moveSteps?.[stepIndex]?.getClickable(context) || []
27205
27205
  );
27206
- possibleMoveMeta[moveName] = { clickableForMove };
27206
+ _possibleMoveMeta[moveName] = { clickableForMove };
27207
27207
  clickableForMove.forEach((entity) => allClickable.add(entity));
27208
27208
  });
27209
- return { possibleMoveMeta, allClickable };
27209
+ return { _possibleMoveMeta, allClickable };
27210
27210
  }
27211
27211
  function isMoveCompleted(state, moves, remainingMoveEntries, stepIndex) {
27212
27212
  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.4",
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,UEsDBBQAAAgIAGuMbVz/k78Bxw8AAEfeAAAZAAAANDI4MGE0YzhkM2NiMzBmNjgzNWEuanNvbu1d7Y7buBV9FVV/MgN4FJESKcpAd9Gk2ewC7aLYZIuiSbpLy7RHO7Y0kOgks9MB+g59wz5JQdkT09T3ly1PlF8Ty6LJy3PJS57Dy3t94a/YD3N9qtuQmNT2yNzyZpa5wMRCVJ8kz3+ka6ZP9dmSGfEt84zfYn2icxbzWJ++u0/+yi3iimCEbYdSm7kAYtt0Fw4Ur/t8JQrlkf/RpyttSddMW4V0Hms0mGtLxt9wytnFpRYxvomCWIvF//WJfhuFvzGP7yq18CO2CD/rE30VepT7YaBP75Nap2q88gOmTwGZ6F642qwDfeo8TPT5Jtq9BhCGE50GQciTT0TrPkx0Tpe7v8IN98LkV9nnW+ZxNhfVofxan77TX4Q0mr+ma/YqWIof+jDRIxZvVjsjqT8Tcxrxt35SGjQhvjKtK2C/Nc2phaYAGRaw/qmLInh0p09N8QK73dl7Z7oXbBFGTPs+DG9E88pLxKJEqSK2jbLK/c7/zDcR097rsyj8FLPovV6leOgcFo9gZq3/QjeBd63tiq5SsIWUggHYF/xholPOqXe9ZgHffeCFm4DrU/GtG//2ls316YKuYvZQ68uTLIt4YcDZZ17JIsQmisGdTHu/jBjlTNsVXaVgZKoFw5NZ5JYuWQVzYMOE1mGtHYsUmEOUW6lUpJaKhm8LB4PDWpvFdc4Y27zwI4vokh2MbeZ+bIP4Ib/6Ez0OxP+5PtW19xvTBLN3rrnWNFf79+6/lrt+H8jPgCk/e3xi4TX7fBtGPPORFwax/ERMFV8eW+s/So9mNGb7J8b+CfvMWTC/uD+sjaZl1gasd3+B5CuZ//61+wqEa6mt279MtdHg4Ge0BJb7ak4zW03ju8CTnlzcJ69pD/sXJ9LjjdzwiWKtH4JFeCmb7BvpC/d6Y9D+SD/6S+FnPNTe688ZZM8XWxzHz2dLdrX2A39NV8Y1X68qQtpVhmmrZFCqOGFDew9q0AjU0FI6UdMuFTRBO/UVqT8/UV9G8SEGJKguQx5e7J9898M/3v7806s3KZQdvrUzdTYG7jXx4z8H3F8Vww6un4nw6Zn06EFtZW2nqeMqGkQ1bLiNoC7yTcn4i7ufwhW7kBp4zejcD5bPsgx6aLSArkvcFK2fKyGb5q18FnDtf//5r7brkyQsfe4f2PQys748fMH+7sf+bMUuLnXZ014lLdXe69JX3uvavoFfmjXZVXyqVa+a9nBZ6pyOYZnKLIlJN76J9r7pOo1886twvNE1Hl2j7YjU+b96QxyWq3vo5h/parNdoZZ6o2Mr8bvVjTc6e2+0QCNvxPnxnwadAhCrkd527ZsT6zXwcLYz74X0rhpm5cdJHaOuHmZIymxFhtt5WrbhPvnBPPyUaaBffpkt2V+3vvkyKSN/FqoQ0DmGRdTlaycgta09SAlqAlIbpgz6oNjcTkd8u1F1i8u8SSTZc8oeOCu8KHXat9kTxbOMj/NmlV+lPbCAfWSRdk3nmpFsgxmvf92/kz/qSs7Cow2TXxnCMFzLjeyiQMFfyONAha66FGunPPf4M1v4AZtX9BK37Uo+00ukwArajbyka3up/ZEOaao42f7D11UQvOuK7uKGepjD+W2UXOvuloWyQbtrvhzhhTOx7S2NIAXLjGrQdc1eoIvbRiGnhdbAO/2UoYydjgCrdIy3iSIW8L+t6B2LKnRIRyNzT/CWg+xG21HnBbAj9XkfAUk9bKfD9IcDIP5pwVlUkW0T6FN2+GEJ11aRO8goeeQORu6gEXfQhsMSOIS1cNgX0ZpRE5BNLK7CuDrN6hi2qTiaewxise6XWRSF0e57Ym24ifWpfkvjOFEnpNQMStmihPBGn4rlYdIRhVqOxRxgihDEC+RYrmvbmGJZy+F7V5x6Vzw8jpQDgTwph23bR1ByJL9SiiLgdCrkECUqGzNQ3j9s60y2StMD3IFoQZSrbnraQ+fpHcO2lNU9cEGBNSppFhzDtlX9htt07DyiLbA1xh1j3NFF3NGXZoFzXlWvIOCsjkewZH6vOis5bfUKKL1AVNdHqIiL6Zk25Zxn9/0ZaRVQerWXb78hErKCiZXirSFoFFJVqqZNcLDiiKQboglJQl/iNvLDJ+9koxsMggirP3wdxDxNdQjEVhXh3WyRYrMtA4AKYjoNmwWgHXUIeZjBalhYbLhWOoS3nJdpEH4O5jU28wly+tjMd0DbxYdjFiDVSZu8fOc82dDJH7m+2K3LsasWkJy0/qK6uKKJWKElz9kXdqCEnUbxxXHhcfRu63Swq4dQVf7zcNmCyiFIFUJl7og12FJJlTxuqYxbKiegctIIPxWVQ5Cby7k0Z3KIetzK+dqZHG9mMjKzbUQcCObuApiul83kTCUCx6AroS31buhsxTQ/1qgWhMEVW9/yO+0N4+0pHQflUTqWc4zDuZZTejbXMchWv90hpUMc5XwGcErmmFpORVS+IZMvqk3ppMsd/DFUxyCuym8hp8AaFSkd4qq7yQgP3RbEMFMHicf4Y4w/zpTSEXBWR9GyAanirERAW0qHpPes1PURSa8Gpb58CrvN/VI6JL2+zbffk9zLHgilQwyAlfUE7mbLhcC2lM7Td7LRDc6T0iFWe0qHGBCqgqVuyFRit6V0DtunNr7osNJI6eRiJuugTL7hhkTpEAParfVkWUh1SevFh1OAVDfNmD8BSsd1G7TKj98wfnQ+pz/guK2Bc1RsHLfPTgNMYKbXTeWNjP3fCwir18nGTvT2mgZfmovWZiV8Su9WhGq9MxJVcxia8g5Lo0P0p0TPaXq1MwTXA7C64GhBRWbgqSMqUpRsjVTkuBV4YiqyPg77oiIzatIBFSlKVfNVfu1UJLQIRLZnu57jMtN14cKleVQkC7jP7zQ/0FQmMhb5gyO2YBELPKbRWPquRGDusqd0kEbYtHKZSoyOwVRiVAFtW2K/M6aSGFBVawPHLp6CavmcY1bxuXpEpShWWTGUZL8bAjcHU+Qqap0ut6zUodrCtkad1BicdBGcDIKntFNRfEfCe2C6LYlKYKb3MFKrt/TSUerMp8Ch9EpUAlB0fuGrYGgGQ1QipMT4Zaqdqo4oTy2NmMqn72WjH5wnUwkOJ/mmTCUG+aF+K8+DLZlKpX1q44vo9ZGpzMVMOiHr+TCVGKikejdQRajl+gMguwCqKM0Onz9VCTKSI1TgDuia/cQWf4rfXNOIzZOBPHsqkGaPnM2m+DrcrObajGn8mmXsO4lP96/uNpyMeP/D2kXy2Gfx8/iWeizWeMTY5UQLQq79tom5NqPBjZGgiF1cHuTmy+uXimTrQJpUxedgL9kbE1n2lxUSMRs53TH9asR7TbwPJqbq8V+9AVPNYtGKgUz5JSzZ/q28yYfhmF9q3OQDJ2cg6+KwPwYyVROQ6Wt1KUgMyUhBHlCQDqImnDOwcCH1CLGIi0geBbmmN0yj2jr8yJL0lqJGEd/Oe5p3TYPlls9rSS8iN49edMAxcls6oDS3JTHwlszukF7ESIU8zmS9GjoUwrnIb0EvYjXBxOBvoCQGxqljkEV5PivSizjFDqPBZ/kkBlHX92PkMUYe50svEqjSi2ZHrAZGbelFnD7ooS54cZ278s6R+KjvLbV8BKc3DPLt9yRplcHQi656Gqvsnu7Kjojb0otP3stGPxjOVli98ctpTy+6hmkqk6CFSqLyyr5H2hKMuOCAGcBFKVlHgjEXNWm5xLkQjK5h9ZOhEIK26S0hKEhvCVPh+FMgGGEGw1/eqvDm6AchBWxay2KzYSNntrQaweaYyDhmj3WEyXqAbHKZrbhoJ2F2qh2ZKylpu4vX2cHIHqErncqwmrC7pwPTgPq524m8HtjTGwQ1Lxt8bENpf8D1M7Oz61QFpHu5CRgCKfWG3QzSAwXWKTu7r7VRPbA3UZIctL+kh+Tmg3PAurTBaDXZ1xgOpE7YuT1hux60G8mWhDzoexq/SuREp4iocS+qMwjkO7Gb5GsYDJpO1bE9YLoenjtUUWXgrKM8Dp0geOQyRy6zpYqqPg77UlGJmqiSkkyhSj0VlSgW1jq/8vRVVAtvRh3oMoAc5HmOieeek6eiul3RO0F2L4WcmAkZVTDXvGvm3WiLMBI7sAGLtDDS5hH91F5OBUGunAoghxxBT7X9mVJQObhTQZUoUdlaBk5nlwW7hkVU3yq6HreGc6XKHfwFuW7GNeTtMzaIUovy1Q/VFkjNLD+GIWMYcq6SKgFnVddodrQyhG0lVRCWSqogfOpij/reUmvxB796KclAJFWugdU7HjpKLQ9hW0nV0/ey0Q/OU1IFYSeSKgeo16tAy+rI+dpqqpQmqu0fNVWNYFNTUyX67Dt/tWLzHOtdLCN/nttW8XD/6NuSEW97lLj69xf+irPoIs779YsaZT2eZ67+xooFS36tlbwgfSiNYnISY6mEb0q/3GdNOpG+QdNw7X6YRNI2twYkBbk1YMbNC09A+kaacDvHl74J2LTfX8iGDW4rfTsqMo7ZY6eQvhGVdavSPHE0N044z3mFdqK1ewawdKT4CDaC5amAcvw+7DpGr4fYJneALJKg6aXo0+MjtpfLVCAhrRF7WtwcvydPi9uidJP+QnpU1HxB54lt+0uFQD7A5dtow6/vTopO12y73uzcXkp/uOnkmFXw96XAXPBtrd/5TkYtsLnp4wGNzCfI4csKVkm+d4ogtZfbf6ArHeux7SboPTK6zqbD+9zeq+ch6VMIssHYKmYZFathyZ3CYv/sD9nbXMFmtZIM9Gj7d4+fKSLYfXk5SeZkaeGHvC56GQac+oWi8m39G2vwsrwTlAgjqpHf3fj9SH6P5Hc7DV4DHPakwcuqSXbipToSvKRUhZU/St6lIUvwZhazLAeBhevMMcUmIlhOZJYI7FgUT+UrkdSblKgWhMEVW9/yO+0N4x2I71ycJ75DYsztXXuHtiN7CZhwp7nMkhJV6R1GxTNMLZdyUll+2kvvsso9iq615UBHUsq7ImNUUd4lparKO3vwtgAGsMbgYww+ugg++lLePc5CFeV3CaYV5YFbcmlZxZnJMmFL9Z2Vut81pb6zzKIrI3rWBT3aOhsF5yPBs8z0GeZ8Iw5VevTYG0PR3+3rU0F8B4EBLXVGdLu5tcWSb9MkzRzxq/Cyr90PhqG/qzdyHehIGonvIDBs1fWsrqZA1JIKURqotr5IFDtK73JBk6bre0tn9nLnjR3mNBOAVdeR3ZAflnySr9FCxAKkAK8gzeudv7DLgmm+p7xVfvyGVWGxu6XNBHLqXQFfFTmw7RL2uOA4cqedCplNMsXF/u8FCVrktET7QMSsBNBaecz6xKqcubFJwpCTouc0vdoZgusBWF13NE8MkoUnkMmUNNgXTJc87guO+4LHJiUFDu1BkJIZNclJXlCLlRTFKicaSi5fOHtW8sPD/wFQSwMEFAAACAgAa4xtXDhkryl/BgAArjoAABkAAABmZmMwOTFjZjhiMWU1NzY4ZmMxMS5qc29u7Vn7b9s2EP5XCGJAbMBxRL0lYBuaPrIMbTr0sQGrs5SWKUerRHoSlQc8/++DFDWiZVmWZLlNMvsnWaSOd8fveMf75tD1fHI6gTZ0XUeykOOaY0Q0QzddByE4SMfPcECgDcdTjx0GHvUC7A+jGXGGf0dwADmJeATtT/P0aa2sQ8eQZVPTiDlBsm4hyR0byQLc4/5X6cDxPUI58BmeRADTCZgS/p5jTnp9EBIehzQCUfIfeBRcEjzxSRSBV15IXHYDB3AWsr+JwzOF3fv3PnMw9xiF9jy1aL01vkcJtJE8gA7z44BC21gM4CQOs+9lSxlATCnj6YvE8PMB5HiaPbGYOyxdndzMiMPJJFEL80tof4JjhsPJFAdk6DGQrQ16lIHjZOAEB+QlnXqU9OH5AIYkiv3Ms4X1I45D/sFLl5ElWT+UlEOkfpAkW9FsGQ1VS/0TJhJ4eAttKfmAzLI9ytx9TFwWEvALY18SszdL1BKJuR7IRGViX3k3PA4JGEGHUU5u+AjWkm4VpJfq/DwkydZnkmvI1SRpWa6Ziz0fQMw5di4DQnn2wmEx5dBOLPvizWZkAm0X+xFZNJo8KHPHDE9JLV9oUtEXulHhjERuHalIKkrVH74vdENf1lqq1rkkzB12RUI8JUvRLeXRLeuL9eoPYEST/xzaEIxiSULjT5YUAGCBf7O/ihWMqDiGJHHs64iiB+RmxkJeOuQwGokjyUF6P6wEPwpDYxyRfGSYj5AbTuikN1/WBoBSbVCQPaF0Sunvr2yKLAeCrXdPUtFotLQMSGGZq2mXWo2jW+oII715+hlY5B8OhOFYNHxQ8NYpdVlfdNlPwoQ5bA3aM3zlTZM44wyM4BGRyZF7h+PoaCmDXPLAr4lpYxnT2oZDqXHuUnN0ozboRkphM0Hq4lLURSS1lcW89/WlFsiSJEn5nH4BK+qKeAET19gTI2EZR8LCU8ZZLx85Pjl9e/Hm9Oz0zbPXFx/fvV5BbAEzc5As9JFyz6+GqRwcJNXIgTC06G8ZY40iS2vgrbuSo7feaYQf375jPukJ5iVllEenB5tdRnGwIai14Oi+xhmNhnlFx6h/e+QtObFfqiJnx+R3L/LGPun1oRiKL1PjwAgKU0YQ5DbdWzLIdLVBpTZg0a8Rr4ZSyEFq8yRUHa9aHq+m1iZen1pA/a8x31n+3uLX6IDSRQ2XI/YK+zHmdepTo5gVN1SnjYPMyINMQW2CTK8o+YwKvBZru7tr3ZrqrkXckszJPeHbYmG1vjLqFm2NcGOuOK3KbVlMlbvt2qMTdl3qnouLBBdv7mDxPBWyPqvUKuAMUylcpbqFqiLkA6SYLbCqrCaExbLzldUTNjtH7+C5LkOkvZjyo7LGh8Lu/VyeGQ5KXq9LI5+F3hAlVyTMOkRkAoYnQ+DjiJ9kM2zww/zX92/PhhEPPTr13NvMzqE4q7/4LBat605qIdB4GBPxkwdwdLf6NYlbRa847DxXPHZqQKKfXM7Kw/HlPzGud6UyTHXbNkF1RArJQ1bbBGTXPitsyWr+qRPP+cuTzbhPd6M3B6npmwoi4fK3/W2pFUjLkstmjzhxGBLKf/PxLQnXnwAviOtRMqm6n2RTasJX2y18TaH2adMQeFT4+kZbv4OzvhHArdUUv4TGZy4nYe32/goEEwKkqrtfu4XbAbj3Ldx9C3e5SmiFw2ZUQjNGownZtaJJOb3js6gJ1WWYzS7Q3dA7TSeTMGRhNi/hceMI2nCGoyilSVdo1YLsRAL7Au2k7E73oZJvtnTZRVhFY9mxHFVGSLa0NXzztccvwQsyjqdghinxaxDQHZLN6lqyWZGN70o2p+tvRJ5ldEw2G5ZZyEaW1R3ZbEpyQbrWCdlsSsW+gPLwGVYTFY4NZJS6uhnbbKLiBhqPgHm3LGNfquxLlUfANh9OklxVn3O2igeqoXZ7xVS35ZzVrTlnrYpzVndOkb14efzx5Ekxz+rTYeHAaNRLQwYwOhr1HxYRvaJcHV5aHkpGodxQO+YhVIGHsNrQEE806PZx8bDI6vYHXAfMtTxESN0pc61uy1yrFcy1umeuW+DmcTLX8hDJ0k6JBl1krjWpBVb1jcy1/hSY6z/u+0022LPYu/g1iWf9oTHaSaSi3Ubqtox25z4rbMljYhy/CaOtryad9oRfCcDEZuUWXbREsrzvou27aN+Z8GuOw10RfiWamKUd9kaEXyK12b3j0RN+54v/AFBLAwQUAAAICABrjG1czpUFpDMGAADROQAAGQAAAGMwOGFlNTIxMzg1NzUwMzRhNWJiLmpzb27tW21vo0YQ/iur7YdzJJLA8mqkVrq0aXvS3el0b6p6Tqs1DA4XYC1Yco5c//cKTOL1GmzwS17u4k+OF8Yzs8/MDs/jTHEQRvDKxy72VIeCSTTdMW1T1Q1qDodYKdff0hiwi2FC43EE2Uk2Bu/ka4YVzCHjGXa/TMt3jXaOLZ8MTc9TLVsPPAdMI3CguD3kkWgZjekIUMSon6FvIb9EPPSOOfWOOQOUQQQeBx/RxEcfOE05GuacswQreJyyr+DxytEgTCFgE6zgiHmUhyzB7rSMpD6KKEwAu5qqYI9FeZxg154p2M/T6l6dOAqmScJ4+UER8IWCOR1V71jOPValaFz6WLhE+SV2v+Dz6vvwhYJTyPKoSphkPisC+hiWVohKrGNVP9aMj6rq6qZLyImm23/jwgJPb7CrFjfAuEp9lcUzCFgK6E/GroqoNlt0CosLPzTHrDP7ezjheQpogD2WcJjwAW5j3SCSdavO+K8pUA6ostzKri7ZNRZ2LxRMOafeZQwJrz7wWJ5w7GoKzq7C8Rh87AY0ymDW6WKlLh8FWtslw5RTbdWmuspGYbeV1b5s9QnkQifSBqrrfa4pYY9dQ0pH0FS9xJo1u6/gLCn+5tjFaJCrqjb80ldjhProv+pPvR8PEnFNU8W12xXdimEyZimvXfJYkokrRYO8W9bjn4WlIc1gsXKyWIEJh8TvTZe9QajWGy2u3mnlJbWvf6pLCImFWOfvVDlobelrUAnLhZtubdQ0u0k8YaU3nff02eJGRVjOxcAVKVuvkoAdiSn7RbhgircG7Vt6HY6KOuMMDfDp7YFwGiY+TE4ueRy1BLKxDOS+3hnJzYeRtoCztg2cJciiMqO9209I/GK3M/fFCnykDWwBiea93QBEJFr/RkOxzJZBKtTSiHHWW6yc//XyzbvX5x/+/fT+9aZYpqj4kk8JD6P1+CfxiyKRQnLQ7GjH4u1UsqRDpuazSq85YcDPbt6zCETUXAL1w2S0cfunKKHxhm5hxqdnjKY++oPGgM6TUZjAabiUu6Nazzg7g89hFg4j6B1hsbTPy5jQAAuXDDBahHIXgFK56KI6J9DsqEUHMDTpWNf32QGIMI7a23SA76lsfkRk7+3U3+LVqevoe9ybskRYKu7MTyMaw/H8KBK2pzGFf9Jr+EyjfGl7hVNNtNHUPe5sDDC6dWnZkVYNwpRm3Q3zeaf+oC/6g+Fs0x9++JJ6ysA9cHPoVP/GoYHUcuZsiaP5HDsYnKL3kAEvEXXwuaMKQRg76t1oOXnIhMI+G4shDB5bNZbvpqx+TGQ/5NzRrfGY0vbMlqvyZcAhbc2HGpbMLJL1dGhrymvF8jPl9Ux5bUV57Ua9dsVhNwa4izqw4olTS4dHLOuiDRiWuWzVug86vOvFkKYsra7LOOV5hl08pllWykYrMpNku7DArrDL03y+D2t1Nx8IMU0nUPVAVw2iU/BtQXfzaFIyf0ucX0H1pUB9VByzRfaDcITCBA1T9i2DdF9qm92othVK0QHVtrkQtQFJtrlntc2wLVlt09cfL53qyZaloNqzq7PaZjiqZFd7/AqT0ddkXczeXW0z+iuC5r20l91yYcka4fPo8Tx6PEW1zbLI4dQ2QnZU28gq1y49RpEubNtjZ+MPKmKRJfpgmWa5plFOeZt2bZvyebtPvAgUia5tgxe9uf2RdaSD3Oeq8ay+020BMKgy3BPulZtMW912V+agE2hkKmB92lLIOucsAO5ditzNopcJ4/PJ12yJz1nDE/4GQZiA36r92ba66znejGZHOMz726DZXkm+3DGIs3JJxZfNEVwPxoSHPISsmbOqUrhfzqoT7vpNcS18fpmm9KauPQvBhFl5VatsNKdDqNri+bAVCh8efn1ByDK2gd+9Yuup7PgBWdwuBaLLv0Nqszkxu261M/vqrdrBwK3v+qD0NOF20F1/kOczfXXCbxlR7co4oh68i+gNpG9oegXp94F2bcdWfo+4eQQb+lha9Oqj6eZMJHk8j3ZN5u+6hRmTfc4iBwQw2fHB7tGh6r439+EeAXX5Zwc7qcG2LfFNxZi6FzV4xfIzJftMyT6AGtwVh4dTg1drzajVazrKwbYt/YMGIfch2DygHnwx+x9QSwMEFAAACAgAa4xtXCnNmPDQAwAAWREAAAsAAAByZXBvcnQuanNvbs2XwW7jNhCGX4XgaReQDZEURcrHtttFL72kQA+LHEbU0NZaIg2K3iQI/O4FZSfrpFbarNPUN45AzXC+mZ8a3dMeIzQQgS7uKZi4he5PH9YYBrpgu4wOEUL8o+2RLphSoii0ZIILkdFmGyC23tEFy0Uh5lpUGbVthwNdfLkfV781dEELrnMojG6EqUVuSy0k0P3O3yH5pfUS58MGzfzrQDMacYh7F2k16WKmS1kWCqDAivGyyCureHq9jV1yGkP7rYWOLKFH0nloBgKuIUuMVxEifvhIAsZtcAMZkk0zugn+K5p4OJRtA1p/SzPaeXNIdJ/V307ctS7h0Rk1vtv2ji7U7gkfWfKMgnM+jk9SdtcZjbA8rPw2Gj9GxdsNmohNOg7EFV18oT95CM1n6PGTW6ZAaf+aLmLYYkYDDtvugAtiBLPq0Y329e56l/0TQ9uwEqTkpZVKVFVRlFAeM2zNLIKZRf8+CCWbQlgUxWUSNHWOui4KqRVnTWVZXpnTBBdH4ObQdT93rVlD3SFpBwLEeTfDfhPvyBXG81EqOYVSqAttRi40l4UpKqMqzKuK2wqmUKKLbbwjrSPPSQ5J7wEtBnQGCQxHe48KMHbs/PMbyD4Xk6RLeZmklYScN8hsxcFoLXQl9RTpHtZIgPT+G47qh2HAEPeKJ2YFbrk/9pkUZTVFUbELlb41NSheIZNKGqPysjFqiuKmgzsS/f5rhAmja4hZoVkT6wO5aZ3DQHwgTYCb83FyNomTSaUvk2ctUAglma1UU0KZS10ed+VIC8Pwvvcor8opkpKJ/wXk9TiVJfOeRh+howuVfY+RjK37buYZtR2s78bVsG43m8PTh3i7VJrHcc1ak1fMWF0zlKrU1jD2fFxr/axvXdtD9/LcdsrXzCjOtZSoG8bLiuW2VuyozMk7MV2LLv6LmSPd6iuEpsNhIL8+FvdVRT+ZzeFa4lPF59UZxa9T8dNVMG89OcQmH5wnz7ri44/q6yT4quSWQcFqbipTcMZ4JSfA37RxRX7BerskG3DY/QfT3wvUi0nqgqsLo35CjPxYjPwsMZpcA0rOhJZK5qIAWddPxYi30G86HF4W4ik/s7LhtTQmL5WwRqMsrMajfnjwTDawfBj/x744/isYsBszG1vjKv0rknobo3eva4cTWTxMV9OtcMZn7NMh3o/q6yTPBjmXUttcWJEXXAA2x/OAATdSfMIvYQsIzX4wMN7ZdpnutDr4mwHDW1FUkxRZ+b4U314v108YpTiPlE4EY+w4WrJeFy6jGIIPD3g2B2r3u4z2YFatw32mfwFQSwECPwMUAAAICABrjG1c/5O/AccPAABH3gAAGQAAAAAAAAAAAAAAtIEAAAAANDI4MGE0YzhkM2NiMzBmNjgzNWEuanNvblBLAQI/AxQAAAgIAGuMbVw4ZK8pfwYAAK46AAAZAAAAAAAAAAAAAAC0gf4PAABmZmMwOTFjZjhiMWU1NzY4ZmMxMS5qc29uUEsBAj8DFAAACAgAa4xtXM6VBaQzBgAA0TkAABkAAAAAAAAAAAAAALSBtBYAAGMwOGFlNTIxMzg1NzUwMzRhNWJiLmpzb25QSwECPwMUAAAICABrjG1cKc2Y8NADAABZEQAACwAAAAAAAAAAAAAAtIEeHQAAcmVwb3J0Lmpzb25QSwUGAAAAAAQABAAOAQAAFyEAAAAA</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,22 @@ 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
+ numPlayers,
50
51
  debug,
51
52
  }
52
53
 
@@ -65,67 +66,70 @@ export class Client {
65
66
  }
66
67
 
67
68
  getState () {
68
- const clientState = this.client?.getState()
69
- if (!clientState) return {}
69
+ const bgioState = this.client?.getState()
70
+ if (!bgioState) return {}
71
+
72
+ const state = this.options.boardgameIOGame
73
+ ? bgioState
74
+ : {
75
+ ...bgioState,
76
+ G: deserialize(JSON.stringify(bgioState.G), registry),
77
+ }
78
+
79
+ const gameover = this.optimisticWinner ?? state?.ctx?.gameover
80
+
81
+ const currentMoves = gameover
82
+ ? []
83
+ : getCurrentMoves(state, this.client)
70
84
 
71
85
  if (this.options.boardgameIOGame) {
72
86
  return {
73
- state: clientState,
74
- gameover: clientState?.ctx?.gameover,
87
+ state,
88
+ gameover,
75
89
  moves: this.client.moves,
90
+ currentMoves
76
91
  }
77
92
  }
78
93
 
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]) => {
94
+ const _wrappedMoves = Object.entries(currentMoves)
95
+ .reduce((acc, [moveName, rawMove]) => {
89
96
  const move = (payload) => {
90
97
  this.client.moves[moveName](preparePayload(payload))
91
98
  }
92
99
  move.moveInstance = rawMove.moveInstance
93
100
  return { ...acc, [moveName]: move }
94
101
  }, {})
95
- : []
96
102
 
97
- const possibleMoves = getPossibleMoves(state, moves, this.moveBuilder)
98
- const allClickable = possibleMoves.allClickable
99
- const possibleMoveMeta = possibleMoves.possibleMoveMeta
103
+ const { allClickable, _possibleMoveMeta } = getPossibleMoves(state, _wrappedMoves, this.moveBuilder)
100
104
 
101
- return { state, gameover, moves, allClickable, possibleMoveMeta }
105
+ return { state, gameover, allClickable, _wrappedMoves, _possibleMoveMeta }
102
106
  }
103
107
 
104
108
  doStep (_target) {
105
109
  if (this.options.boardgameIOGame) return
106
110
 
107
- const { state, moves, possibleMoveMeta } = this.getState()
111
+ const { state, _wrappedMoves, _possibleMoveMeta } = this.getState()
108
112
 
109
113
  const target = _target.abstract
110
114
  ? _target
111
115
  : state.G.bank.locate(_target.entityId)
112
116
 
113
- const newEliminated = Object.entries(possibleMoveMeta)
117
+ const newEliminated = Object.entries(_possibleMoveMeta)
114
118
  .filter(([_, meta]) => !hasTarget(meta.clickableForMove, target))
115
119
  .map(([name]) => name)
116
120
  .concat(this.moveBuilder.eliminatedMoves);
117
121
 
118
- if (newEliminated.length === Object.keys(moves).length) {
122
+ if (newEliminated.length === Object.keys(_wrappedMoves).length) {
119
123
  console.error('invalid move with target:', target?.rule);
120
124
  return;
121
125
  }
122
126
 
123
- const remainingMoveEntries = Object.entries(possibleMoveMeta)
127
+ const remainingMoveEntries = Object.entries(_possibleMoveMeta)
124
128
  .filter(([name]) => !newEliminated.includes(name))
125
129
 
126
- if (isMoveCompleted(state, moves, remainingMoveEntries, this.moveBuilder.stepIndex)) {
130
+ if (isMoveCompleted(state, _wrappedMoves, remainingMoveEntries, this.moveBuilder.stepIndex)) {
127
131
  const [moveName] = remainingMoveEntries[0]
128
- const move = moves[moveName]
132
+ const move = _wrappedMoves[moveName]
129
133
  const payload = createPayload(
130
134
  state,
131
135
  move.moveInstance.rule,
@@ -174,7 +178,7 @@ function hasTarget(clickableSet, target) {
174
178
 
175
179
  function getPossibleMoves(bgioState, moves, moveBuilder) {
176
180
  const { eliminatedMoves, stepIndex } = moveBuilder;
177
- const possibleMoveMeta = {};
181
+ const _possibleMoveMeta = {};
178
182
  const allClickable = new Set();
179
183
 
180
184
  Object.entries(moves)
@@ -202,11 +206,11 @@ function getPossibleMoves(bgioState, moves, moveBuilder) {
202
206
  (moveIsAllowed && moveSteps?.[stepIndex]?.getClickable(context)) || []
203
207
  );
204
208
 
205
- possibleMoveMeta[moveName] = { clickableForMove };
209
+ _possibleMoveMeta[moveName] = { clickableForMove };
206
210
  clickableForMove.forEach(entity => allClickable.add(entity));
207
211
  });
208
212
 
209
- return { possibleMoveMeta, allClickable };
213
+ return { _possibleMoveMeta, allClickable };
210
214
  }
211
215
 
212
216
  function isMoveCompleted(state, moves, remainingMoveEntries, stepIndex) {