From b790f19e83b89118043bcd8249c6d626cb97dc90 Mon Sep 17 00:00:00 2001 From: Damien Simonin Feugas Date: Sat, 14 Oct 2023 14:07:33 +0100 Subject: [PATCH] feat(web): introduce score tracking (#169) - fix(web): anchorable flip is ignore when loading game. - fix(web): lost meshes when decrementing a quantifiable. - fix(web): game loading screen hides game aside tabs. - feat(server)!: GraphQL game queries now return serialized preferences. - feat(server): bundles game descriptors on server start with esbuild. - feat(server): returns bundled rule engine when promoting and joining a game. - feat(server): supports multiple anchors and migrate existing games. - feat(web): initializes rule engine with bundled scripts and computes score on game init, joined player, and every action. - feat(web): displays game scores, loads serialized preferences, provides preferences to `computeScore()`. - feat(web): supports anchors with multiple snapped meshes. - feat(game-utils): supports multiple anchors. - feat(game-utils): allows testing random game code. - feat(mah-jong): supports multiple anchors and computes score. - feat(draughts): supports multiple anchors and computes score. - feat(klondike): supports multiple anchors. - feat(chess): supports multiple anchors. - feat(playground): supports multiple anchors. - chore(server): configures repositories `isProduction` option. - refactor(web): moves some types to `@tabulous/types`. - refactor(game-utils): takes `findPlayerPreferences()` from web. --- .github/workflows/CI.yml | 4 +- .gitignore | 3 +- TODO.md | 16 +- apps/game-utils/src/descriptor.js | 26 +- apps/game-utils/src/hand.js | 10 +- apps/game-utils/src/mesh.js | 15 +- apps/game-utils/src/preference.js | 18 + apps/game-utils/tests/descriptor.test.js | 79 +- apps/game-utils/tests/game.js | 56 +- apps/game-utils/tests/hand.test.js | 65 +- apps/game-utils/tests/mesh.test.js | 82 +- apps/game-utils/tests/preference.test.js | 38 +- apps/game-utils/tests/test-utils.js | 10 +- .../chess/__snapshots__/index.test.js.snap | 576 +++-- apps/games/chess/logic/builders/board.js | 3 +- .../draughts/__snapshots__/index.test.js.snap | 969 +++++--- apps/games/draughts/index.js | 11 +- apps/games/draughts/index.test.js | 107 +- apps/games/draughts/logic/builders/board.js | 27 +- apps/games/draughts/logic/constants.js | 13 +- apps/games/draughts/logic/players.js | 9 +- apps/games/draughts/logic/score.js | 46 + .../klondike/__snapshots__/index.test.js.snap | 321 ++- apps/games/klondike/logic/builders/board.js | 21 +- apps/games/klondike/logic/builders/cards.js | 14 +- apps/games/klondike/logic/players.js | 3 +- .../mah-jong/__snapshots__/index.test.js.snap | 2146 +++++++++++++---- apps/games/mah-jong/index.js | 1 + apps/games/mah-jong/index.test.js | 160 +- apps/games/mah-jong/logic/builders/boards.js | 98 +- apps/games/mah-jong/logic/builders/sticks.js | 59 +- apps/games/mah-jong/logic/constants.js | 28 +- apps/games/mah-jong/logic/player.js | 2 +- apps/games/mah-jong/logic/score.js | 39 + apps/games/playground/logic/build.js | 14 +- .../server/migrations/006-multiple-anchors.js | 27 + apps/server/nodemon.json | 8 +- apps/server/package.json | 1 + apps/server/src/graphql/games-resolver.js | 17 +- apps/server/src/graphql/games.graphql | 23 +- apps/server/src/graphql/index.d.ts | 16 +- apps/server/src/repositories/catalog-items.js | 57 +- apps/server/src/server.js | 10 +- apps/server/src/services/games.js | 8 +- .../unbuildable-games/import/index.js | 6 + .../tests/graphql/games-resolver.test.js | 92 +- .../tests/repositories/catalog-items.test.js | 75 +- apps/server/tests/server.test.js | 10 +- apps/server/tests/services/games.test.js | 49 +- apps/types/index.d.ts | 86 +- apps/web/package.json | 1 + apps/web/src/3d/behaviors/anchorable.js | 108 +- apps/web/src/3d/behaviors/drawable.js | 2 +- apps/web/src/3d/behaviors/quantifiable.js | 14 +- apps/web/src/3d/behaviors/stackable.js | 2 +- apps/web/src/3d/behaviors/targetable.js | 5 +- apps/web/src/3d/engine.js | 16 +- apps/web/src/3d/managers/control.js | 24 +- apps/web/src/3d/managers/hand.js | 7 +- apps/web/src/3d/managers/index.js | 2 + apps/web/src/3d/managers/replay.js | 4 +- apps/web/src/3d/managers/rule.js | 117 + apps/web/src/3d/managers/score.js | 93 - apps/web/src/3d/managers/selection.js | 4 +- apps/web/src/3d/managers/target.js | 8 +- apps/web/src/3d/utils/behaviors.js | 2 +- apps/web/src/3d/utils/gravity.js | 16 +- apps/web/src/3d/utils/mesh.js | 3 +- apps/web/src/3d/utils/scene-loader.js | 4 +- apps/web/src/graphql/games.graphql | 17 +- .../(auth)/game/[gameId]/GameMenu.svelte | 2 +- .../game/[gameId]/LoadingScreen/Screen.svelte | 3 +- .../(auth)/game/[gameId]/Page.svelte | 13 +- .../(auth)/game/[gameId]/Scores.svelte | 73 + apps/web/src/stores/game-engine.js | 19 +- apps/web/src/stores/game-manager.js | 8 +- apps/web/src/stores/graphql-client.js | 28 +- apps/web/src/types/@babylonjs.d.ts | 9 +- apps/web/src/types/graphql.d.ts | 14 +- apps/web/src/types/index.d.ts | 20 +- apps/web/src/utils/game-interaction.js | 2 +- apps/web/src/utils/game.js | 20 +- apps/web/src/utils/logger.js | 1 + .../web/tests/3d/behaviors/anchorable.test.js | 511 +++- apps/web/tests/3d/behaviors/flippable.test.js | 4 +- .../tests/3d/behaviors/quantifiable.test.js | 32 +- apps/web/tests/3d/behaviors/rotable.test.js | 4 +- apps/web/tests/3d/behaviors/stackable.test.js | 16 +- .../web/tests/3d/behaviors/targetable.test.js | 9 +- apps/web/tests/3d/engine.test.js | 90 +- apps/web/tests/3d/managers/control.test.js | 6 +- apps/web/tests/3d/managers/hand.test.js | 17 +- apps/web/tests/3d/managers/input.test.js | 2 +- apps/web/tests/3d/managers/move.test.js | 6 +- apps/web/tests/3d/managers/replay.test.js | 12 +- apps/web/tests/3d/managers/rule.test.js | 163 ++ apps/web/tests/3d/managers/selection.test.js | 32 +- apps/web/tests/3d/managers/target.test.js | 46 +- .../web/tests/3d/meshes/rounded-tiles.test.js | 3 +- apps/web/tests/3d/utils/behaviors.test.js | 10 +- apps/web/tests/3d/utils/gravity.test.js | 55 + apps/web/tests/3d/utils/mesh.test.js | 4 +- apps/web/tests/3d/utils/scene-loader.test.js | 6 +- apps/web/tests/integration/utils/coverage.js | 2 +- .../(auth)/game/[gameid]/Scores.tools.svelte | 26 + .../[gameid]/__snapshots__/Scores.tools.shot | 112 + apps/web/tests/stores/game-engine.test.js | 19 +- apps/web/tests/stores/game-manager.test.js | 61 +- apps/web/tests/stores/indicators.test.js | 14 +- apps/web/tests/test-utils.js | 34 +- apps/web/tests/utils/game.test.js | 24 - pnpm-lock.yaml | 238 ++ 112 files changed, 6000 insertions(+), 1793 deletions(-) create mode 100644 apps/games/draughts/logic/score.js create mode 100644 apps/games/mah-jong/logic/score.js create mode 100644 apps/server/migrations/006-multiple-anchors.js create mode 100644 apps/server/tests/fixtures/unbuildable-games/import/index.js create mode 100644 apps/web/src/3d/managers/rule.js delete mode 100644 apps/web/src/3d/managers/score.js create mode 100644 apps/web/src/routes/[[lang=lang]]/(auth)/game/[gameId]/Scores.svelte create mode 100644 apps/web/tests/3d/managers/rule.test.js create mode 100644 apps/web/tests/routes/[[lang=lang]]/(auth)/game/[gameid]/Scores.tools.svelte create mode 100644 apps/web/tests/routes/[[lang=lang]]/(auth)/game/[gameid]/__snapshots__/Scores.tools.shot diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9dbc6a1a..c7d34e90 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -167,8 +167,6 @@ jobs: - name: Intall Pnpm uses: pnpm/action-setup@v2 - with: - version: 7 - name: Set Node.js up uses: actions/setup-node@v3 @@ -241,4 +239,4 @@ jobs: uses: codacy/codacy-coverage-reporter-action@master with: project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - coverage-reports: apps/cli/coverage/clover.xml + coverage-reports: apps/games/coverage/clover.xml diff --git a/.gitignore b/.gitignore index 61e1964c..67d9ef39 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ test-results .vercel .last-db upload-manually.sh -en \ No newline at end of file +en +engine.min.js \ No newline at end of file diff --git a/TODO.md b/TODO.md index 7aa65dda..6348cc0b 100644 --- a/TODO.md +++ b/TODO.md @@ -2,9 +2,10 @@ ## Refactor +- Control manager: trigger rule when action is finished? - hand: reuse playMeshes() and pickMesh() in handDrag() - replace windicss with a successor (UnoCSS) -- add tests for web/src/utils/peer-connection +- add tests for web/src/utils/peer-connection + web/src/stores/graphql - group candidate target per kind for performance - all manager managing a collection of behaviors should check their capabilities - stackable/anchorable should check the capabilities of stacked/anchored meshes @@ -14,9 +15,10 @@ ## UI -- when hovering target, highlight should have the dragged mesh's shape, not the target shape (what about parts?) +- fix(web): indicators attach point does not consider the camera position. +- when hovering target, highlight could have the dragged mesh's shape, not the target shape (what about parts?) +- improve selection accuracy, especially with cylindric meshes - hand count on peer pointers/player tab? -- score (Mah-jong, Belote) - command to reset some mesh state and restart a game (Mah-jong, Belote) - "box" space for unusued/undesired meshes - hide/distinguish non-connected participants? @@ -212,3 +214,11 @@ For choosing game colors: Besides being hard to debug and troubleshoot, it requires many encoding/decoding operations since GraphQL only allows text, discards any database migrations or server-side operations (unless rebuilding the document on server-side), and the overall effort of refactoring all communciation pipes and storage code does not really worth it. + +Finite State Machines (FSM) could be a good fit for game rules. Chess for example: a global FSM for game, one for each player, one for each piece. +Transition are possible moves, all relative to the piece's position and direction. FSM could be used to restict action menu items, highlight possible location when starting moving, or revert invalid moves after they are made. +[XState](https://xstate.js.org/docs/) could match (what about the performance?), especially because of designing and debugging tools. + +Good [read](https://gameprogrammingpatterns.com) + +TypeScript compiler's `checkJS` is the root of evil. It checks files in transitive dependencies we have no control on, and that are extremely hard to trace and exclude. Selective opt-in with `@ts-check` is annoying but yields much better results. diff --git a/apps/game-utils/src/descriptor.js b/apps/game-utils/src/descriptor.js index abc3aed3..5f0d7dc2 100644 --- a/apps/game-utils/src/descriptor.js +++ b/apps/game-utils/src/descriptor.js @@ -7,8 +7,9 @@ import { mergeProps } from './utils.js' /** * Creates a unique game from a game descriptor. + * @template {Record} Parameters * @param {string} kind - created game's kind. - * @param {Partial} descriptor - to create game from. + * @param {Partial>} descriptor - to create game from. * @returns a list of serialized 3D meshes. */ export async function createMeshes(kind, descriptor) { @@ -122,6 +123,10 @@ function randomizeBags(bags, meshById) { } /** + * Picks in the bag as many random mesh as requested by the slot (defaults to all). + * Then snaps as many meshes as possible to the slot's anchor (according to its max), and + * stacks the remaining meshes on top of the first one. + * When snapping on multiple anchors, meshes are NOT layed out. * @param {import('@tabulous/types').Slot} slot - slot to fill. * @param {Map} meshesByBagId - randomized meshes per bags. * @param {import('@tabulous/types').Mesh[]} allMeshes - all meshes @@ -134,23 +139,28 @@ function fillSlot( const candidates = meshesByBagId.get(bagId) if (candidates?.length) { const meshes = candidates.splice(0, count ?? candidates.length) + /** @type {import('@tabulous/types').Mesh[]} */ + let stack = meshes for (const mesh of meshes) { mergeProps(mesh, props) } if (anchorId) { const anchor = findAnchor(anchorId, allMeshes) if (anchor) { - if (anchor.snappedId) { - const mesh = findMesh(anchor.snappedId, allMeshes) - if (mesh) { - meshes.splice(0, 0, mesh) + const snapped = findMesh(anchor.snappedIds[0], allMeshes, false) + stack = snapped ? [snapped] : [] + for (const mesh of meshes) { + if (anchor.snappedIds.length === (anchor.max ?? 1)) { + stack.push(mesh) + } else { + anchor.snappedIds.push(mesh.id) + stack = [mesh] + // TODO: lay out multiple meshes } - } else { - anchor.snappedId = meshes[0].id } } } - stackMeshes(meshes) + stackMeshes(stack) } } diff --git a/apps/game-utils/src/hand.js b/apps/game-utils/src/hand.js index 8d0be945..62fa141b 100644 --- a/apps/game-utils/src/hand.js +++ b/apps/game-utils/src/hand.js @@ -21,14 +21,16 @@ export function drawInHand( const hand = findOrCreateHand(game, playerId) const meshes = game.meshes const anchor = findAnchor(fromAnchor, meshes) - const stack = findMesh(anchor.snappedId, meshes, false) + const stack = findMesh(anchor.snappedIds[0], meshes, false) if (!stack) { throw new Error(`Anchor ${fromAnchor} has no snapped mesh`) } for (let i = 0; i < count; i++) { /** @type {import('@tabulous/types').Mesh} */ const drawn = - stack.stackable?.stackIds?.length === 0 ? stack : popMesh(stack, meshes) + (stack.stackable?.stackIds?.length ?? 0) === 0 + ? stack + : popMesh(stack, meshes) mergeProps(drawn, props) hand.meshes.push(drawn) meshes.splice(meshes.indexOf(drawn), 1) @@ -36,8 +38,8 @@ export function drawInHand( break } } - if (stack.stackable?.stackIds?.length === 0) { - anchor.snappedId = null + if ((stack.stackable?.stackIds?.length ?? 0) === 0) { + anchor.snappedIds.splice(0, 1) } } diff --git a/apps/game-utils/src/mesh.js b/apps/game-utils/src/mesh.js index de2fbe08..ad9d375e 100644 --- a/apps/game-utils/src/mesh.js +++ b/apps/game-utils/src/mesh.js @@ -58,7 +58,7 @@ export function findAnchor( if (!match) { return null } - candidates = meshes.filter(({ id }) => id === match.anchor.snappedId) + candidates = meshes.filter(({ id }) => match.anchor.snappedIds.includes(id)) anchor = match.anchor } return anchor ?? null @@ -188,14 +188,14 @@ export function snapTo( } return false } - if (anchor.snappedId) { - const snapped = findMesh(anchor.snappedId, meshes, throwOnMiss) + if (anchor.snappedIds.length === (anchor.max ?? 1)) { + const snapped = findMesh(anchor.snappedIds[0], meshes, throwOnMiss) if (!canStack(snapped, mesh)) { return false } stackMeshes([snapped, mesh]) } else { - anchor.snappedId = mesh.id + anchor.snappedIds.push(mesh.id) } return true } @@ -206,7 +206,7 @@ export function snapTo( * @param {string} anchorId - desired anchor id. * @param {import('@tabulous/types').Mesh[]} meshes - all meshes to search the anchor in. * @param {boolean} [throwOnMiss=true] - * @returns {?import('@tabulous/types').Mesh} unsnapped meshes, or null if anchor has no snapped mesh + * @returns {import('@tabulous/types').Mesh} unsnapped meshes, or null if anchor has no snapped mesh * @throws {Error} when anchor (or snapped mesh) could not be found. * * @overload @@ -222,14 +222,13 @@ export function unsnap( throwOnMiss = true ) { const anchor = findAnchor(anchorId, meshes, throwOnMiss) - if (!anchor || !anchor.snappedId) { + if (!anchor || anchor.snappedIds.length === 0) { if (throwOnMiss) { throw new Error(`Anchor ${anchorId} has no snapped mesh`) } return null } - const id = anchor.snappedId - anchor.snappedId = null + const [id] = anchor.snappedIds.splice(0, 1) return findMesh(id, meshes, throwOnMiss) } diff --git a/apps/game-utils/src/preference.js b/apps/game-utils/src/preference.js index 43480d0b..dbd1632e 100644 --- a/apps/game-utils/src/preference.js +++ b/apps/game-utils/src/preference.js @@ -15,3 +15,21 @@ export function findAvailableValues(preferences, name, possibleValues) { preferences.every(pref => value !== pref[name]) ) } + +/** + * Find game preferences of a given player. + * @param {import('@tabulous/types').PlayerPreference[]|undefined} preferences - list of all players preferences. + * @param {string} playerId - desired player + * @returns found preferences, or an empty object. + */ +export function findPlayerPreferences(preferences, playerId) { + /** @type {Omit} */ + const preference = { + ...(preferences?.find(preference => preference.playerId === playerId) ?? { + color: undefined, + angle: undefined + }) + } + delete preference.playerId + return preference +} diff --git a/apps/game-utils/tests/descriptor.test.js b/apps/game-utils/tests/descriptor.test.js index 6a22a58e..3160060e 100644 --- a/apps/game-utils/tests/descriptor.test.js +++ b/apps/game-utils/tests/descriptor.test.js @@ -179,12 +179,12 @@ describe('createMeshes()', () => { id: boardId, texture: '', shape: /** @type {const} */ ('box'), - anchorable: { anchors: [{ id: 'anchor' }] } + anchorable: { anchors: [{ id: 'anchor', snappedIds: [] }] } } ], bags: new Map([['cards', ids]]), slots: [ - { bagId: 'cards', anchorId: 'anchor', count: 2 }, + { bagId: 'cards', anchorId: 'anchor', count: 2, name: 'first' }, { bagId: 'cards', anchorId: 'anchor', count: 3 }, { bagId: 'cards', anchorId: 'anchor' } ] @@ -200,7 +200,7 @@ describe('createMeshes()', () => { const { id: stackId } = expectStackedOnSlot(meshes, slot, ids.length) const board = meshes.find(({ id }) => id === boardId) expect(board).toBeDefined() - expect(board?.anchorable?.anchors?.[0]?.snappedId).toEqual(stackId) + expect(board?.anchorable?.anchors?.[0]?.snappedIds).toEqual([stackId]) }) }) @@ -212,14 +212,23 @@ describe('createMeshes()', () => { texture: '', shape: /** @type {const} */ ('box'), anchorable: { - anchors: [{ id: 'first' }, { id: 'second' }, { id: 'third' }] + anchors: [ + { id: 'first', snappedIds: [] }, + { id: 'second', snappedIds: [], max: 1 }, + { id: 'third', snappedIds: [], max: 2 } + ] } }, ...ids.map(id => ({ id, texture: '', shape: /** @type {const} */ ('box'), - anchorable: { anchors: [{ id: 'top' }, { id: 'bottom' }] } + anchorable: { + anchors: [ + { id: 'top', snappedIds: [] }, + { id: 'bottom', snappedIds: [] } + ] + } })) ] const bags = new Map([['cards', ids]]) @@ -227,7 +236,7 @@ describe('createMeshes()', () => { it('snaps a random mesh on anchor', async () => { const slots = [ { bagId: 'cards', anchorId: 'first', count: 1, name: 'first' }, - { bagId: 'cards', anchorId: 'third', count: 1, name: 'third' } + { bagId: 'cards', anchorId: 'second', count: 1, name: 'second' } ] const meshes = await createMeshes('cards', { build: () => ({ meshes: initialMeshes, slots, bags }) @@ -241,12 +250,12 @@ describe('createMeshes()', () => { slots[0].name, board?.anchorable?.anchors?.[0] ) - expect(board?.anchorable?.anchors?.[1].snappedId).toBeUndefined() expectSnappedByName( meshes, slots[1].name, - board?.anchorable?.anchors?.[2] + board?.anchorable?.anchors?.[1] ) + expect(board?.anchorable?.anchors?.[2].snappedIds).toHaveLength(0) }) it('snaps a random mesh on chained anchor', async () => { @@ -262,9 +271,9 @@ describe('createMeshes()', () => { const board = meshes.find(({ id }) => id === 'board') expect(board).toBeDefined() - expect(board?.anchorable?.anchors?.[0].snappedId).toBeUndefined() + expect(board?.anchorable?.anchors?.[0].snappedIds).toHaveLength(0) expectSnappedByName(meshes, 'base', board?.anchorable?.anchors?.[1]) - expect(board?.anchorable?.anchors?.[2].snappedId).toBeUndefined() + expect(board?.anchorable?.anchors?.[2].snappedIds).toHaveLength(0) const base = meshes.find(mesh => 'name' in mesh && mesh.name === 'base') expectSnappedByName(meshes, 'top', base?.anchorable?.anchors?.[0]) @@ -295,27 +304,27 @@ describe('createMeshes()', () => { const board = meshes.find(({ id }) => id === 'board') expect(board).toBeDefined() - expect(board?.anchorable?.anchors?.[0].snappedId).toBeUndefined() + expect(board?.anchorable?.anchors?.[0].snappedIds).toHaveLength(0) expectSnappedByName(meshes, 'base', board?.anchorable?.anchors?.[1]) - expect(board?.anchorable?.anchors?.[2].snappedId).toBeUndefined() + expect(board?.anchorable?.anchors?.[2].snappedIds).toHaveLength(0) const base = meshes.find(mesh => 'name' in mesh && mesh.name === 'base') expectSnappedByName(meshes, 'first', base?.anchorable?.anchors?.[0]) - expect(base?.anchorable?.anchors?.[1].snappedId).toBeUndefined() + expect(base?.anchorable?.anchors?.[1].snappedIds).toHaveLength(0) const first = meshes.find(mesh => 'name' in mesh && mesh.name === 'first') expectSnappedByName(meshes, 'second', first?.anchorable?.anchors?.[0]) - expect(first?.anchorable?.anchors?.[1].snappedId).toBeUndefined() + expect(first?.anchorable?.anchors?.[1].snappedIds).toHaveLength(0) const second = meshes.find( mesh => 'name' in mesh && mesh.name === 'second' ) expectSnappedByName(meshes, 'third', second?.anchorable?.anchors?.[1]) - expect(second?.anchorable?.anchors?.[0].snappedId).toBeUndefined() + expect(second?.anchorable?.anchors?.[0].snappedIds).toHaveLength(0) const third = meshes.find(mesh => 'name' in mesh && mesh.name === 'third') - expect(third?.anchorable?.anchors?.[0].snappedId).toBeUndefined() - expect(third?.anchorable?.anchors?.[1].snappedId).toBeUndefined() + expect(third?.anchorable?.anchors?.[0].snappedIds).toHaveLength(0) + expect(third?.anchorable?.anchors?.[1].snappedIds).toHaveLength(0) }) it('can stack on top of an anchor', async () => { @@ -343,7 +352,7 @@ describe('createMeshes()', () => { ) expect(board).toBeDefined() expect(snapped).toHaveLength(3) - expect(board?.anchorable?.anchors?.[0].snappedId).toBeUndefined() + expect(board?.anchorable?.anchors?.[0].snappedIds).toHaveLength(0) const base = snapped.filter(mesh => mesh.stackable) expect(base).toHaveLength(1) expect(base?.[0]?.stackable?.stackIds).toEqual( @@ -378,9 +387,9 @@ describe('createMeshes()', () => { expect(board).toBeDefined() expect(base).toBeDefined() expect(base?.x).toBeUndefined() - expect(board?.anchorable?.anchors?.[0].snappedId).toBeUndefined() + expect(board?.anchorable?.anchors?.[0].snappedIds).toHaveLength(0) expectSnappedByName(meshes, 'base', board?.anchorable?.anchors?.[1]) - expect(board?.anchorable?.anchors?.[2].snappedId).toBeUndefined() + expect(board?.anchorable?.anchors?.[2].snappedIds).toHaveLength(0) expect( meshes .filter( @@ -390,6 +399,27 @@ describe('createMeshes()', () => { .every(({ x }) => x === 1) ).toBe(true) }) + + it('snaps meshes on a multiple anchor', async () => { + const slots = [ + { bagId: 'cards', anchorId: 'third', count: 2, name: 'third' } + ] + const meshes = await createMeshes('cards', { + build: () => ({ meshes: initialMeshes, slots, bags }) + }) + + const board = meshes.find(({ id }) => id === 'board') + expect(board).toBeDefined() + + expect(board?.anchorable?.anchors?.[0].snappedIds).toHaveLength(0) + expect(board?.anchorable?.anchors?.[1].snappedIds).toHaveLength(0) + expectSnappedByName( + meshes, + slots[0].name, + board?.anchorable?.anchors?.[2], + 2 + ) + }) }) describe('given a descriptor with multiple slots on the same bag', () => { @@ -663,7 +693,7 @@ describe('reportReusedIds()', () => { id: 'box1', shape: 'box', texture: '', - anchorable: { anchors: [{ id: 'anchor1' }] } + anchorable: { anchors: [{ id: 'anchor1', snappedIds: [] }] } }, { id: 'box2', shape: 'box', texture: '' } ], @@ -675,7 +705,12 @@ describe('reportReusedIds()', () => { id: 'box3', shape: 'box', texture: '', - anchorable: { anchors: [{ id: 'anchor1' }, { id: 'box2' }] } + anchorable: { + anchors: [ + { id: 'anchor1', snappedIds: [] }, + { id: 'box2', snappedIds: [] } + ] + } } ] } diff --git a/apps/game-utils/tests/game.js b/apps/game-utils/tests/game.js index 87a28db6..6f568386 100644 --- a/apps/game-utils/tests/game.js +++ b/apps/game-utils/tests/game.js @@ -14,7 +14,8 @@ const ajv = new Ajv({ /** @template {Record} Parameters */ export function buildDescriptorTestSuite( /** @type {string} */ name, - /** @type {Partial>} */ descriptor + /** @type {Partial>} */ descriptor, + /** @type {(utils: GameTestUtils) => void} */ customTests = () => {} ) { describe(`${name} game descriptor`, () => { let counter = 1 @@ -95,7 +96,7 @@ export function buildDescriptorTestSuite( () => { it('enrolls each allowed players with a valid JSON schema', async () => { let game = await buildGame( - /** @type {import('@tabulous/types').GameDescriptor} */ ({ + /** @type {import('@tabulous/types').GameDescriptor} */ ({ ...descriptor, name }) @@ -113,7 +114,9 @@ export function buildDescriptorTestSuite( } } game = await enroll( - descriptor, + /** @type {Required, 'addPlayer'>>} */ ( + descriptor + ), game, player, buildParameters(schema) @@ -124,9 +127,20 @@ export function buildDescriptorTestSuite( }) } ) + + customTests?.({ name, makePlayer, buildGame, enroll, buildParameters }) }) } +/** + * @typedef {object} GameTestUtils + * @property {string} name - tested game name. + * @property {typeof makePlayer} makePlayer - buils a player. + * @property {typeof buildGame} buildGame - builds a game from a game setup. + * @property {typeof enroll} enroll - enrolls a player in a game. + * @property {typeof buildParameters} buildParameters - builds game parameters from the provided schema. + */ + /** @returns {import('@tabulous/types').Player} */ function makePlayer(/** @type {number} */ rank) { return { @@ -136,9 +150,12 @@ function makePlayer(/** @type {number} */ rank) { } } -/** @returns {Promise} */ +/** + * @template {Record} Parameters + * @returns {Promise} + */ async function buildGame( - /** @type {import('@tabulous/types').GameDescriptor} */ descriptor + /** @type {import('@tabulous/types').GameDescriptor} */ descriptor ) { return { id: 'game-unique-id', @@ -160,8 +177,8 @@ async function buildGame( } /** @template {Record} Parameters */ -async function enroll( - /** @type {Partial>} */ descriptor, +export async function enroll( + /** @type {Required, 'addPlayer'>>} */ descriptor, /** @type {import('@tabulous/types').StartedGame} */ game, /** @type {import('@tabulous/types').Player} */ guest, /** @type {Record} */ parameters @@ -177,13 +194,16 @@ async function enroll( game.preferences.map(({ color }) => color) ) }) - return await /** @type {import('@tabulous/types').GameDescriptor & { addPlayer: import('@tabulous/types').AddPlayer }} */ ( - descriptor - ).addPlayer(game, guest, /** @type {?} */ (parameters)) + return await descriptor.addPlayer( + game, + guest, + /** @type {Parameters} */ (parameters) + ) } -function buildParameters( - /** @type {?import('@tabulous/types').Schema} */ schema +/** @template {Record} Parameters */ +export function buildParameters( + /** @type {?import('@tabulous/types').Schema} */ schema ) { /** @type {Record} */ const result = {} @@ -313,3 +333,15 @@ function expectNumber( function last(/** @type {string} */ property) { return /** @type {string} */ (property.split('.').pop()) } + +export function toEngineState( + /** @type {import('@tabulous/types').StartedGame} */ game, + /** @type {import('@tabulous/types').Player|undefined} */ player +) { + return { + meshes: game.meshes, + handMeshes: + game.hands.find(({ playerId }) => playerId === player?.id)?.meshes ?? [], + history: [] + } +} diff --git a/apps/game-utils/tests/hand.test.js b/apps/game-utils/tests/hand.test.js index add2c399..203c2c06 100644 --- a/apps/game-utils/tests/hand.test.js +++ b/apps/game-utils/tests/hand.test.js @@ -18,7 +18,12 @@ describe('drawInHand()', () => { id: 'B', texture: '', shape: 'box', - anchorable: { anchors: [{ id: 'discard', snappedId: 'C' }] } + anchorable: { + anchors: [ + { id: 'discard', snappedIds: ['C'] }, + { id: 'reserve', snappedIds: ['F', 'G'], max: 2 } + ] + } }, { id: 'C', @@ -27,7 +32,9 @@ describe('drawInHand()', () => { stackable: { stackIds: ['A', 'E', 'D'] } }, { id: 'D', texture: '', shape: 'box' }, - { id: 'E', texture: '', shape: 'box' } + { id: 'E', texture: '', shape: 'box' }, + { id: 'F', texture: '', shape: 'box' }, + { id: 'G', texture: '', shape: 'box' } ] }) }) @@ -40,16 +47,30 @@ describe('drawInHand()', () => { it('draws one mesh into a new hand', () => { drawInHand(game, { playerId, fromAnchor: 'discard' }) + drawInHand(game, { playerId, fromAnchor: 'reserve' }) expect(game).toEqual( expect.objectContaining({ - hands: [{ playerId, meshes: [{ id: 'D', texture: '', shape: 'box' }] }], + hands: [ + { + playerId, + meshes: [ + { id: 'D', texture: '', shape: 'box' }, + { id: 'F', texture: '', shape: 'box' } + ] + } + ], meshes: [ { id: 'A', texture: '', shape: 'box' }, { id: 'B', texture: '', shape: 'box', - anchorable: { anchors: [{ id: 'discard', snappedId: 'C' }] } + anchorable: { + anchors: [ + { id: 'discard', snappedIds: ['C'] }, + { id: 'reserve', snappedIds: ['G'], max: 2 } + ] + } }, { id: 'C', @@ -57,7 +78,8 @@ describe('drawInHand()', () => { shape: 'box', stackable: { stackIds: ['A', 'E'] } }, - { id: 'E', texture: '', shape: 'box' } + { id: 'E', texture: '', shape: 'box' }, + { id: 'G', texture: '', shape: 'box' } ] }) ) @@ -83,9 +105,21 @@ describe('drawInHand()', () => { id: 'B', texture: '', shape: 'box', - anchorable: { anchors: [{ id: 'discard', snappedId: 'C' }] } + anchorable: { + anchors: [ + { id: 'discard', snappedIds: ['C'] }, + { id: 'reserve', snappedIds: ['F', 'G'], max: 2 } + ] + } + }, + { + id: 'C', + texture: '', + shape: 'box', + stackable: { stackIds: ['A'] } }, - { id: 'C', texture: '', shape: 'box', stackable: { stackIds: ['A'] } } + { id: 'F', texture: '', shape: 'box' }, + { id: 'G', texture: '', shape: 'box' } ] }) ) @@ -93,6 +127,7 @@ describe('drawInHand()', () => { it('draws until depletion into a new hand', () => { drawInHand(game, { playerId, count: 10, fromAnchor: 'discard' }) + drawInHand(game, { playerId, count: 10, fromAnchor: 'reserve' }) expect(game).toEqual( expect.objectContaining({ hands: [ @@ -107,7 +142,8 @@ describe('drawInHand()', () => { texture: '', shape: 'box', stackable: { stackIds: [] } - } + }, + { id: 'F', texture: '', shape: 'box' } ] } ], @@ -116,15 +152,22 @@ describe('drawInHand()', () => { id: 'B', texture: '', shape: 'box', - anchorable: { anchors: [{ id: 'discard', snappedId: null }] } - } + anchorable: { + anchors: [ + { id: 'discard', snappedIds: [] }, + { id: 'reserve', snappedIds: ['G'], max: 2 } + ] + } + }, + { id: 'G', texture: '', shape: 'box' } ] }) ) }) it('throws when drawing from empty anchor', () => { - delete game.meshes[1].anchorable?.anchors?.[0].snappedId + // @ts-expect-error -- we can't use ! operator in JS + game.meshes[1].anchorable.anchors[0].snappedIds = [] expect(() => drawInHand(game, { playerId, count: 2, fromAnchor: 'discard' }) ).toThrow('Anchor discard has no snapped mesh') diff --git a/apps/game-utils/tests/mesh.test.js b/apps/game-utils/tests/mesh.test.js index 7aefe908..a93d2bc5 100644 --- a/apps/game-utils/tests/mesh.test.js +++ b/apps/game-utils/tests/mesh.test.js @@ -25,7 +25,7 @@ describe('pop()', () => { id: 'B', texture: '', shape: 'box', - anchorable: { anchors: [{ id: 'discard', snappedId: 'C' }] } + anchorable: { anchors: [{ id: 'discard', snappedIds: ['C'] }] } }, { id: 'C', @@ -108,7 +108,8 @@ describe('findMesh()', () => { describe('findAnchor()', () => { const anchors = Array.from({ length: 10 }, () => ({ - id: faker.string.uuid() + id: faker.string.uuid(), + snappedIds: [] })) const meshes = [ @@ -167,25 +168,25 @@ describe('findAnchor()', () => { id: 'mesh0', texture: '', shape: /** @type {const} */ ('box'), - anchorable: { anchors: [{ id: 'bottom' }] } + anchorable: { anchors: [{ id: 'bottom', snappedIds: [] }] } }, { id: 'mesh1', texture: '', shape: /** @type {const} */ ('box'), - anchorable: { anchors: [{ id: 'bottom', snappedId: 'mesh3' }] } + anchorable: { anchors: [{ id: 'bottom', snappedIds: ['mesh3'] }] } }, { id: 'mesh2', texture: '', shape: /** @type {const} */ ('box'), - anchorable: { anchors: [{ id: 'start', snappedId: 'mesh1' }] } + anchorable: { anchors: [{ id: 'start', snappedIds: ['mesh1'] }] } }, { id: 'mesh3', texture: '', shape: /** @type {const} */ ('box'), - anchorable: { anchors: [{ id: 'bottom', snappedId: 'mesh0' }] } + anchorable: { anchors: [{ id: 'bottom', snappedIds: ['mesh0'] }] } } ] expect(findAnchor('start.bottom', meshes)).toEqual( @@ -214,13 +215,18 @@ describe('snapTo()', () => { id: 'mesh1', texture: '', shape: 'box', - anchorable: { anchors: [{ id: 'anchor1' }] } + anchorable: { anchors: [{ id: 'anchor1', snappedIds: [] }] } }, { id: 'mesh2', texture: '', shape: 'box', - anchorable: { anchors: [{ id: 'anchor2' }, { id: 'anchor3' }] } + anchorable: { + anchors: [ + { id: 'anchor2', snappedIds: [] }, + { id: 'anchor3', snappedIds: [], max: 2 } + ] + } }, { id: 'mesh3', texture: '', shape: 'box' } ] @@ -233,7 +239,26 @@ describe('snapTo()', () => { texture: '', shape: 'box', anchorable: { - anchors: [{ id: 'anchor2' }, { id: 'anchor3', snappedId: 'mesh0' }] + anchors: [ + { id: 'anchor2', snappedIds: [] }, + { id: 'anchor3', snappedIds: ['mesh0'], max: 2 } + ] + } + }) + }) + + it('snaps meshes to a multiple anchor', () => { + expect(snapTo('anchor3', meshes[0], meshes)).toBe(true) + expect(snapTo('anchor3', meshes[2], meshes)).toBe(true) + expect(meshes[2]).toEqual({ + id: 'mesh2', + texture: '', + shape: 'box', + anchorable: { + anchors: [ + { id: 'anchor2', snappedIds: [] }, + { id: 'anchor3', snappedIds: ['mesh0', 'mesh2'], max: 2 } + ] } }) }) @@ -248,7 +273,10 @@ describe('snapTo()', () => { texture: '', shape: 'box', anchorable: { - anchors: [{ id: 'anchor2', snappedId: 'mesh0' }, { id: 'anchor3' }] + anchors: [ + { id: 'anchor2', snappedIds: ['mesh0'] }, + { id: 'anchor3', snappedIds: [], max: 2 } + ] } }) expect(meshes[0]).toEqual({ @@ -311,7 +339,10 @@ describe('unsnap()', () => { texture: '', shape: 'box', anchorable: { - anchors: [{ id: 'anchor1', snappedId: 'mesh2' }, { id: 'anchor2' }] + anchors: [ + { id: 'anchor1', snappedIds: ['mesh2'] }, + { id: 'anchor2', snappedIds: [] } + ] } }, { @@ -320,16 +351,13 @@ describe('unsnap()', () => { shape: 'box', anchorable: { anchors: [ - { id: 'anchor3', snappedId: 'mesh3' }, - { id: 'anchor4', snappedId: 'unknown' } + { id: 'anchor3', snappedIds: ['mesh3', 'mesh4'], max: 2 }, + { id: 'anchor4', snappedIds: ['unknown'] } ] } }, - { - id: 'mesh3', - texture: '', - shape: 'box' - } + { id: 'mesh3', texture: '', shape: 'box' }, + { id: 'mesh4', texture: '', shape: 'box' } ] }) @@ -360,11 +388,25 @@ describe('unsnap()', () => { }) it('returns mesh and unsnapps it', () => { + expect(unsnap('anchor1', meshes)).toEqual(meshes[1]) + expect(meshes[0].anchorable?.anchors).toEqual([ + { id: 'anchor1', snappedIds: [] }, + { id: 'anchor2', snappedIds: [] } + ]) + }) + + it('can unsapps multiple meshes', () => { expect(unsnap('anchor3', meshes)).toEqual(meshes[2]) expect(meshes[1].anchorable?.anchors).toEqual([ - { id: 'anchor3', snappedId: null }, - { id: 'anchor4', snappedId: 'unknown' } + { id: 'anchor3', snappedIds: ['mesh4'], max: 2 }, + { id: 'anchor4', snappedIds: ['unknown'] } + ]) + expect(unsnap('anchor3', meshes)).toEqual(meshes[3]) + expect(meshes[1].anchorable?.anchors).toEqual([ + { id: 'anchor3', snappedIds: [], max: 2 }, + { id: 'anchor4', snappedIds: ['unknown'] } ]) + expect(unsnap('anchor3', meshes, false)).toBeNull() }) }) diff --git a/apps/game-utils/tests/preference.test.js b/apps/game-utils/tests/preference.test.js index 0310c1db..996926e8 100644 --- a/apps/game-utils/tests/preference.test.js +++ b/apps/game-utils/tests/preference.test.js @@ -1,7 +1,10 @@ // @ts-check import { describe, expect, it } from 'vitest' -import { findAvailableValues } from '../src/preference.js' +import { + findAvailableValues, + findPlayerPreferences +} from '../src/preference.js' describe('findAvailableValues()', () => { const colors = ['red', 'green', 'blue'] @@ -50,3 +53,36 @@ describe('findAvailableValues()', () => { ).toEqual(colors) }) }) + +describe('findPlayerPreferences()', () => { + it('returns an empty object without preferences', async () => { + expect(findPlayerPreferences(undefined, 'whatever')).toEqual({}) + }) + + it('returns an empty object for an unknown player', async () => { + expect( + findPlayerPreferences( + [ + { playerId: 'a', color: 'red' }, + { playerId: 'b', color: 'blue' } + ], + 'whatever' + ) + ).toEqual({}) + }) + + it('returns preferences of a given player, omitting the playerId', async () => { + const preferences = [ + { playerId: 'a', color: 'red' }, + { playerId: 'b', color: 'blue' } + ] + expect(findPlayerPreferences(preferences, 'a')).toEqual({ + ...preferences[0], + playerId: undefined + }) + expect(findPlayerPreferences(preferences, 'b')).toEqual({ + ...preferences[1], + playerId: undefined + }) + }) +}) diff --git a/apps/game-utils/tests/test-utils.js b/apps/game-utils/tests/test-utils.js index a540a522..d1de90a6 100644 --- a/apps/game-utils/tests/test-utils.js +++ b/apps/game-utils/tests/test-utils.js @@ -9,6 +9,7 @@ export function expectStackedOnSlot( const stack = meshes.find( ({ stackable }) => stackable?.stackIds?.length === count - 1 ) + // console.log(slot, JSON.stringify(meshes, null, 2)) expect(stack).toBeDefined() const stackedMeshes = meshes.filter( ({ id }) => stack?.stackable?.stackIds?.includes(id) || id === stack?.id @@ -25,11 +26,14 @@ export function expectStackedOnSlot( export function expectSnappedByName( /** @type {import('@tabulous/types').Mesh[]} */ meshes, /** @type {string} */ name, - /** @type {import('@tabulous/types').Anchor|undefined} */ anchor + /** @type {import('@tabulous/types').Anchor|undefined} */ anchor, + count = 1 ) { const candidates = meshes.filter(mesh => 'name' in mesh && name === mesh.name) - expect(candidates).toHaveLength(1) - expect(anchor?.snappedId).toEqual(candidates[0].id) + expect(candidates).toHaveLength(count) + expect(anchor?.snappedIds).toEqual( + expect.objectContaining(candidates.map(({ id }) => id)) + ) } /** diff --git a/apps/games/chess/__snapshots__/index.test.js.snap b/apps/games/chess/__snapshots__/index.test.js.snap index 209fa81e..2e0d12a9 100644 --- a/apps/games/chess/__snapshots__/index.test.js.snap +++ b/apps/games/chess/__snapshots__/index.test.js.snap @@ -40,7 +40,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-0", - "snappedId": "white-rook-1", + "snappedIds": [ + "white-rook-1", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -50,7 +52,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-1", - "snappedId": "white-pawn-0", + "snappedIds": [ + "white-pawn-0", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -60,7 +64,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -70,7 +74,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -80,7 +84,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -90,7 +94,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -100,7 +104,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-6", - "snappedId": "black-pawn-0", + "snappedIds": [ + "black-pawn-0", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -110,7 +116,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-7", - "snappedId": "black-rook-1", + "snappedIds": [ + "black-rook-1", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -120,7 +128,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-0", - "snappedId": "white-knight-1", + "snappedIds": [ + "white-knight-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -130,7 +140,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-1", - "snappedId": "white-pawn-1", + "snappedIds": [ + "white-pawn-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -140,7 +152,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -150,7 +162,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -160,7 +172,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -170,7 +182,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -180,7 +192,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-6", - "snappedId": "black-pawn-1", + "snappedIds": [ + "black-pawn-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -190,7 +204,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-7", - "snappedId": "black-knight-1", + "snappedIds": [ + "black-knight-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -200,7 +216,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-0", - "snappedId": "white-bishop-1", + "snappedIds": [ + "white-bishop-1", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -210,7 +228,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-1", - "snappedId": "white-pawn-2", + "snappedIds": [ + "white-pawn-2", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -220,7 +240,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -230,7 +250,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -240,7 +260,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -250,7 +270,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -260,7 +280,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-6", - "snappedId": "black-pawn-2", + "snappedIds": [ + "black-pawn-2", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -270,7 +292,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-7", - "snappedId": "black-bishop-1", + "snappedIds": [ + "black-bishop-1", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -280,7 +304,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-0", - "snappedId": "white-queen", + "snappedIds": [ + "white-queen", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -290,7 +316,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-1", - "snappedId": "white-pawn-3", + "snappedIds": [ + "white-pawn-3", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -300,7 +328,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -310,7 +338,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -320,7 +348,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -330,7 +358,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -340,7 +368,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-6", - "snappedId": "black-pawn-3", + "snappedIds": [ + "black-pawn-3", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -350,7 +380,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-7", - "snappedId": "black-queen", + "snappedIds": [ + "black-queen", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -360,7 +392,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-0", - "snappedId": "white-king", + "snappedIds": [ + "white-king", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -370,7 +404,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-1", - "snappedId": "white-pawn-4", + "snappedIds": [ + "white-pawn-4", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -380,7 +416,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -390,7 +426,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -400,7 +436,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -410,7 +446,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -420,7 +456,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-6", - "snappedId": "black-pawn-4", + "snappedIds": [ + "black-pawn-4", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -430,7 +468,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-7", - "snappedId": "black-king", + "snappedIds": [ + "black-king", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -440,7 +480,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-0", - "snappedId": "white-bishop-2", + "snappedIds": [ + "white-bishop-2", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -450,7 +492,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-1", - "snappedId": "white-pawn-5", + "snappedIds": [ + "white-pawn-5", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -460,7 +504,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -470,7 +514,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -480,7 +524,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -490,7 +534,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -500,7 +544,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-6", - "snappedId": "black-pawn-5", + "snappedIds": [ + "black-pawn-5", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -510,7 +556,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-7", - "snappedId": "black-bishop-2", + "snappedIds": [ + "black-bishop-2", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -520,7 +568,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-0", - "snappedId": "white-knight-2", + "snappedIds": [ + "white-knight-2", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -530,7 +580,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-1", - "snappedId": "white-pawn-6", + "snappedIds": [ + "white-pawn-6", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -540,7 +592,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -550,7 +602,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -560,7 +612,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -570,7 +622,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -580,7 +632,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-6", - "snappedId": "black-pawn-6", + "snappedIds": [ + "black-pawn-6", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -590,7 +644,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-7", - "snappedId": "black-knight-2", + "snappedIds": [ + "black-knight-2", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -600,7 +656,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-0", - "snappedId": "white-rook-2", + "snappedIds": [ + "white-rook-2", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -610,7 +668,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-1", - "snappedId": "white-pawn-7", + "snappedIds": [ + "white-pawn-7", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -620,7 +680,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -630,7 +690,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -640,7 +700,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -650,7 +710,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -660,7 +720,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-6", - "snappedId": "black-pawn-7", + "snappedIds": [ + "black-pawn-7", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -670,7 +732,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-7", - "snappedId": "black-rook-2", + "snappedIds": [ + "black-rook-2", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -1131,7 +1195,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-0", - "snappedId": "white-rook-1", + "snappedIds": [ + "white-rook-1", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -1141,7 +1207,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-1", - "snappedId": "white-pawn-0", + "snappedIds": [ + "white-pawn-0", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -1151,7 +1219,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -1161,7 +1229,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -1171,7 +1239,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -1181,7 +1249,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -1191,7 +1259,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-6", - "snappedId": "black-pawn-0", + "snappedIds": [ + "black-pawn-0", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -1201,7 +1271,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-0-7", - "snappedId": "black-rook-1", + "snappedIds": [ + "black-rook-1", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -1211,7 +1283,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-0", - "snappedId": "white-knight-1", + "snappedIds": [ + "white-knight-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -1221,7 +1295,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-1", - "snappedId": "white-pawn-1", + "snappedIds": [ + "white-pawn-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -1231,7 +1307,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -1241,7 +1317,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -1251,7 +1327,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -1261,7 +1337,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -1271,7 +1347,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-6", - "snappedId": "black-pawn-1", + "snappedIds": [ + "black-pawn-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -1281,7 +1359,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-1-7", - "snappedId": "black-knight-1", + "snappedIds": [ + "black-knight-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -1291,7 +1371,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-0", - "snappedId": "white-bishop-1", + "snappedIds": [ + "white-bishop-1", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -1301,7 +1383,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-1", - "snappedId": "white-pawn-2", + "snappedIds": [ + "white-pawn-2", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -1311,7 +1395,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -1321,7 +1405,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -1331,7 +1415,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -1341,7 +1425,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -1351,7 +1435,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-6", - "snappedId": "black-pawn-2", + "snappedIds": [ + "black-pawn-2", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -1361,7 +1447,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-2-7", - "snappedId": "black-bishop-1", + "snappedIds": [ + "black-bishop-1", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -1371,7 +1459,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-0", - "snappedId": "white-queen", + "snappedIds": [ + "white-queen", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -1381,7 +1471,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-1", - "snappedId": "white-pawn-3", + "snappedIds": [ + "white-pawn-3", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -1391,7 +1483,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -1401,7 +1493,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -1411,7 +1503,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -1421,7 +1513,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -1431,7 +1523,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-6", - "snappedId": "black-pawn-3", + "snappedIds": [ + "black-pawn-3", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -1441,7 +1535,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-3-7", - "snappedId": "black-queen", + "snappedIds": [ + "black-queen", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -1451,7 +1547,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-0", - "snappedId": "white-king", + "snappedIds": [ + "white-king", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -1461,7 +1559,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-1", - "snappedId": "white-pawn-4", + "snappedIds": [ + "white-pawn-4", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -1471,7 +1571,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -1481,7 +1581,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -1491,7 +1591,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -1501,7 +1601,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -1511,7 +1611,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-6", - "snappedId": "black-pawn-4", + "snappedIds": [ + "black-pawn-4", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -1521,7 +1623,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-4-7", - "snappedId": "black-king", + "snappedIds": [ + "black-king", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -1531,7 +1635,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-0", - "snappedId": "white-bishop-2", + "snappedIds": [ + "white-bishop-2", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -1541,7 +1647,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-1", - "snappedId": "white-pawn-5", + "snappedIds": [ + "white-pawn-5", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -1551,7 +1659,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -1561,7 +1669,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -1571,7 +1679,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -1581,7 +1689,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -1591,7 +1699,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-6", - "snappedId": "black-pawn-5", + "snappedIds": [ + "black-pawn-5", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -1601,7 +1711,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-5-7", - "snappedId": "black-bishop-2", + "snappedIds": [ + "black-bishop-2", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -1611,7 +1723,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-0", - "snappedId": "white-knight-2", + "snappedIds": [ + "white-knight-2", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -1621,7 +1735,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-1", - "snappedId": "white-pawn-6", + "snappedIds": [ + "white-pawn-6", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -1631,7 +1747,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -1641,7 +1757,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -1651,7 +1767,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -1661,7 +1777,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -1671,7 +1787,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-6", - "snappedId": "black-pawn-6", + "snappedIds": [ + "black-pawn-6", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -1681,7 +1799,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-6-7", - "snappedId": "black-knight-2", + "snappedIds": [ + "black-knight-2", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -1691,7 +1811,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-0", - "snappedId": "white-rook-2", + "snappedIds": [ + "white-rook-2", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -1701,7 +1823,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-1", - "snappedId": "white-pawn-7", + "snappedIds": [ + "white-pawn-7", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -1711,7 +1835,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -1721,7 +1845,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -1731,7 +1855,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -1741,7 +1865,7 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -1751,7 +1875,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-6", - "snappedId": "black-pawn-7", + "snappedIds": [ + "black-pawn-7", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -1761,7 +1887,9 @@ exports[`chess game descriptor > askForParameters() + addPlayer() > enrolls each "depth": 3, "height": 0.01, "id": "tile-7-7", - "snappedId": "black-rook-2", + "snappedIds": [ + "black-rook-2", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -2185,7 +2313,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-0-0", - "snappedId": "white-rook-1", + "snappedIds": [ + "white-rook-1", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -2195,7 +2325,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-0-1", - "snappedId": "white-pawn-0", + "snappedIds": [ + "white-pawn-0", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -2205,7 +2337,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-0-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -2215,7 +2347,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-0-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -2225,7 +2357,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-0-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -2235,7 +2367,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-0-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -10.5, "y": 0.25, @@ -2245,7 +2377,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-0-6", - "snappedId": "black-pawn-0", + "snappedIds": [ + "black-pawn-0", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -2255,7 +2389,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-0-7", - "snappedId": "black-rook-1", + "snappedIds": [ + "black-rook-1", + ], "width": 3, "x": -10.5, "y": 0.25, @@ -2265,7 +2401,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-1-0", - "snappedId": "white-knight-1", + "snappedIds": [ + "white-knight-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -2275,7 +2413,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-1-1", - "snappedId": "white-pawn-1", + "snappedIds": [ + "white-pawn-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -2285,7 +2425,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-1-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -2295,7 +2435,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-1-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -2305,7 +2445,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-1-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -2315,7 +2455,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-1-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -7.5, "y": 0.25, @@ -2325,7 +2465,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-1-6", - "snappedId": "black-pawn-1", + "snappedIds": [ + "black-pawn-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -2335,7 +2477,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-1-7", - "snappedId": "black-knight-1", + "snappedIds": [ + "black-knight-1", + ], "width": 3, "x": -7.5, "y": 0.25, @@ -2345,7 +2489,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-2-0", - "snappedId": "white-bishop-1", + "snappedIds": [ + "white-bishop-1", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -2355,7 +2501,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-2-1", - "snappedId": "white-pawn-2", + "snappedIds": [ + "white-pawn-2", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -2365,7 +2513,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-2-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -2375,7 +2523,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-2-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -2385,7 +2533,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-2-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -2395,7 +2543,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-2-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -4.5, "y": 0.25, @@ -2405,7 +2553,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-2-6", - "snappedId": "black-pawn-2", + "snappedIds": [ + "black-pawn-2", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -2415,7 +2565,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-2-7", - "snappedId": "black-bishop-1", + "snappedIds": [ + "black-bishop-1", + ], "width": 3, "x": -4.5, "y": 0.25, @@ -2425,7 +2577,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-3-0", - "snappedId": "white-queen", + "snappedIds": [ + "white-queen", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -2435,7 +2589,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-3-1", - "snappedId": "white-pawn-3", + "snappedIds": [ + "white-pawn-3", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -2445,7 +2601,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-3-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -2455,7 +2611,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-3-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -2465,7 +2621,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-3-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -2475,7 +2631,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-3-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": -1.5, "y": 0.25, @@ -2485,7 +2641,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-3-6", - "snappedId": "black-pawn-3", + "snappedIds": [ + "black-pawn-3", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -2495,7 +2653,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-3-7", - "snappedId": "black-queen", + "snappedIds": [ + "black-queen", + ], "width": 3, "x": -1.5, "y": 0.25, @@ -2505,7 +2665,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-4-0", - "snappedId": "white-king", + "snappedIds": [ + "white-king", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -2515,7 +2677,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-4-1", - "snappedId": "white-pawn-4", + "snappedIds": [ + "white-pawn-4", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -2525,7 +2689,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-4-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -2535,7 +2699,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-4-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -2545,7 +2709,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-4-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -2555,7 +2719,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-4-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 1.5, "y": 0.25, @@ -2565,7 +2729,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-4-6", - "snappedId": "black-pawn-4", + "snappedIds": [ + "black-pawn-4", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -2575,7 +2741,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-4-7", - "snappedId": "black-king", + "snappedIds": [ + "black-king", + ], "width": 3, "x": 1.5, "y": 0.25, @@ -2585,7 +2753,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-5-0", - "snappedId": "white-bishop-2", + "snappedIds": [ + "white-bishop-2", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -2595,7 +2765,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-5-1", - "snappedId": "white-pawn-5", + "snappedIds": [ + "white-pawn-5", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -2605,7 +2777,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-5-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -2615,7 +2787,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-5-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -2625,7 +2797,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-5-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -2635,7 +2807,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-5-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 4.5, "y": 0.25, @@ -2645,7 +2817,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-5-6", - "snappedId": "black-pawn-5", + "snappedIds": [ + "black-pawn-5", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -2655,7 +2829,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-5-7", - "snappedId": "black-bishop-2", + "snappedIds": [ + "black-bishop-2", + ], "width": 3, "x": 4.5, "y": 0.25, @@ -2665,7 +2841,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-6-0", - "snappedId": "white-knight-2", + "snappedIds": [ + "white-knight-2", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -2675,7 +2853,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-6-1", - "snappedId": "white-pawn-6", + "snappedIds": [ + "white-pawn-6", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -2685,7 +2865,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-6-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -2695,7 +2875,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-6-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -2705,7 +2885,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-6-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -2715,7 +2895,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-6-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 7.5, "y": 0.25, @@ -2725,7 +2905,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-6-6", - "snappedId": "black-pawn-6", + "snappedIds": [ + "black-pawn-6", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -2735,7 +2917,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-6-7", - "snappedId": "black-knight-2", + "snappedIds": [ + "black-knight-2", + ], "width": 3, "x": 7.5, "y": 0.25, @@ -2745,7 +2929,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-7-0", - "snappedId": "white-rook-2", + "snappedIds": [ + "white-rook-2", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -2755,7 +2941,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-7-1", - "snappedId": "white-pawn-7", + "snappedIds": [ + "white-pawn-7", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -2765,7 +2953,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-7-2", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -2775,7 +2963,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-7-3", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -2785,7 +2973,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-7-4", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -2795,7 +2983,7 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-7-5", - "snappedId": null, + "snappedIds": [], "width": 3, "x": 10.5, "y": 0.25, @@ -2805,7 +2993,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-7-6", - "snappedId": "black-pawn-7", + "snappedIds": [ + "black-pawn-7", + ], "width": 3, "x": 10.5, "y": 0.25, @@ -2815,7 +3005,9 @@ exports[`chess game descriptor > build() > builds game setup 1`] = ` "depth": 3, "height": 0.01, "id": "tile-7-7", - "snappedId": "black-rook-2", + "snappedIds": [ + "black-rook-2", + ], "width": 3, "x": 10.5, "y": 0.25, diff --git a/apps/games/chess/logic/builders/board.js b/apps/games/chess/logic/builders/board.js index 3bdce097..0cd2bcbd 100644 --- a/apps/games/chess/logic/builders/board.js +++ b/apps/games/chess/logic/builders/board.js @@ -21,6 +21,7 @@ function buildAnchors() { const x = sizes.tile * (max * -0.5 + 0.5) for (let column = 0; column < max; column++) { for (let row = 0; row < max; row++) { + const pieceId = getPieceId({ column, row }) anchors.push({ id: `tile-${column}-${row}`, x: x + sizes.tile * column, @@ -29,7 +30,7 @@ function buildAnchors() { width: sizes.tile, depth: sizes.tile, height: 0.01, - snappedId: getPieceId({ column, row }) + snappedIds: pieceId ? [pieceId] : [] }) } } diff --git a/apps/games/draughts/__snapshots__/index.test.js.snap b/apps/games/draughts/__snapshots__/index.test.js.snap index 1261ccf7..ba06761c 100644 --- a/apps/games/draughts/__snapshots__/index.test.js.snap +++ b/apps/games/draughts/__snapshots__/index.test.js.snap @@ -47,7 +47,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -63,7 +63,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -79,7 +79,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -95,7 +95,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -111,7 +111,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -127,7 +127,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -143,7 +143,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -159,7 +159,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -175,7 +175,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -191,7 +191,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -207,7 +207,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -223,7 +223,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -239,7 +239,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -255,7 +255,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -271,7 +271,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -287,7 +287,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -303,7 +303,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -319,7 +319,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -335,7 +335,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -351,7 +351,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -367,7 +367,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -383,7 +383,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -399,7 +399,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -415,7 +415,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -431,7 +431,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -447,7 +447,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -463,7 +463,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -479,7 +479,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -495,7 +495,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -511,7 +511,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -527,7 +527,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -543,7 +543,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -559,7 +559,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -575,7 +575,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -591,7 +591,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -607,7 +607,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -623,7 +623,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -639,7 +639,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -655,7 +655,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -671,7 +671,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "anchorable": { @@ -680,442 +680,541 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e "diameter": 2.9, "height": 0.01, "id": "pawn-0-0", - "snappedId": "white-6", + "snappedIds": [ + "white-6", + ], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-2", - "snappedId": "white-16", + "snappedIds": [ + "white-16", + ], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-4", + "snappedIds": [], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-6", - "snappedId": "black-6", + "snappedIds": [ + "black-6", + ], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-8", - "snappedId": "black-16", + "snappedIds": [ + "black-16", + ], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-1", - "snappedId": "white-11", + "snappedIds": [ + "white-11", + ], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-3", - "snappedId": "white-1", + "snappedIds": [ + "white-1", + ], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-5", + "snappedIds": [], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-7", - "snappedId": "black-11", + "snappedIds": [ + "black-11", + ], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-9", - "snappedId": "black-1", + "snappedIds": [ + "black-1", + ], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-0", - "snappedId": "white-7", + "snappedIds": [ + "white-7", + ], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-2", - "snappedId": "white-17", + "snappedIds": [ + "white-17", + ], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-4", + "snappedIds": [], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-6", - "snappedId": "black-7", + "snappedIds": [ + "black-7", + ], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-8", - "snappedId": "black-17", + "snappedIds": [ + "black-17", + ], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-1", - "snappedId": "white-12", + "snappedIds": [ + "white-12", + ], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-3", - "snappedId": "white-2", + "snappedIds": [ + "white-2", + ], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-5", + "snappedIds": [], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-7", - "snappedId": "black-12", + "snappedIds": [ + "black-12", + ], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-9", - "snappedId": "black-2", + "snappedIds": [ + "black-2", + ], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-0", - "snappedId": "white-8", + "snappedIds": [ + "white-8", + ], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-2", - "snappedId": "white-18", + "snappedIds": [ + "white-18", + ], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-4", + "snappedIds": [], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-6", - "snappedId": "black-8", + "snappedIds": [ + "black-8", + ], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-8", - "snappedId": "black-18", + "snappedIds": [ + "black-18", + ], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-1", - "snappedId": "white-13", + "snappedIds": [ + "white-13", + ], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-3", - "snappedId": "white-3", + "snappedIds": [ + "white-3", + ], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-5", + "snappedIds": [], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-7", - "snappedId": "black-13", + "snappedIds": [ + "black-13", + ], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-9", - "snappedId": "black-3", + "snappedIds": [ + "black-3", + ], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-0", - "snappedId": "white-9", + "snappedIds": [ + "white-9", + ], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-2", - "snappedId": "white-19", + "snappedIds": [ + "white-19", + ], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-4", + "snappedIds": [], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-6", - "snappedId": "black-9", + "snappedIds": [ + "black-9", + ], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-8", - "snappedId": "black-19", + "snappedIds": [ + "black-19", + ], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-1", - "snappedId": "white-14", + "snappedIds": [ + "white-14", + ], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-3", - "snappedId": "white-4", + "snappedIds": [ + "white-4", + ], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-5", + "snappedIds": [], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-7", - "snappedId": "black-14", + "snappedIds": [ + "black-14", + ], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-9", - "snappedId": "black-4", + "snappedIds": [ + "black-4", + ], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-0", - "snappedId": "white-10", + "snappedIds": [ + "white-10", + ], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-2", - "snappedId": "white-20", + "snappedIds": [ + "white-20", + ], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-4", + "snappedIds": [], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-6", - "snappedId": "black-10", + "snappedIds": [ + "black-10", + ], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-8", - "snappedId": "black-20", + "snappedIds": [ + "black-20", + ], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-1", - "snappedId": "white-15", + "snappedIds": [ + "white-15", + ], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-3", - "snappedId": "white-5", + "snappedIds": [ + "white-5", + ], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-5", + "snappedIds": [], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-7", - "snappedId": "black-15", + "snappedIds": [ + "black-15", + ], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-9", - "snappedId": "black-5", + "snappedIds": [ + "black-5", + ], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, + { + "depth": 45, + "height": 0.01, + "id": "score", + "max": 40, + "snappedIds": [], + "width": 45, + "y": -0.3, + }, ], }, "borderRadius": 0.8, @@ -1158,12 +1257,12 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e 0, ], ], - "height": 0.5, + "height": 0.6, "id": "board", "shape": "roundedTile", "texture": "board.ktx2", "width": 30, - "y": 0.25, + "y": 0.3, }, ], "messages": [], @@ -1242,7 +1341,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1258,7 +1357,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1274,7 +1373,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1290,7 +1389,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1306,7 +1405,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1322,7 +1421,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1338,7 +1437,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1354,7 +1453,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1370,7 +1469,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1386,7 +1485,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1402,7 +1501,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1418,7 +1517,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1434,7 +1533,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1450,7 +1549,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1466,7 +1565,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1482,7 +1581,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1498,7 +1597,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1514,7 +1613,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1530,7 +1629,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1546,7 +1645,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1562,7 +1661,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1578,7 +1677,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1594,7 +1693,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1610,7 +1709,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1626,7 +1725,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1642,7 +1741,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1658,7 +1757,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1674,7 +1773,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1690,7 +1789,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1706,7 +1805,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1722,7 +1821,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1738,7 +1837,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1754,7 +1853,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1770,7 +1869,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1786,7 +1885,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1802,7 +1901,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1818,7 +1917,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1834,7 +1933,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1850,7 +1949,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -1866,7 +1965,7 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "anchorable": { @@ -1875,442 +1974,541 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e "diameter": 2.9, "height": 0.01, "id": "pawn-0-0", - "snappedId": "white-6", + "snappedIds": [ + "white-6", + ], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-2", - "snappedId": "white-16", + "snappedIds": [ + "white-16", + ], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-4", + "snappedIds": [], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-6", - "snappedId": "black-6", + "snappedIds": [ + "black-6", + ], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-8", - "snappedId": "black-16", + "snappedIds": [ + "black-16", + ], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-1", - "snappedId": "white-11", + "snappedIds": [ + "white-11", + ], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-3", - "snappedId": "white-1", + "snappedIds": [ + "white-1", + ], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-5", + "snappedIds": [], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-7", - "snappedId": "black-11", + "snappedIds": [ + "black-11", + ], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-9", - "snappedId": "black-1", + "snappedIds": [ + "black-1", + ], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-0", - "snappedId": "white-7", + "snappedIds": [ + "white-7", + ], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-2", - "snappedId": "white-17", + "snappedIds": [ + "white-17", + ], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-4", + "snappedIds": [], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-6", - "snappedId": "black-7", + "snappedIds": [ + "black-7", + ], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-8", - "snappedId": "black-17", + "snappedIds": [ + "black-17", + ], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-1", - "snappedId": "white-12", + "snappedIds": [ + "white-12", + ], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-3", - "snappedId": "white-2", + "snappedIds": [ + "white-2", + ], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-5", + "snappedIds": [], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-7", - "snappedId": "black-12", + "snappedIds": [ + "black-12", + ], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-9", - "snappedId": "black-2", + "snappedIds": [ + "black-2", + ], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-0", - "snappedId": "white-8", + "snappedIds": [ + "white-8", + ], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-2", - "snappedId": "white-18", + "snappedIds": [ + "white-18", + ], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-4", + "snappedIds": [], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-6", - "snappedId": "black-8", + "snappedIds": [ + "black-8", + ], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-8", - "snappedId": "black-18", + "snappedIds": [ + "black-18", + ], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-1", - "snappedId": "white-13", + "snappedIds": [ + "white-13", + ], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-3", - "snappedId": "white-3", + "snappedIds": [ + "white-3", + ], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-5", + "snappedIds": [], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-7", - "snappedId": "black-13", + "snappedIds": [ + "black-13", + ], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-9", - "snappedId": "black-3", + "snappedIds": [ + "black-3", + ], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-0", - "snappedId": "white-9", + "snappedIds": [ + "white-9", + ], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-2", - "snappedId": "white-19", + "snappedIds": [ + "white-19", + ], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-4", + "snappedIds": [], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-6", - "snappedId": "black-9", + "snappedIds": [ + "black-9", + ], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-8", - "snappedId": "black-19", + "snappedIds": [ + "black-19", + ], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-1", - "snappedId": "white-14", + "snappedIds": [ + "white-14", + ], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-3", - "snappedId": "white-4", + "snappedIds": [ + "white-4", + ], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-5", + "snappedIds": [], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-7", - "snappedId": "black-14", + "snappedIds": [ + "black-14", + ], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-9", - "snappedId": "black-4", + "snappedIds": [ + "black-4", + ], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-0", - "snappedId": "white-10", + "snappedIds": [ + "white-10", + ], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-2", - "snappedId": "white-20", + "snappedIds": [ + "white-20", + ], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-4", + "snappedIds": [], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-6", - "snappedId": "black-10", + "snappedIds": [ + "black-10", + ], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-8", - "snappedId": "black-20", + "snappedIds": [ + "black-20", + ], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-1", - "snappedId": "white-15", + "snappedIds": [ + "white-15", + ], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-3", - "snappedId": "white-5", + "snappedIds": [ + "white-5", + ], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-5", + "snappedIds": [], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-7", - "snappedId": "black-15", + "snappedIds": [ + "black-15", + ], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-9", - "snappedId": "black-5", + "snappedIds": [ + "black-5", + ], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, + { + "depth": 45, + "height": 0.01, + "id": "score", + "max": 40, + "snappedIds": [], + "width": 45, + "y": -0.3, + }, ], }, "borderRadius": 0.8, @@ -2353,12 +2551,12 @@ exports[`draughts game descriptor > askForParameters() + addPlayer() > enrolls e 0, ], ], - "height": 0.5, + "height": 0.6, "id": "board", "shape": "roundedTile", "texture": "board.ktx2", "width": 30, - "y": 0.25, + "y": 0.3, }, ], "messages": [], @@ -2446,7 +2644,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2462,7 +2660,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2478,7 +2676,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2494,7 +2692,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2510,7 +2708,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2526,7 +2724,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2542,7 +2740,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2558,7 +2756,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2574,7 +2772,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2590,7 +2788,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2606,7 +2804,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2622,7 +2820,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2638,7 +2836,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2654,7 +2852,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2670,7 +2868,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2686,7 +2884,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2702,7 +2900,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2718,7 +2916,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2734,7 +2932,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2750,7 +2948,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#e4e1dfff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2766,7 +2964,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2782,7 +2980,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2798,7 +2996,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2814,7 +3012,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2830,7 +3028,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2846,7 +3044,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2862,7 +3060,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2878,7 +3076,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2894,7 +3092,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2910,7 +3108,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2926,7 +3124,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2942,7 +3140,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2958,7 +3156,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2974,7 +3172,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -2990,7 +3188,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -3006,7 +3204,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -3022,7 +3220,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -3038,7 +3236,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -3054,7 +3252,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "diameter": 2.9, @@ -3070,7 +3268,7 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` ], }, "texture": "#633b21ff", - "y": 0.75, + "y": 0.85, }, { "anchorable": { @@ -3079,402 +3277,461 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` "diameter": 2.9, "height": 0.01, "id": "pawn-0-0", + "snappedIds": [], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-2", + "snappedIds": [], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-4", + "snappedIds": [], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-6", + "snappedIds": [], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-0-8", + "snappedIds": [], "x": -13.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-1", + "snappedIds": [], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-3", + "snappedIds": [], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-5", + "snappedIds": [], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-7", + "snappedIds": [], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-1-9", + "snappedIds": [], "x": -10.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-0", + "snappedIds": [], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-2", + "snappedIds": [], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-4", + "snappedIds": [], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-6", + "snappedIds": [], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-2-8", + "snappedIds": [], "x": -7.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-1", + "snappedIds": [], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-3", + "snappedIds": [], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-5", + "snappedIds": [], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-7", + "snappedIds": [], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-3-9", + "snappedIds": [], "x": -4.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-0", + "snappedIds": [], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-2", + "snappedIds": [], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-4", + "snappedIds": [], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-6", + "snappedIds": [], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-4-8", + "snappedIds": [], "x": -1.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-1", + "snappedIds": [], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-3", + "snappedIds": [], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-5", + "snappedIds": [], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-7", + "snappedIds": [], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-5-9", + "snappedIds": [], "x": 1.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-0", + "snappedIds": [], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-2", + "snappedIds": [], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-4", + "snappedIds": [], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-6", + "snappedIds": [], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-6-8", + "snappedIds": [], "x": 4.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-1", + "snappedIds": [], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-3", + "snappedIds": [], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-5", + "snappedIds": [], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-7", + "snappedIds": [], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-7-9", + "snappedIds": [], "x": 7.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-0", + "snappedIds": [], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": -13.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-2", + "snappedIds": [], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": -7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-4", + "snappedIds": [], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": -1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-6", + "snappedIds": [], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": 4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-8-8", + "snappedIds": [], "x": 10.5, - "y": 0.5, + "y": 0.3, "z": 10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-1", + "snappedIds": [], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": -10.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-3", + "snappedIds": [], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": -4.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-5", + "snappedIds": [], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": 1.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-7", + "snappedIds": [], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": 7.5, }, { "diameter": 2.9, "height": 0.01, "id": "pawn-9-9", + "snappedIds": [], "x": 13.5, - "y": 0.5, + "y": 0.3, "z": 13.5, }, + { + "depth": 45, + "height": 0.01, + "id": "score", + "max": 40, + "snappedIds": [], + "width": 45, + "y": -0.3, + }, ], }, "borderRadius": 0.8, @@ -3517,12 +3774,12 @@ exports[`draughts game descriptor > build() > builds game setup 1`] = ` 0, ], ], - "height": 0.5, + "height": 0.6, "id": "board", "shape": "roundedTile", "texture": "board.ktx2", "width": 30, - "y": 0.25, + "y": 0.3, }, ], "slots": [ diff --git a/apps/games/draughts/index.js b/apps/games/draughts/index.js index de332a34..88eeebb9 100644 --- a/apps/games/draughts/index.js +++ b/apps/games/draughts/index.js @@ -1,8 +1,9 @@ // @ts-check export { build } from './logic/build.js' export { addPlayer, askForParameters } from './logic/players.js' +export { computeScore } from './logic/score.js' -/** @type {import('@tabulous/types').GameDescriptor['locales']} */ +/** @type {import('@tabulous/types').GameDescriptor['locales']} */ export const locales = { fr: { title: 'Dames' }, en: { title: 'Draughts' } @@ -18,18 +19,18 @@ export const minSeats = 2 export const maxSeats = 2 -/** @type {import('@tabulous/types').GameDescriptor['tableSpec']} */ +/** @type {import('@tabulous/types').GameDescriptor['tableSpec']} */ export const tableSpec = { texture: '/table-textures/wood-4.webp', width: 100, height: 100 } -/** @type {import('@tabulous/types').GameDescriptor['zoomSpec']} */ +/** @type {import('@tabulous/types').GameDescriptor['zoomSpec']} */ export const zoomSpec = { min: 20 } // https://coolors.co/ebd8c3-fbe0e0-ffeeee-bd5d2c-6d938e -/** @type {import('@tabulous/types').GameDescriptor['colors']} */ +/** @type {import('@tabulous/types').GameDescriptor['colors']} */ export const colors = { base: '#ebd8c3', primary: '#fbe0e0', @@ -38,5 +39,5 @@ export const colors = { } // disable all button actions -/** @type {import('@tabulous/types').GameDescriptor['actions']} */ +/** @type {import('@tabulous/types').GameDescriptor['actions']} */ export const actions = {} diff --git a/apps/games/draughts/index.test.js b/apps/games/draughts/index.test.js index 674efe53..214c1f24 100644 --- a/apps/games/draughts/index.test.js +++ b/apps/games/draughts/index.test.js @@ -1,6 +1,111 @@ // @ts-check +import { findMesh, snapTo, stackMeshes } from '@tabulous/game-utils' import { buildDescriptorTestSuite } from '@tabulous/game-utils/tests/game.js' +import { beforeEach, describe, expect, it } from 'vitest' import * as descriptor from '.' +import { blackId, ids, whiteId } from './logic/constants.js' -buildDescriptorTestSuite('draughts', descriptor) +buildDescriptorTestSuite('draughts', descriptor, utils => { + describe('computeScore', () => { + const player = utils.makePlayer(0) + const player2 = utils.makePlayer(1) + /** @type {import('@tabulous/types').Mesh[]} */ + let meshes + + beforeEach(async () => { + ;({ meshes } = await utils.buildGame({ + ...descriptor, + name: utils.name + })) + }) + + it('computes on snap', async () => { + const pawn = findMesh(`${blackId}-1`, meshes) + snapTo(ids.scoreAnchor, pawn, meshes) + expect( + descriptor.computeScore( + { + fn: 'snap', + args: [pawn.id, ids.scoreAnchor, true], + fromHand: false, + meshId: pawn.id + }, + { meshes, handMeshes: [], history: [] }, + [player], + [{ playerId: player.id, side: whiteId }] + ) + ).toEqual({ [player.id]: { total: 1 } }) + }) + + it('computes on unsnap', async () => { + const pawn1 = findMesh(`${blackId}-1`, meshes) + const pawn2 = findMesh(`${blackId}-2`, meshes) + const pawn3 = findMesh(`${whiteId}-1`, meshes) + snapTo(ids.scoreAnchor, pawn1, meshes) + snapTo(ids.scoreAnchor, pawn2, meshes) + snapTo(ids.scoreAnchor, pawn3, meshes) + expect( + descriptor.computeScore( + { + fn: 'unsnap', + args: [pawn1.id, ids.scoreAnchor], + fromHand: false, + meshId: pawn1.id + }, + { meshes, handMeshes: [], history: [] }, + [player, player2], + [ + { playerId: player.id, side: whiteId }, + { playerId: player2.id, side: blackId } + ] + ) + ).toEqual({ [player.id]: { total: 2 }, [player2.id]: { total: 1 } }) + }) + + it('computes on push', async () => { + const pawn1 = findMesh(`${blackId}-1`, meshes) + const pawn2 = findMesh(`${blackId}-2`, meshes) + snapTo(ids.scoreAnchor, pawn1, meshes) + stackMeshes([pawn1, pawn2]) + expect( + descriptor.computeScore( + { + fn: 'push', + args: [pawn2.id, true], + fromHand: false, + meshId: pawn1.id + }, + { meshes, handMeshes: [], history: [] }, + [player, player2], + [ + { playerId: player.id, side: whiteId }, + { playerId: player2.id, side: blackId } + ] + ) + ).toEqual({ [player.id]: { total: 2 }, [player2.id]: { total: 0 } }) + }) + + it.each([ + { + fn: /** @type {const} */ ('snap'), + args: [`${blackId}-1`, `${ids.pawnAnchor}-0-0`, true] + }, + { + fn: /** @type {const} */ ('unsnap'), + args: [`${blackId}-1`, `${ids.pawnAnchor}-0-0`, true] + } + ])('ignores $fn outside the score anchor', async ({ fn, args }) => { + const pawn = findMesh(`${blackId}-1`, meshes) + snapTo(ids.scoreAnchor, pawn, meshes) + expect( + descriptor.computeScore( + { fn, args, fromHand: false, meshId: pawn.id }, + { meshes, handMeshes: [], history: [] }, + [player], + [{ playerId: player.id, side: whiteId }] + ) + ).toBeUndefined() + }) + }) +}) diff --git a/apps/games/draughts/logic/builders/board.js b/apps/games/draughts/logic/builders/board.js index 77a02e1a..4bc4aaff 100644 --- a/apps/games/draughts/logic/builders/board.js +++ b/apps/games/draughts/logic/builders/board.js @@ -1,16 +1,16 @@ // @ts-check -import { counts, faceUVs, sizes } from '../constants.js' +import { counts, faceUVs, ids, sizes } from '../constants.js' /** @returns {import('@tabulous/types').Mesh} */ export function buildBoard() { return { shape: 'roundedTile', - id: 'board', + id: ids.board, texture: 'board.ktx2', faceUV: faceUVs.board, y: sizes.board.height / 2, ...sizes.board, - anchorable: { anchors: buildPawnAnchors() } + anchorable: { anchors: [...buildPawnAnchors(), buildScoreAnchor()] } } } @@ -23,22 +23,37 @@ function buildPawnAnchors() { } const position = { x: sizes.board.width * -0.5 + (sizes.board.width / counts.columns) * 0.5, - y: sizes.board.height, + y: sizes.board.height * 0.5, z: sizes.board.depth * -0.5 + (sizes.board.depth / counts.columns) * 0.5 } for (let column = 0; column < counts.columns; column++) { for (let row = 0; row < counts.columns; row++) { if ((row % 2 && column % 2) || (!(row % 2) && !(column % 2))) { anchors.push({ - id: `pawn-${column}-${row}`, + id: `${ids.pawnAnchor}-${column}-${row}`, x: position.x + spacing.x * column, y: position.y, z: position.z + spacing.z * row, ...sizes.pawn, - height: 0.01 + height: 0.01, + snappedIds: [] }) } } } return anchors } + +/** @returns {import('@tabulous/types').Anchor} */ +function buildScoreAnchor() { + const extent = 1.5 + return { + id: ids.scoreAnchor, + y: sizes.board.height * -0.5, + width: sizes.board.width * extent, + height: 0.01, + depth: sizes.board.depth * extent, + snappedIds: [], + max: counts.pawns * 2 + } +} diff --git a/apps/games/draughts/logic/constants.js b/apps/games/draughts/logic/constants.js index 2f1250e4..2961a200 100644 --- a/apps/games/draughts/logic/constants.js +++ b/apps/games/draughts/logic/constants.js @@ -1,6 +1,11 @@ // @ts-check /** @typedef {'white'|'black'} Side */ +/** + * @typedef {object} Parameters + * @property {Side} side - player and pieces color; + */ + export const counts = { pawns: 20, columns: 10 } /** @type {Side} */ @@ -9,6 +14,12 @@ export const blackId = 'black' /** @type {Side} */ export const whiteId = 'white' +export const ids = { + board: 'board', + pawnAnchor: 'pawn', + scoreAnchor: 'score' +} + /** @type {Record} */ export const colors = { [blackId]: '#633b21ff', @@ -16,7 +27,7 @@ export const colors = { } export const sizes = { - board: { depth: 30, width: 30, height: 0.5, borderRadius: 0.8 }, + board: { depth: 30, width: 30, height: 0.6, borderRadius: 0.8 }, pawn: { diameter: 2.9, height: 0.5 } } diff --git a/apps/games/draughts/logic/players.js b/apps/games/draughts/logic/players.js index 9b485809..fa120066 100644 --- a/apps/games/draughts/logic/players.js +++ b/apps/games/draughts/logic/players.js @@ -3,12 +3,7 @@ import { buildCameraPosition, findAvailableValues } from '@tabulous/game-utils' import { blackId, cameraPositions, whiteId } from './constants.js' -/** - * @typedef {object} Parameters - * @property {import('./constants').Side} side - player and pieces color; - */ - -/** @type {import('@tabulous/types').AskForParameters} */ +/** @type {import('@tabulous/types').AskForParameters} */ export function askForParameters({ game: { preferences } }) { const sides = findAvailableValues(preferences, 'side', [whiteId, blackId]) return sides.length <= 1 @@ -28,7 +23,7 @@ export function askForParameters({ game: { preferences } }) { } } -/** @type {import('@tabulous/types').AddPlayer} */ +/** @type {import('@tabulous/types').AddPlayer} */ export function addPlayer(game, player, parameters) { const { cameras, preferences } = game // use selected preferences, or look for the first player color, and choose the other one. diff --git a/apps/games/draughts/logic/score.js b/apps/games/draughts/logic/score.js new file mode 100644 index 00000000..c1aae3a3 --- /dev/null +++ b/apps/games/draughts/logic/score.js @@ -0,0 +1,46 @@ +// @ts-check +import { + findAnchor, + findMesh, + findPlayerPreferences +} from '@tabulous/game-utils' + +import { ids } from './constants.js' + +/** @type {import('@tabulous/types').ComputeScore} */ +export function computeScore(action, state, players, preferences) { + if ( + action === null || + action.fn === 'pop' || + action.fn === 'push' || + ((action.fn === 'snap' || action.fn === 'unsnap') && + action.args[1] === ids.scoreAnchor) + ) { + return compute(state, players, preferences) + } +} + +function compute( + /** @type {import('@tabulous/types').EngineState} */ state, + /** @type {Pick[]} */ players, + /** @type {import('@tabulous/types').PlayerPreference[]} */ preferences +) { + /** @type {import('@tabulous/types').Scores} */ + const scores = {} + const anchor = findAnchor(ids.scoreAnchor, state.meshes) + for (const { id } of players) { + const { side } = findPlayerPreferences(preferences, id) + const total = + anchor.snappedIds.reduce((total, snappedId) => { + const mesh = findMesh(snappedId, state.meshes) + return ( + total + + (snappedId.startsWith(side ?? '') + ? 0 + : (mesh.stackable?.stackIds?.length ?? 0) + 1) + ) + }, 0) ?? 0 + scores[id] = { total } + } + return scores +} diff --git a/apps/games/klondike/__snapshots__/index.test.js.snap b/apps/games/klondike/__snapshots__/index.test.js.snap index 8121b256..91ac0547 100644 --- a/apps/games/klondike/__snapshots__/index.test.js.snap +++ b/apps/games/klondike/__snapshots__/index.test.js.snap @@ -40,7 +40,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-1-bottom", - "snappedId": "spades-13", + "snappedIds": [ + "spades-13", + ], "width": 3, "z": -1, }, @@ -48,6 +50,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-1-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -80,7 +83,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-2-bottom", - "snappedId": "diamonds-1", + "snappedIds": [ + "diamonds-1", + ], "width": 3, "z": -1, }, @@ -88,6 +93,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-2-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -120,6 +126,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-3-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -127,6 +134,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-3-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -159,7 +167,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-4-bottom", - "snappedId": "diamonds-3", + "snappedIds": [ + "diamonds-3", + ], "width": 3, "z": -1, }, @@ -167,6 +177,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-4-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -199,6 +210,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-5-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -206,6 +218,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-5-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -238,6 +251,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-6-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -245,6 +259,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-6-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -302,6 +317,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-7-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -309,6 +325,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-7-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -341,6 +358,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-8-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -348,6 +366,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-8-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -380,6 +399,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-9-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -387,6 +407,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-9-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -419,6 +440,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-10-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -426,6 +448,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-10-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -458,6 +481,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-11-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -465,6 +489,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-11-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -497,6 +522,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-12-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -504,6 +530,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-12-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -536,6 +563,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-13-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -543,6 +571,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "diamonds-13-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -575,6 +604,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-1-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -582,6 +612,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-1-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -614,6 +645,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-2-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -621,6 +653,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-2-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -653,6 +686,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-3-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -660,6 +694,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-3-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -692,6 +727,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-4-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -699,6 +735,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-4-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -731,6 +768,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-5-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -738,6 +776,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-5-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -770,6 +809,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-6-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -777,6 +817,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-6-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -809,6 +850,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-7-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -816,6 +858,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-7-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -848,6 +891,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-8-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -855,6 +899,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-8-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -887,6 +932,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-9-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -894,6 +940,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-9-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -926,6 +973,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-10-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -933,6 +981,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-10-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -965,6 +1014,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-11-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -972,6 +1022,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-11-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1004,6 +1055,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-12-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -1011,6 +1063,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-12-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1043,6 +1096,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-13-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -1050,6 +1104,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "clubs-13-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1082,6 +1137,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-1-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -1089,6 +1145,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-1-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1121,6 +1178,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-2-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -1128,6 +1186,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-2-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1160,6 +1219,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-3-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -1167,6 +1227,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-3-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1199,6 +1260,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-4-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -1206,6 +1268,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-4-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1238,7 +1301,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-5-bottom", - "snappedId": "hearts-4", + "snappedIds": [ + "hearts-4", + ], "width": 3, "z": -1, }, @@ -1246,6 +1311,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-5-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1278,7 +1344,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-6-bottom", - "snappedId": "hearts-5", + "snappedIds": [ + "hearts-5", + ], "width": 3, "z": -1, }, @@ -1286,6 +1354,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-6-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1318,7 +1387,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-7-bottom", - "snappedId": "hearts-6", + "snappedIds": [ + "hearts-6", + ], "width": 3, "z": -1, }, @@ -1326,6 +1397,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-7-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1358,7 +1430,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-8-bottom", - "snappedId": "hearts-7", + "snappedIds": [ + "hearts-7", + ], "width": 3, "z": -1, }, @@ -1366,6 +1440,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-8-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1398,7 +1473,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-9-bottom", - "snappedId": "hearts-8", + "snappedIds": [ + "hearts-8", + ], "width": 3, "z": -1, }, @@ -1406,6 +1483,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-9-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1438,7 +1516,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-10-bottom", - "snappedId": "hearts-9", + "snappedIds": [ + "hearts-9", + ], "width": 3, "z": -1, }, @@ -1446,6 +1526,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-10-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1478,6 +1559,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-11-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -1485,6 +1567,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-11-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1517,7 +1600,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-12-bottom", - "snappedId": "hearts-11", + "snappedIds": [ + "hearts-11", + ], "width": 3, "z": -1, }, @@ -1525,6 +1610,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-12-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1557,7 +1643,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-13-bottom", - "snappedId": "hearts-12", + "snappedIds": [ + "hearts-12", + ], "width": 3, "z": -1, }, @@ -1565,6 +1653,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "hearts-13-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1597,7 +1686,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-1-bottom", - "snappedId": "hearts-13", + "snappedIds": [ + "hearts-13", + ], "width": 3, "z": -1, }, @@ -1605,6 +1696,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-1-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1637,7 +1729,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-2-bottom", - "snappedId": "spades-1", + "snappedIds": [ + "spades-1", + ], "width": 3, "z": -1, }, @@ -1645,6 +1739,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-2-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1677,7 +1772,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-3-bottom", - "snappedId": "spades-2", + "snappedIds": [ + "spades-2", + ], "width": 3, "z": -1, }, @@ -1685,6 +1782,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-3-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1717,6 +1815,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-4-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -1724,6 +1823,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-4-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1756,7 +1856,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-5-bottom", - "snappedId": "spades-4", + "snappedIds": [ + "spades-4", + ], "width": 3, "z": -1, }, @@ -1764,6 +1866,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-5-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1796,7 +1899,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-6-bottom", - "snappedId": "spades-5", + "snappedIds": [ + "spades-5", + ], "width": 3, "z": -1, }, @@ -1804,6 +1909,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-6-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1836,7 +1942,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-7-bottom", - "snappedId": "spades-6", + "snappedIds": [ + "spades-6", + ], "width": 3, "z": -1, }, @@ -1844,6 +1952,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-7-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1876,7 +1985,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-8-bottom", - "snappedId": "spades-7", + "snappedIds": [ + "spades-7", + ], "width": 3, "z": -1, }, @@ -1884,6 +1995,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-8-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1916,6 +2028,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-9-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -1923,6 +2036,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-9-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1955,7 +2069,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-10-bottom", - "snappedId": "spades-9", + "snappedIds": [ + "spades-9", + ], "width": 3, "z": -1, }, @@ -1963,6 +2079,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-10-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -1995,7 +2112,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-11-bottom", - "snappedId": "spades-10", + "snappedIds": [ + "spades-10", + ], "width": 3, "z": -1, }, @@ -2003,6 +2122,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-11-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2035,7 +2155,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-12-bottom", - "snappedId": "spades-11", + "snappedIds": [ + "spades-11", + ], "width": 3, "z": -1, }, @@ -2043,6 +2165,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-12-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2075,6 +2198,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-13-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2082,6 +2206,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "spades-13-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2114,7 +2239,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "reserve", - "snappedId": "diamonds-6", + "snappedIds": [ + "diamonds-6", + ], "width": 3, "x": -12.25, "z": 11.5, @@ -2124,6 +2251,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "flip": false, "height": 0.01, "id": "discard", + "snappedIds": [], "width": 3, "x": -8.25, "z": 11.5, @@ -2135,6 +2263,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "diamonds", ], + "snappedIds": [], "width": 3, "x": -0.25, "z": 11.5, @@ -2146,6 +2275,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "clubs", ], + "snappedIds": [], "width": 3, "x": 3.75, "z": 11.5, @@ -2157,6 +2287,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "hearts", ], + "snappedIds": [], "width": 3, "x": 7.75, "z": 11.5, @@ -2168,6 +2299,7 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "spades", ], + "snappedIds": [], "width": 3, "x": 11.75, "z": 11.5, @@ -2176,7 +2308,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "column-1", - "snappedId": "diamonds-5", + "snappedIds": [ + "diamonds-5", + ], "width": 3, "x": -12.25, "z": 6.55, @@ -2185,7 +2319,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "column-2", - "snappedId": "diamonds-4", + "snappedIds": [ + "diamonds-4", + ], "width": 3, "x": -8.25, "z": 6.55, @@ -2194,7 +2330,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "column-3", - "snappedId": "diamonds-2", + "snappedIds": [ + "diamonds-2", + ], "width": 3, "x": -4.25, "z": 6.55, @@ -2203,7 +2341,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "column-4", - "snappedId": "spades-12", + "snappedIds": [ + "spades-12", + ], "width": 3, "x": -0.25, "z": 6.55, @@ -2212,7 +2352,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "column-5", - "snappedId": "spades-8", + "snappedIds": [ + "spades-8", + ], "width": 3, "x": 3.75, "z": 6.55, @@ -2221,7 +2363,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "column-6", - "snappedId": "spades-3", + "snappedIds": [ + "spades-3", + ], "width": 3, "x": 7.75, "z": 6.55, @@ -2230,7 +2374,9 @@ exports[`klondike game descriptor > askForParameters() + addPlayer() > enrolls e "depth": 4.25, "height": 0.01, "id": "column-7", - "snappedId": "hearts-10", + "snappedIds": [ + "hearts-10", + ], "width": 3, "x": 11.75, "z": 6.55, @@ -2348,6 +2494,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-1-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2355,6 +2502,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-1-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2385,6 +2533,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-2-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2392,6 +2541,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-2-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2422,6 +2572,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-3-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2429,6 +2580,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-3-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2459,6 +2611,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-4-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2466,6 +2619,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-4-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2496,6 +2650,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-5-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2503,6 +2658,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-5-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2533,6 +2689,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-6-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2540,6 +2697,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-6-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2570,6 +2728,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-7-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2577,6 +2736,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-7-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2607,6 +2767,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-8-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2614,6 +2775,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-8-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2644,6 +2806,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-9-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2651,6 +2814,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-9-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2681,6 +2845,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-10-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2688,6 +2853,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-10-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2718,6 +2884,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-11-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2725,6 +2892,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-11-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2755,6 +2923,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-12-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2762,6 +2931,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-12-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2792,6 +2962,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-13-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2799,6 +2970,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "diamonds-13-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2829,6 +3001,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-1-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2836,6 +3009,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-1-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2866,6 +3040,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-2-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2873,6 +3048,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-2-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2903,6 +3079,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-3-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2910,6 +3087,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-3-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2940,6 +3118,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-4-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2947,6 +3126,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-4-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -2977,6 +3157,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-5-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -2984,6 +3165,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-5-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3014,6 +3196,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-6-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3021,6 +3204,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-6-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3051,6 +3235,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-7-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3058,6 +3243,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-7-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3088,6 +3274,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-8-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3095,6 +3282,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-8-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3125,6 +3313,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-9-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3132,6 +3321,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-9-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3162,6 +3352,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-10-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3169,6 +3360,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-10-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3199,6 +3391,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-11-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3206,6 +3399,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-11-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3236,6 +3430,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-12-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3243,6 +3438,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-12-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3273,6 +3469,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-13-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3280,6 +3477,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "clubs-13-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3310,6 +3508,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-1-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3317,6 +3516,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-1-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3347,6 +3547,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-2-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3354,6 +3555,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-2-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3384,6 +3586,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-3-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3391,6 +3594,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-3-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3421,6 +3625,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-4-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3428,6 +3633,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-4-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3458,6 +3664,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-5-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3465,6 +3672,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-5-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3495,6 +3703,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-6-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3502,6 +3711,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-6-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3532,6 +3742,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-7-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3539,6 +3750,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-7-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3569,6 +3781,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-8-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3576,6 +3789,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-8-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3606,6 +3820,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-9-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3613,6 +3828,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-9-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3643,6 +3859,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-10-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3650,6 +3867,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-10-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3680,6 +3898,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-11-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3687,6 +3906,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-11-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3717,6 +3937,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-12-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3724,6 +3945,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-12-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3754,6 +3976,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-13-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3761,6 +3984,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "hearts-13-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3791,6 +4015,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-1-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3798,6 +4023,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-1-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3828,6 +4054,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-2-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3835,6 +4062,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-2-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3865,6 +4093,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-3-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3872,6 +4101,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-3-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3902,6 +4132,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-4-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3909,6 +4140,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-4-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3939,6 +4171,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-5-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3946,6 +4179,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-5-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -3976,6 +4210,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-6-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -3983,6 +4218,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-6-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -4013,6 +4249,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-7-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -4020,6 +4257,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-7-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -4050,6 +4288,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-8-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -4057,6 +4296,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-8-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -4087,6 +4327,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-9-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -4094,6 +4335,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-9-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -4124,6 +4366,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-10-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -4131,6 +4374,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-10-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -4161,6 +4405,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-11-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -4168,6 +4413,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-11-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -4198,6 +4444,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-12-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -4205,6 +4452,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-12-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -4235,6 +4483,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-13-bottom", + "snappedIds": [], "width": 3, "z": -1, }, @@ -4242,6 +4491,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "spades-13-top", + "snappedIds": [], "width": 3, "z": 1, }, @@ -4272,6 +4522,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "reserve", + "snappedIds": [], "width": 3, "x": -12.25, "z": 11.5, @@ -4281,6 +4532,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "flip": false, "height": 0.01, "id": "discard", + "snappedIds": [], "width": 3, "x": -8.25, "z": 11.5, @@ -4292,6 +4544,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "kinds": [ "diamonds", ], + "snappedIds": [], "width": 3, "x": -0.25, "z": 11.5, @@ -4303,6 +4556,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "kinds": [ "clubs", ], + "snappedIds": [], "width": 3, "x": 3.75, "z": 11.5, @@ -4314,6 +4568,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "kinds": [ "hearts", ], + "snappedIds": [], "width": 3, "x": 7.75, "z": 11.5, @@ -4325,6 +4580,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "kinds": [ "spades", ], + "snappedIds": [], "width": 3, "x": 11.75, "z": 11.5, @@ -4333,6 +4589,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "column-1", + "snappedIds": [], "width": 3, "x": -12.25, "z": 6.55, @@ -4341,6 +4598,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "column-2", + "snappedIds": [], "width": 3, "x": -8.25, "z": 6.55, @@ -4349,6 +4607,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "column-3", + "snappedIds": [], "width": 3, "x": -4.25, "z": 6.55, @@ -4357,6 +4616,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "column-4", + "snappedIds": [], "width": 3, "x": -0.25, "z": 6.55, @@ -4365,6 +4625,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "column-5", + "snappedIds": [], "width": 3, "x": 3.75, "z": 6.55, @@ -4373,6 +4634,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "column-6", + "snappedIds": [], "width": 3, "x": 7.75, "z": 6.55, @@ -4381,6 +4643,7 @@ exports[`klondike game descriptor > build() > builds game setup 1`] = ` "depth": 4.25, "height": 0.01, "id": "column-7", + "snappedIds": [], "width": 3, "x": 11.75, "z": 6.55, diff --git a/apps/games/klondike/logic/builders/board.js b/apps/games/klondike/logic/builders/board.js index 79c1ffc7..40e273bb 100644 --- a/apps/games/klondike/logic/builders/board.js +++ b/apps/games/klondike/logic/builders/board.js @@ -20,8 +20,19 @@ export function buildBoard() { ...sizes.board, anchorable: { anchors: [ - { id: anchorIds.reserve, ...positions.reserve, ...sizes.card }, - { id: 'discard', ...positions.discard, ...sizes.card, flip: false }, + { + id: anchorIds.reserve, + ...positions.reserve, + ...sizes.card, + snappedIds: [] + }, + { + id: 'discard', + ...positions.discard, + ...sizes.card, + flip: false, + snappedIds: [] + }, ...buildGoalAnchors(), ...buildColumnAnchors() ] @@ -38,7 +49,8 @@ function buildGoalAnchors() { x: positions.goal.x + spacing.column.x * column, z: positions.goal.z, ...sizes.card, - kinds: [suit] + kinds: [suit], + snappedIds: [] }) } return anchors @@ -52,7 +64,8 @@ function buildColumnAnchors() { id: `${anchorIds.column}-${column + 1}`, x: positions.column.x + spacing.column.x * column, z: positions.column.z, - ...sizes.card + ...sizes.card, + snappedIds: [] }) } return anchors diff --git a/apps/games/klondike/logic/builders/cards.js b/apps/games/klondike/logic/builders/cards.js index dfd08295..5fc62c33 100644 --- a/apps/games/klondike/logic/builders/cards.js +++ b/apps/games/klondike/logic/builders/cards.js @@ -17,8 +17,18 @@ export function buildCards() { }, anchorable: { anchors: [ - { id: `${id}-bottom`, z: spacing.cardAnchor.z, ...sizes.card }, - { id: `${id}-top`, z: -spacing.cardAnchor.z, ...sizes.card } + { + id: `${id}-bottom`, + z: spacing.cardAnchor.z, + ...sizes.card, + snappedIds: [] + }, + { + id: `${id}-top`, + z: -spacing.cardAnchor.z, + ...sizes.card, + snappedIds: [] + } ] }, movable: { kind: suit }, diff --git a/apps/games/klondike/logic/players.js b/apps/games/klondike/logic/players.js index 3e27e5c6..ee8b993c 100644 --- a/apps/games/klondike/logic/players.js +++ b/apps/games/klondike/logic/players.js @@ -19,7 +19,8 @@ export function addPlayer(game, player) { ) const reserveId = - findAnchor(anchorIds.reserve, game.meshes)?.snappedId ?? 'reserve-not-found' + findAnchor(anchorIds.reserve, game.meshes)?.snappedIds[0] ?? + 'reserve-not-found' for (let column = 0; column < counts.columns; column++) { const anchorId = `${anchorIds.column}-${column + 1}` diff --git a/apps/games/mah-jong/__snapshots__/index.test.js.snap b/apps/games/mah-jong/__snapshots__/index.test.js.snap index 8fa691a0..8bb85383 100644 --- a/apps/games/mah-jong/__snapshots__/index.test.js.snap +++ b/apps/games/mah-jong/__snapshots__/index.test.js.snap @@ -44,7 +44,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-9-4", + "snappedIds": [ + "sou-9-4", + ], "width": 2.5, "x": -20.25, "z": 24, @@ -57,7 +59,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-1-2", + "snappedIds": [ + "wind-1-2", + ], "width": 2.5, "x": -17.75, "z": 24, @@ -70,7 +74,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-1-4", + "snappedIds": [ + "wind-1-4", + ], "width": 2.5, "x": -15.25, "z": 24, @@ -83,7 +89,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-2-2", + "snappedIds": [ + "wind-2-2", + ], "width": 2.5, "x": -12.75, "z": 24, @@ -96,7 +104,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-2-4", + "snappedIds": [ + "wind-2-4", + ], "width": 2.5, "x": -10.25, "z": 24, @@ -109,7 +119,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-3-2", + "snappedIds": [ + "wind-3-2", + ], "width": 2.5, "x": -7.75, "z": 24, @@ -122,7 +134,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-3-4", + "snappedIds": [ + "wind-3-4", + ], "width": 2.5, "x": -5.25, "z": 24, @@ -135,7 +149,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-4-2", + "snappedIds": [ + "wind-4-2", + ], "width": 2.5, "x": -2.75, "z": 24, @@ -148,7 +164,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-4-4", + "snappedIds": [ + "wind-4-4", + ], "width": 2.5, "x": -0.25, "z": 24, @@ -161,7 +179,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-1-2", + "snappedIds": [ + "dragon-1-2", + ], "width": 2.5, "x": 2.25, "z": 24, @@ -174,7 +194,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-1-4", + "snappedIds": [ + "dragon-1-4", + ], "width": 2.5, "x": 4.75, "z": 24, @@ -187,7 +209,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-2-2", + "snappedIds": [ + "dragon-2-2", + ], "width": 2.5, "x": 7.25, "z": 24, @@ -200,7 +224,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-2-4", + "snappedIds": [ + "dragon-2-4", + ], "width": 2.5, "x": 9.75, "z": 24, @@ -213,7 +239,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-3-2", + "snappedIds": [ + "dragon-3-2", + ], "width": 2.5, "x": 12.25, "z": 24, @@ -226,7 +254,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-3-4", + "snappedIds": [ + "dragon-3-4", + ], "width": 2.5, "x": 14.75, "z": 24, @@ -239,7 +269,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-1-2", + "snappedIds": [ + "man-1-2", + ], "width": 2.5, "x": 17.25, "z": 24, @@ -252,7 +284,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-1-4", + "snappedIds": [ + "man-1-4", + ], "width": 2.5, "x": 19.75, "z": 24, @@ -265,6 +299,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 11.2, @@ -277,6 +312,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 11.2, @@ -289,6 +325,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 11.2, @@ -301,6 +338,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 11.2, @@ -313,6 +351,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 11.2, @@ -325,6 +364,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 11.2, @@ -337,6 +377,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 15.325, @@ -349,6 +390,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 15.325, @@ -361,6 +403,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 15.325, @@ -373,6 +416,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 15.325, @@ -385,6 +429,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 15.325, @@ -397,6 +442,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 15.325, @@ -409,6 +455,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 19.45, @@ -421,6 +468,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 19.45, @@ -433,6 +481,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 19.45, @@ -445,6 +494,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 19.45, @@ -457,6 +507,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 19.45, @@ -469,10 +520,28 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 19.45, }, + { + "angle": 0, + "depth": 8, + "height": 0.1, + "id": "score-2", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [], + "width": 40, + "x": 0, + "z": 32, + }, { "angle": 1.5707963267948966, "depth": 2.5, @@ -481,7 +550,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-2-2", + "snappedIds": [ + "man-2-2", + ], "width": 3.75, "x": 24, "z": 20.25, @@ -494,7 +565,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-2-4", + "snappedIds": [ + "man-2-4", + ], "width": 3.75, "x": 24, "z": 17.75, @@ -507,7 +580,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-3-2", + "snappedIds": [ + "man-3-2", + ], "width": 3.75, "x": 24, "z": 15.25, @@ -520,7 +595,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-3-4", + "snappedIds": [ + "man-3-4", + ], "width": 3.75, "x": 24, "z": 12.75, @@ -533,7 +610,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-4-2", + "snappedIds": [ + "man-4-2", + ], "width": 3.75, "x": 24, "z": 10.25, @@ -546,7 +625,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-4-4", + "snappedIds": [ + "man-4-4", + ], "width": 3.75, "x": 24, "z": 7.75, @@ -559,7 +640,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-5-2", + "snappedIds": [ + "man-5-2", + ], "width": 3.75, "x": 24, "z": 5.25, @@ -572,7 +655,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-5-4", + "snappedIds": [ + "man-5-4", + ], "width": 3.75, "x": 24, "z": 2.75, @@ -585,7 +670,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-6-2", + "snappedIds": [ + "man-6-2", + ], "width": 3.75, "x": 24, "z": 0.25, @@ -598,7 +685,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-6-4", + "snappedIds": [ + "man-6-4", + ], "width": 3.75, "x": 24, "z": -2.25, @@ -611,7 +700,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-7-2", + "snappedIds": [ + "man-7-2", + ], "width": 3.75, "x": 24, "z": -4.75, @@ -624,7 +715,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-7-4", + "snappedIds": [ + "man-7-4", + ], "width": 3.75, "x": 24, "z": -7.25, @@ -637,7 +730,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-8-2", + "snappedIds": [ + "man-8-2", + ], "width": 3.75, "x": 24, "z": -9.75, @@ -650,7 +745,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-8-4", + "snappedIds": [ + "man-8-4", + ], "width": 3.75, "x": 24, "z": -12.25, @@ -663,7 +760,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-9-2", + "snappedIds": [ + "man-9-2", + ], "width": 3.75, "x": 24, "z": -14.75, @@ -676,7 +775,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-9-4", + "snappedIds": [ + "man-9-4", + ], "width": 3.75, "x": 24, "z": -17.25, @@ -689,7 +790,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-1-2", + "snappedIds": [ + "pei-1-2", + ], "width": 3.75, "x": 24, "z": -19.75, @@ -702,6 +805,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 8, @@ -714,6 +818,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 5, @@ -726,6 +831,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 2, @@ -738,6 +844,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -1, @@ -750,6 +857,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -4, @@ -762,6 +870,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -7, @@ -774,6 +883,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 8, @@ -786,6 +896,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 5, @@ -798,6 +909,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 2, @@ -810,6 +922,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -1, @@ -822,6 +935,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -4, @@ -834,6 +948,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -7, @@ -846,6 +961,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 8, @@ -858,6 +974,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 5, @@ -870,6 +987,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 2, @@ -882,6 +1000,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -1, @@ -894,6 +1013,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -4, @@ -906,10 +1026,28 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -7, }, + { + "angle": 1.5707963267948966, + "depth": 40, + "height": 0.1, + "id": "score-3", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [], + "width": 8, + "x": -32, + "z": 0, + }, { "angle": 3.141592653589793, "depth": 3.75, @@ -918,7 +1056,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-1-4", + "snappedIds": [ + "pei-1-4", + ], "width": 2.5, "x": -20.25, "z": -24, @@ -931,7 +1071,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-2-2", + "snappedIds": [ + "pei-2-2", + ], "width": 2.5, "x": -17.75, "z": -24, @@ -944,7 +1086,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-2-4", + "snappedIds": [ + "pei-2-4", + ], "width": 2.5, "x": -15.25, "z": -24, @@ -957,7 +1101,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-3-2", + "snappedIds": [ + "pei-3-2", + ], "width": 2.5, "x": -12.75, "z": -24, @@ -970,7 +1116,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-3-4", + "snappedIds": [ + "pei-3-4", + ], "width": 2.5, "x": -10.25, "z": -24, @@ -983,7 +1131,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-4-2", + "snappedIds": [ + "pei-4-2", + ], "width": 2.5, "x": -7.75, "z": -24, @@ -996,7 +1146,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-4-4", + "snappedIds": [ + "pei-4-4", + ], "width": 2.5, "x": -5.25, "z": -24, @@ -1009,7 +1161,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-5-2", + "snappedIds": [ + "pei-5-2", + ], "width": 2.5, "x": -2.75, "z": -24, @@ -1022,7 +1176,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-5-4", + "snappedIds": [ + "pei-5-4", + ], "width": 2.5, "x": -0.25, "z": -24, @@ -1035,7 +1191,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-6-2", + "snappedIds": [ + "pei-6-2", + ], "width": 2.5, "x": 2.25, "z": -24, @@ -1048,7 +1206,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-6-4", + "snappedIds": [ + "pei-6-4", + ], "width": 2.5, "x": 4.75, "z": -24, @@ -1061,7 +1221,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-7-2", + "snappedIds": [ + "pei-7-2", + ], "width": 2.5, "x": 7.25, "z": -24, @@ -1074,7 +1236,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-7-4", + "snappedIds": [ + "pei-7-4", + ], "width": 2.5, "x": 9.75, "z": -24, @@ -1087,7 +1251,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-8-2", + "snappedIds": [ + "pei-8-2", + ], "width": 2.5, "x": 12.25, "z": -24, @@ -1100,7 +1266,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-8-4", + "snappedIds": [ + "pei-8-4", + ], "width": 2.5, "x": 14.75, "z": -24, @@ -1113,7 +1281,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-9-2", + "snappedIds": [ + "pei-9-2", + ], "width": 2.5, "x": 17.25, "z": -24, @@ -1126,7 +1296,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-9-4", + "snappedIds": [ + "pei-9-4", + ], "width": 2.5, "x": 19.75, "z": -24, @@ -1139,6 +1311,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -11.2, @@ -1151,6 +1324,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -11.2, @@ -1163,6 +1337,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -11.2, @@ -1175,6 +1350,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -11.2, @@ -1187,6 +1363,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -11.2, @@ -1199,6 +1376,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -11.2, @@ -1211,6 +1389,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -15.325, @@ -1223,6 +1402,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -15.325, @@ -1235,6 +1415,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -15.325, @@ -1247,6 +1428,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -15.325, @@ -1259,6 +1441,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -15.325, @@ -1271,6 +1454,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -15.325, @@ -1283,6 +1467,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -19.45, @@ -1295,6 +1480,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -19.45, @@ -1307,6 +1493,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -19.45, @@ -1319,6 +1506,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -19.45, @@ -1331,6 +1519,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -19.45, @@ -1343,10 +1532,33 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -19.45, }, + { + "angle": 3.141592653589793, + "depth": 8, + "height": 0.1, + "id": "score-0", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [ + "stick-100-0", + "stick-1000-0", + "stick-5000-0", + "stick-10000-0", + ], + "width": 40, + "x": 0, + "z": -32, + }, { "angle": 4.71238898038469, "depth": 2.5, @@ -1355,7 +1567,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-1-2", + "snappedIds": [ + "sou-1-2", + ], "width": 3.75, "x": -24, "z": 20.25, @@ -1368,7 +1582,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-1-4", + "snappedIds": [ + "sou-1-4", + ], "width": 3.75, "x": -24, "z": 17.75, @@ -1381,7 +1597,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-2-2", + "snappedIds": [ + "sou-2-2", + ], "width": 3.75, "x": -24, "z": 15.25, @@ -1394,7 +1612,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-2-4", + "snappedIds": [ + "sou-2-4", + ], "width": 3.75, "x": -24, "z": 12.75, @@ -1407,7 +1627,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-3-2", + "snappedIds": [ + "sou-3-2", + ], "width": 3.75, "x": -24, "z": 10.25, @@ -1420,7 +1642,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-3-4", + "snappedIds": [ + "sou-3-4", + ], "width": 3.75, "x": -24, "z": 7.75, @@ -1433,7 +1657,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-4-2", + "snappedIds": [ + "sou-4-2", + ], "width": 3.75, "x": -24, "z": 5.25, @@ -1446,7 +1672,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-4-4", + "snappedIds": [ + "sou-4-4", + ], "width": 3.75, "x": -24, "z": 2.75, @@ -1459,7 +1687,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-5-2", + "snappedIds": [ + "sou-5-2", + ], "width": 3.75, "x": -24, "z": 0.25, @@ -1472,7 +1702,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-5-4", + "snappedIds": [ + "sou-5-4", + ], "width": 3.75, "x": -24, "z": -2.25, @@ -1485,7 +1717,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-6-2", + "snappedIds": [ + "sou-6-2", + ], "width": 3.75, "x": -24, "z": -4.75, @@ -1498,7 +1732,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-6-4", + "snappedIds": [ + "sou-6-4", + ], "width": 3.75, "x": -24, "z": -7.25, @@ -1511,7 +1747,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-7-2", + "snappedIds": [ + "sou-7-2", + ], "width": 3.75, "x": -24, "z": -9.75, @@ -1524,7 +1762,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-7-4", + "snappedIds": [ + "sou-7-4", + ], "width": 3.75, "x": -24, "z": -12.25, @@ -1537,7 +1777,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-8-2", + "snappedIds": [ + "sou-8-2", + ], "width": 3.75, "x": -24, "z": -14.75, @@ -1550,7 +1792,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-8-4", + "snappedIds": [ + "sou-8-4", + ], "width": 3.75, "x": -24, "z": -17.25, @@ -1563,7 +1807,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-9-2", + "snappedIds": [ + "sou-9-2", + ], "width": 3.75, "x": -24, "z": -19.75, @@ -1576,6 +1822,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 8, @@ -1588,6 +1835,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 5, @@ -1600,6 +1848,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 2, @@ -1612,6 +1861,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -1, @@ -1624,6 +1874,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -4, @@ -1636,6 +1887,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -7, @@ -1648,6 +1900,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 8, @@ -1660,6 +1913,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 5, @@ -1672,6 +1926,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 2, @@ -1684,6 +1939,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -1, @@ -1696,6 +1952,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -4, @@ -1708,6 +1965,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -7, @@ -1720,6 +1978,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 8, @@ -1732,6 +1991,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 5, @@ -1744,6 +2004,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 2, @@ -1756,6 +2017,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -1, @@ -1768,6 +2030,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -4, @@ -1780,10 +2043,28 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -7, }, + { + "angle": 4.71238898038469, + "depth": 40, + "height": 0.1, + "id": "score-1", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [], + "width": 8, + "x": 32, + "z": 0, + }, ], }, "id": "main-board", @@ -10912,9 +11193,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 10, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-100.ktx2", "transform": { @@ -10923,7 +11202,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -27, + "z": -29, }, { "diameter": 0.3, @@ -10959,9 +11238,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 4, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-1000.ktx2", "transform": { @@ -10970,7 +11247,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -28, + "z": -30, }, { "diameter": 0.3, @@ -11006,9 +11283,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 2, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-5000.ktx2", "transform": { @@ -11017,7 +11292,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -29, + "z": -31, }, { "diameter": 0.3, @@ -11053,9 +11328,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 1, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-10000.ktx2", "transform": { @@ -11064,7 +11337,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -30, + "z": -32, }, ], "messages": [], @@ -11140,7 +11413,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-9-4", + "snappedIds": [ + "sou-9-4", + ], "width": 2.5, "x": -20.25, "z": 24, @@ -11153,7 +11428,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-1-2", + "snappedIds": [ + "wind-1-2", + ], "width": 2.5, "x": -17.75, "z": 24, @@ -11166,7 +11443,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-1-4", + "snappedIds": [ + "wind-1-4", + ], "width": 2.5, "x": -15.25, "z": 24, @@ -11179,7 +11458,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-2-2", + "snappedIds": [ + "wind-2-2", + ], "width": 2.5, "x": -12.75, "z": 24, @@ -11192,7 +11473,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-2-4", + "snappedIds": [ + "wind-2-4", + ], "width": 2.5, "x": -10.25, "z": 24, @@ -11205,7 +11488,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-3-2", + "snappedIds": [ + "wind-3-2", + ], "width": 2.5, "x": -7.75, "z": 24, @@ -11218,7 +11503,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-3-4", + "snappedIds": [ + "wind-3-4", + ], "width": 2.5, "x": -5.25, "z": 24, @@ -11231,7 +11518,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-4-2", + "snappedIds": [ + "wind-4-2", + ], "width": 2.5, "x": -2.75, "z": 24, @@ -11244,7 +11533,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-4-4", + "snappedIds": [ + "wind-4-4", + ], "width": 2.5, "x": -0.25, "z": 24, @@ -11257,7 +11548,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-1-2", + "snappedIds": [ + "dragon-1-2", + ], "width": 2.5, "x": 2.25, "z": 24, @@ -11270,7 +11563,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-1-4", + "snappedIds": [ + "dragon-1-4", + ], "width": 2.5, "x": 4.75, "z": 24, @@ -11283,7 +11578,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-2-2", + "snappedIds": [ + "dragon-2-2", + ], "width": 2.5, "x": 7.25, "z": 24, @@ -11296,7 +11593,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-2-4", + "snappedIds": [ + "dragon-2-4", + ], "width": 2.5, "x": 9.75, "z": 24, @@ -11309,7 +11608,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-3-2", + "snappedIds": [ + "dragon-3-2", + ], "width": 2.5, "x": 12.25, "z": 24, @@ -11322,7 +11623,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-3-4", + "snappedIds": [ + "dragon-3-4", + ], "width": 2.5, "x": 14.75, "z": 24, @@ -11335,7 +11638,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-1-2", + "snappedIds": [ + "man-1-2", + ], "width": 2.5, "x": 17.25, "z": 24, @@ -11348,7 +11653,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-1-4", + "snappedIds": [ + "man-1-4", + ], "width": 2.5, "x": 19.75, "z": 24, @@ -11361,6 +11668,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 11.2, @@ -11373,6 +11681,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 11.2, @@ -11385,6 +11694,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 11.2, @@ -11397,6 +11707,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 11.2, @@ -11409,6 +11720,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 11.2, @@ -11421,6 +11733,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 11.2, @@ -11433,6 +11746,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 15.325, @@ -11445,6 +11759,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 15.325, @@ -11457,6 +11772,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 15.325, @@ -11469,6 +11785,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 15.325, @@ -11481,6 +11798,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 15.325, @@ -11493,6 +11811,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 15.325, @@ -11505,6 +11824,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 19.45, @@ -11517,6 +11837,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 19.45, @@ -11529,6 +11850,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 19.45, @@ -11541,6 +11863,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 19.45, @@ -11553,6 +11876,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 19.45, @@ -11565,10 +11889,28 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 19.45, }, + { + "angle": 0, + "depth": 8, + "height": 0.1, + "id": "score-2", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [], + "width": 40, + "x": 0, + "z": 32, + }, { "angle": 1.5707963267948966, "depth": 2.5, @@ -11577,7 +11919,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-2-2", + "snappedIds": [ + "man-2-2", + ], "width": 3.75, "x": 24, "z": 20.25, @@ -11590,7 +11934,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-2-4", + "snappedIds": [ + "man-2-4", + ], "width": 3.75, "x": 24, "z": 17.75, @@ -11603,7 +11949,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-3-2", + "snappedIds": [ + "man-3-2", + ], "width": 3.75, "x": 24, "z": 15.25, @@ -11616,7 +11964,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-3-4", + "snappedIds": [ + "man-3-4", + ], "width": 3.75, "x": 24, "z": 12.75, @@ -11629,7 +11979,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-4-2", + "snappedIds": [ + "man-4-2", + ], "width": 3.75, "x": 24, "z": 10.25, @@ -11642,7 +11994,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-4-4", + "snappedIds": [ + "man-4-4", + ], "width": 3.75, "x": 24, "z": 7.75, @@ -11655,7 +12009,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-5-2", + "snappedIds": [ + "man-5-2", + ], "width": 3.75, "x": 24, "z": 5.25, @@ -11668,7 +12024,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-5-4", + "snappedIds": [ + "man-5-4", + ], "width": 3.75, "x": 24, "z": 2.75, @@ -11681,7 +12039,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-6-2", + "snappedIds": [ + "man-6-2", + ], "width": 3.75, "x": 24, "z": 0.25, @@ -11694,7 +12054,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-6-4", + "snappedIds": [ + "man-6-4", + ], "width": 3.75, "x": 24, "z": -2.25, @@ -11707,7 +12069,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-7-2", + "snappedIds": [ + "man-7-2", + ], "width": 3.75, "x": 24, "z": -4.75, @@ -11720,7 +12084,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-7-4", + "snappedIds": [ + "man-7-4", + ], "width": 3.75, "x": 24, "z": -7.25, @@ -11733,7 +12099,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-8-2", + "snappedIds": [ + "man-8-2", + ], "width": 3.75, "x": 24, "z": -9.75, @@ -11746,7 +12114,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-8-4", + "snappedIds": [ + "man-8-4", + ], "width": 3.75, "x": 24, "z": -12.25, @@ -11759,7 +12129,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-9-2", + "snappedIds": [ + "man-9-2", + ], "width": 3.75, "x": 24, "z": -14.75, @@ -11772,7 +12144,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-9-4", + "snappedIds": [ + "man-9-4", + ], "width": 3.75, "x": 24, "z": -17.25, @@ -11785,7 +12159,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-1-2", + "snappedIds": [ + "pei-1-2", + ], "width": 3.75, "x": 24, "z": -19.75, @@ -11798,6 +12174,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 8, @@ -11810,6 +12187,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 5, @@ -11822,6 +12200,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 2, @@ -11834,6 +12213,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -1, @@ -11846,6 +12226,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -4, @@ -11858,6 +12239,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -7, @@ -11870,6 +12252,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 8, @@ -11882,6 +12265,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 5, @@ -11894,6 +12278,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 2, @@ -11906,6 +12291,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -1, @@ -11918,6 +12304,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -4, @@ -11930,6 +12317,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -7, @@ -11942,6 +12330,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 8, @@ -11954,6 +12343,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 5, @@ -11966,6 +12356,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 2, @@ -11978,6 +12369,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -1, @@ -11990,6 +12382,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -4, @@ -12002,10 +12395,28 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -7, }, + { + "angle": 1.5707963267948966, + "depth": 40, + "height": 0.1, + "id": "score-3", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [], + "width": 8, + "x": -32, + "z": 0, + }, { "angle": 3.141592653589793, "depth": 3.75, @@ -12014,7 +12425,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-1-4", + "snappedIds": [ + "pei-1-4", + ], "width": 2.5, "x": -20.25, "z": -24, @@ -12027,7 +12440,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-2-2", + "snappedIds": [ + "pei-2-2", + ], "width": 2.5, "x": -17.75, "z": -24, @@ -12040,7 +12455,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-2-4", + "snappedIds": [ + "pei-2-4", + ], "width": 2.5, "x": -15.25, "z": -24, @@ -12053,7 +12470,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-3-2", + "snappedIds": [ + "pei-3-2", + ], "width": 2.5, "x": -12.75, "z": -24, @@ -12066,7 +12485,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-3-4", + "snappedIds": [ + "pei-3-4", + ], "width": 2.5, "x": -10.25, "z": -24, @@ -12079,7 +12500,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-4-2", + "snappedIds": [ + "pei-4-2", + ], "width": 2.5, "x": -7.75, "z": -24, @@ -12092,7 +12515,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-4-4", + "snappedIds": [ + "pei-4-4", + ], "width": 2.5, "x": -5.25, "z": -24, @@ -12105,7 +12530,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-5-2", + "snappedIds": [ + "pei-5-2", + ], "width": 2.5, "x": -2.75, "z": -24, @@ -12118,7 +12545,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-5-4", + "snappedIds": [ + "pei-5-4", + ], "width": 2.5, "x": -0.25, "z": -24, @@ -12131,7 +12560,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-6-2", + "snappedIds": [ + "pei-6-2", + ], "width": 2.5, "x": 2.25, "z": -24, @@ -12144,7 +12575,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-6-4", + "snappedIds": [ + "pei-6-4", + ], "width": 2.5, "x": 4.75, "z": -24, @@ -12157,7 +12590,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-7-2", + "snappedIds": [ + "pei-7-2", + ], "width": 2.5, "x": 7.25, "z": -24, @@ -12170,7 +12605,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-7-4", + "snappedIds": [ + "pei-7-4", + ], "width": 2.5, "x": 9.75, "z": -24, @@ -12183,7 +12620,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-8-2", + "snappedIds": [ + "pei-8-2", + ], "width": 2.5, "x": 12.25, "z": -24, @@ -12196,7 +12635,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-8-4", + "snappedIds": [ + "pei-8-4", + ], "width": 2.5, "x": 14.75, "z": -24, @@ -12209,7 +12650,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-9-2", + "snappedIds": [ + "pei-9-2", + ], "width": 2.5, "x": 17.25, "z": -24, @@ -12222,7 +12665,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-9-4", + "snappedIds": [ + "pei-9-4", + ], "width": 2.5, "x": 19.75, "z": -24, @@ -12235,6 +12680,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -11.2, @@ -12247,6 +12693,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -11.2, @@ -12259,6 +12706,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -11.2, @@ -12271,6 +12719,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -11.2, @@ -12283,6 +12732,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -11.2, @@ -12295,6 +12745,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -11.2, @@ -12307,6 +12758,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -15.325, @@ -12319,6 +12771,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -15.325, @@ -12331,6 +12784,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -15.325, @@ -12343,6 +12797,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -15.325, @@ -12355,6 +12810,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -15.325, @@ -12367,6 +12823,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -15.325, @@ -12379,6 +12836,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -19.45, @@ -12391,6 +12849,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -19.45, @@ -12403,6 +12862,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -19.45, @@ -12415,6 +12875,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -19.45, @@ -12427,6 +12888,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -19.45, @@ -12439,10 +12901,33 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -19.45, }, + { + "angle": 3.141592653589793, + "depth": 8, + "height": 0.1, + "id": "score-0", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [ + "stick-100-0", + "stick-1000-0", + "stick-5000-0", + "stick-10000-0", + ], + "width": 40, + "x": 0, + "z": -32, + }, { "angle": 4.71238898038469, "depth": 2.5, @@ -12451,7 +12936,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-1-2", + "snappedIds": [ + "sou-1-2", + ], "width": 3.75, "x": -24, "z": 20.25, @@ -12464,7 +12951,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-1-4", + "snappedIds": [ + "sou-1-4", + ], "width": 3.75, "x": -24, "z": 17.75, @@ -12477,7 +12966,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-2-2", + "snappedIds": [ + "sou-2-2", + ], "width": 3.75, "x": -24, "z": 15.25, @@ -12490,7 +12981,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-2-4", + "snappedIds": [ + "sou-2-4", + ], "width": 3.75, "x": -24, "z": 12.75, @@ -12503,7 +12996,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-3-2", + "snappedIds": [ + "sou-3-2", + ], "width": 3.75, "x": -24, "z": 10.25, @@ -12516,7 +13011,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-3-4", + "snappedIds": [ + "sou-3-4", + ], "width": 3.75, "x": -24, "z": 7.75, @@ -12529,7 +13026,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-4-2", + "snappedIds": [ + "sou-4-2", + ], "width": 3.75, "x": -24, "z": 5.25, @@ -12542,7 +13041,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-4-4", + "snappedIds": [ + "sou-4-4", + ], "width": 3.75, "x": -24, "z": 2.75, @@ -12555,7 +13056,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-5-2", + "snappedIds": [ + "sou-5-2", + ], "width": 3.75, "x": -24, "z": 0.25, @@ -12568,7 +13071,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-5-4", + "snappedIds": [ + "sou-5-4", + ], "width": 3.75, "x": -24, "z": -2.25, @@ -12581,7 +13086,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-6-2", + "snappedIds": [ + "sou-6-2", + ], "width": 3.75, "x": -24, "z": -4.75, @@ -12594,7 +13101,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-6-4", + "snappedIds": [ + "sou-6-4", + ], "width": 3.75, "x": -24, "z": -7.25, @@ -12607,7 +13116,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-7-2", + "snappedIds": [ + "sou-7-2", + ], "width": 3.75, "x": -24, "z": -9.75, @@ -12620,7 +13131,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-7-4", + "snappedIds": [ + "sou-7-4", + ], "width": 3.75, "x": -24, "z": -12.25, @@ -12633,7 +13146,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-8-2", + "snappedIds": [ + "sou-8-2", + ], "width": 3.75, "x": -24, "z": -14.75, @@ -12646,7 +13161,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-8-4", + "snappedIds": [ + "sou-8-4", + ], "width": 3.75, "x": -24, "z": -17.25, @@ -12659,7 +13176,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-9-2", + "snappedIds": [ + "sou-9-2", + ], "width": 3.75, "x": -24, "z": -19.75, @@ -12672,6 +13191,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 8, @@ -12684,6 +13204,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 5, @@ -12696,6 +13217,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 2, @@ -12708,6 +13230,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -1, @@ -12720,6 +13243,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -4, @@ -12732,6 +13256,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -7, @@ -12744,6 +13269,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 8, @@ -12756,6 +13282,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 5, @@ -12768,6 +13295,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 2, @@ -12780,6 +13308,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -1, @@ -12792,6 +13321,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -4, @@ -12804,6 +13334,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -7, @@ -12816,6 +13347,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 8, @@ -12828,6 +13360,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 5, @@ -12840,6 +13373,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 2, @@ -12852,6 +13386,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -1, @@ -12864,6 +13399,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -4, @@ -12876,10 +13412,33 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -7, }, + { + "angle": 4.71238898038469, + "depth": 40, + "height": 0.1, + "id": "score-1", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [ + "stick-100-1", + "stick-1000-1", + "stick-5000-1", + "stick-10000-1", + ], + "width": 8, + "x": 32, + "z": 0, + }, ], }, "id": "main-board", @@ -22008,9 +22567,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 10, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-100.ktx2", "transform": { @@ -22019,7 +22576,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -27, + "z": -29, }, { "diameter": 0.3, @@ -22055,9 +22612,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 4, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-1000.ktx2", "transform": { @@ -22066,7 +22621,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -28, + "z": -30, }, { "diameter": 0.3, @@ -22102,9 +22657,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 2, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-5000.ktx2", "transform": { @@ -22113,7 +22666,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -29, + "z": -31, }, { "diameter": 0.3, @@ -22149,9 +22702,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 1, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-10000.ktx2", "transform": { @@ -22160,7 +22711,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -30, + "z": -32, }, { "diameter": 0.3, @@ -22196,16 +22747,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 10, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-100.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -27, + "x": -29, "y": 0.15, "z": 0, }, @@ -22243,16 +22792,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 4, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-1000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -28, + "x": -30, "y": 0.15, "z": 0, }, @@ -22290,16 +22837,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 2, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-5000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -29, + "x": -31, "y": 0.15, "z": 0, }, @@ -22337,16 +22882,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 1, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-10000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -30, + "x": -32, "y": 0.15, "z": 0, }, @@ -22443,7 +22986,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-9-4", + "snappedIds": [ + "sou-9-4", + ], "width": 2.5, "x": -20.25, "z": 24, @@ -22456,7 +23001,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-1-2", + "snappedIds": [ + "wind-1-2", + ], "width": 2.5, "x": -17.75, "z": 24, @@ -22469,7 +23016,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-1-4", + "snappedIds": [ + "wind-1-4", + ], "width": 2.5, "x": -15.25, "z": 24, @@ -22482,7 +23031,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-2-2", + "snappedIds": [ + "wind-2-2", + ], "width": 2.5, "x": -12.75, "z": 24, @@ -22495,7 +23046,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-2-4", + "snappedIds": [ + "wind-2-4", + ], "width": 2.5, "x": -10.25, "z": 24, @@ -22508,7 +23061,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-3-2", + "snappedIds": [ + "wind-3-2", + ], "width": 2.5, "x": -7.75, "z": 24, @@ -22521,7 +23076,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-3-4", + "snappedIds": [ + "wind-3-4", + ], "width": 2.5, "x": -5.25, "z": 24, @@ -22534,7 +23091,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-4-2", + "snappedIds": [ + "wind-4-2", + ], "width": 2.5, "x": -2.75, "z": 24, @@ -22547,7 +23106,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-4-4", + "snappedIds": [ + "wind-4-4", + ], "width": 2.5, "x": -0.25, "z": 24, @@ -22560,7 +23121,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-1-2", + "snappedIds": [ + "dragon-1-2", + ], "width": 2.5, "x": 2.25, "z": 24, @@ -22573,7 +23136,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-1-4", + "snappedIds": [ + "dragon-1-4", + ], "width": 2.5, "x": 4.75, "z": 24, @@ -22586,7 +23151,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-2-2", + "snappedIds": [ + "dragon-2-2", + ], "width": 2.5, "x": 7.25, "z": 24, @@ -22599,7 +23166,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-2-4", + "snappedIds": [ + "dragon-2-4", + ], "width": 2.5, "x": 9.75, "z": 24, @@ -22612,7 +23181,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-3-2", + "snappedIds": [ + "dragon-3-2", + ], "width": 2.5, "x": 12.25, "z": 24, @@ -22625,7 +23196,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-3-4", + "snappedIds": [ + "dragon-3-4", + ], "width": 2.5, "x": 14.75, "z": 24, @@ -22638,7 +23211,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-1-2", + "snappedIds": [ + "man-1-2", + ], "width": 2.5, "x": 17.25, "z": 24, @@ -22651,7 +23226,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-1-4", + "snappedIds": [ + "man-1-4", + ], "width": 2.5, "x": 19.75, "z": 24, @@ -22664,6 +23241,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 11.2, @@ -22676,6 +23254,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 11.2, @@ -22688,6 +23267,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 11.2, @@ -22700,6 +23280,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 11.2, @@ -22712,6 +23293,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 11.2, @@ -22724,6 +23306,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 11.2, @@ -22736,6 +23319,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 15.325, @@ -22748,6 +23332,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 15.325, @@ -22760,6 +23345,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 15.325, @@ -22772,6 +23358,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 15.325, @@ -22784,6 +23371,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 15.325, @@ -22796,6 +23384,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 15.325, @@ -22808,6 +23397,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 19.45, @@ -22820,6 +23410,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 19.45, @@ -22832,6 +23423,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 19.45, @@ -22844,6 +23436,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 19.45, @@ -22856,6 +23449,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 19.45, @@ -22868,10 +23462,33 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 19.45, }, + { + "angle": 0, + "depth": 8, + "height": 0.1, + "id": "score-2", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [ + "stick-100-2", + "stick-1000-2", + "stick-5000-2", + "stick-10000-2", + ], + "width": 40, + "x": 0, + "z": 32, + }, { "angle": 1.5707963267948966, "depth": 2.5, @@ -22880,7 +23497,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-2-2", + "snappedIds": [ + "man-2-2", + ], "width": 3.75, "x": 24, "z": 20.25, @@ -22893,7 +23512,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-2-4", + "snappedIds": [ + "man-2-4", + ], "width": 3.75, "x": 24, "z": 17.75, @@ -22906,7 +23527,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-3-2", + "snappedIds": [ + "man-3-2", + ], "width": 3.75, "x": 24, "z": 15.25, @@ -22919,7 +23542,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-3-4", + "snappedIds": [ + "man-3-4", + ], "width": 3.75, "x": 24, "z": 12.75, @@ -22932,7 +23557,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-4-2", + "snappedIds": [ + "man-4-2", + ], "width": 3.75, "x": 24, "z": 10.25, @@ -22945,7 +23572,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-4-4", + "snappedIds": [ + "man-4-4", + ], "width": 3.75, "x": 24, "z": 7.75, @@ -22958,7 +23587,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-5-2", + "snappedIds": [ + "man-5-2", + ], "width": 3.75, "x": 24, "z": 5.25, @@ -22971,7 +23602,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-5-4", + "snappedIds": [ + "man-5-4", + ], "width": 3.75, "x": 24, "z": 2.75, @@ -22984,7 +23617,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-6-2", + "snappedIds": [ + "man-6-2", + ], "width": 3.75, "x": 24, "z": 0.25, @@ -22997,7 +23632,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-6-4", + "snappedIds": [ + "man-6-4", + ], "width": 3.75, "x": 24, "z": -2.25, @@ -23010,7 +23647,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-7-2", + "snappedIds": [ + "man-7-2", + ], "width": 3.75, "x": 24, "z": -4.75, @@ -23023,7 +23662,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-7-4", + "snappedIds": [ + "man-7-4", + ], "width": 3.75, "x": 24, "z": -7.25, @@ -23036,7 +23677,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-8-2", + "snappedIds": [ + "man-8-2", + ], "width": 3.75, "x": 24, "z": -9.75, @@ -23049,7 +23692,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-8-4", + "snappedIds": [ + "man-8-4", + ], "width": 3.75, "x": 24, "z": -12.25, @@ -23062,7 +23707,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-9-2", + "snappedIds": [ + "man-9-2", + ], "width": 3.75, "x": 24, "z": -14.75, @@ -23075,7 +23722,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-9-4", + "snappedIds": [ + "man-9-4", + ], "width": 3.75, "x": 24, "z": -17.25, @@ -23088,7 +23737,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-1-2", + "snappedIds": [ + "pei-1-2", + ], "width": 3.75, "x": 24, "z": -19.75, @@ -23101,6 +23752,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 8, @@ -23113,6 +23765,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 5, @@ -23125,6 +23778,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 2, @@ -23137,6 +23791,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -1, @@ -23149,6 +23804,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -4, @@ -23161,6 +23817,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -7, @@ -23173,6 +23830,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 8, @@ -23185,6 +23843,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 5, @@ -23197,6 +23856,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 2, @@ -23209,6 +23869,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -1, @@ -23221,6 +23882,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -4, @@ -23233,6 +23895,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -7, @@ -23245,6 +23908,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 8, @@ -23257,6 +23921,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 5, @@ -23269,6 +23934,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 2, @@ -23281,6 +23947,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -1, @@ -23293,6 +23960,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -4, @@ -23305,10 +23973,28 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -7, }, + { + "angle": 1.5707963267948966, + "depth": 40, + "height": 0.1, + "id": "score-3", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [], + "width": 8, + "x": -32, + "z": 0, + }, { "angle": 3.141592653589793, "depth": 3.75, @@ -23317,7 +24003,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-1-4", + "snappedIds": [ + "pei-1-4", + ], "width": 2.5, "x": -20.25, "z": -24, @@ -23330,7 +24018,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-2-2", + "snappedIds": [ + "pei-2-2", + ], "width": 2.5, "x": -17.75, "z": -24, @@ -23343,7 +24033,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-2-4", + "snappedIds": [ + "pei-2-4", + ], "width": 2.5, "x": -15.25, "z": -24, @@ -23356,7 +24048,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-3-2", + "snappedIds": [ + "pei-3-2", + ], "width": 2.5, "x": -12.75, "z": -24, @@ -23369,7 +24063,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-3-4", + "snappedIds": [ + "pei-3-4", + ], "width": 2.5, "x": -10.25, "z": -24, @@ -23382,7 +24078,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-4-2", + "snappedIds": [ + "pei-4-2", + ], "width": 2.5, "x": -7.75, "z": -24, @@ -23395,7 +24093,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-4-4", + "snappedIds": [ + "pei-4-4", + ], "width": 2.5, "x": -5.25, "z": -24, @@ -23408,7 +24108,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-5-2", + "snappedIds": [ + "pei-5-2", + ], "width": 2.5, "x": -2.75, "z": -24, @@ -23421,7 +24123,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-5-4", + "snappedIds": [ + "pei-5-4", + ], "width": 2.5, "x": -0.25, "z": -24, @@ -23434,7 +24138,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-6-2", + "snappedIds": [ + "pei-6-2", + ], "width": 2.5, "x": 2.25, "z": -24, @@ -23447,7 +24153,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-6-4", + "snappedIds": [ + "pei-6-4", + ], "width": 2.5, "x": 4.75, "z": -24, @@ -23460,7 +24168,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-7-2", + "snappedIds": [ + "pei-7-2", + ], "width": 2.5, "x": 7.25, "z": -24, @@ -23473,7 +24183,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-7-4", + "snappedIds": [ + "pei-7-4", + ], "width": 2.5, "x": 9.75, "z": -24, @@ -23486,7 +24198,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-8-2", + "snappedIds": [ + "pei-8-2", + ], "width": 2.5, "x": 12.25, "z": -24, @@ -23499,7 +24213,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-8-4", + "snappedIds": [ + "pei-8-4", + ], "width": 2.5, "x": 14.75, "z": -24, @@ -23512,7 +24228,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-9-2", + "snappedIds": [ + "pei-9-2", + ], "width": 2.5, "x": 17.25, "z": -24, @@ -23525,7 +24243,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-9-4", + "snappedIds": [ + "pei-9-4", + ], "width": 2.5, "x": 19.75, "z": -24, @@ -23538,6 +24258,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -11.2, @@ -23550,6 +24271,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -11.2, @@ -23562,6 +24284,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -11.2, @@ -23574,6 +24297,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -11.2, @@ -23586,6 +24310,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -11.2, @@ -23598,6 +24323,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -11.2, @@ -23610,6 +24336,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -15.325, @@ -23622,6 +24349,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -15.325, @@ -23634,6 +24362,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -15.325, @@ -23646,6 +24375,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -15.325, @@ -23658,6 +24388,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -15.325, @@ -23670,6 +24401,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -15.325, @@ -23682,6 +24414,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -19.45, @@ -23694,6 +24427,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -19.45, @@ -23706,6 +24440,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -19.45, @@ -23718,6 +24453,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -19.45, @@ -23730,6 +24466,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -19.45, @@ -23742,10 +24479,33 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -19.45, }, + { + "angle": 3.141592653589793, + "depth": 8, + "height": 0.1, + "id": "score-0", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [ + "stick-100-0", + "stick-1000-0", + "stick-5000-0", + "stick-10000-0", + ], + "width": 40, + "x": 0, + "z": -32, + }, { "angle": 4.71238898038469, "depth": 2.5, @@ -23754,7 +24514,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-1-2", + "snappedIds": [ + "sou-1-2", + ], "width": 3.75, "x": -24, "z": 20.25, @@ -23767,7 +24529,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-1-4", + "snappedIds": [ + "sou-1-4", + ], "width": 3.75, "x": -24, "z": 17.75, @@ -23780,7 +24544,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-2-2", + "snappedIds": [ + "sou-2-2", + ], "width": 3.75, "x": -24, "z": 15.25, @@ -23793,7 +24559,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-2-4", + "snappedIds": [ + "sou-2-4", + ], "width": 3.75, "x": -24, "z": 12.75, @@ -23806,7 +24574,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-3-2", + "snappedIds": [ + "sou-3-2", + ], "width": 3.75, "x": -24, "z": 10.25, @@ -23819,7 +24589,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-3-4", + "snappedIds": [ + "sou-3-4", + ], "width": 3.75, "x": -24, "z": 7.75, @@ -23832,7 +24604,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-4-2", + "snappedIds": [ + "sou-4-2", + ], "width": 3.75, "x": -24, "z": 5.25, @@ -23845,7 +24619,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-4-4", + "snappedIds": [ + "sou-4-4", + ], "width": 3.75, "x": -24, "z": 2.75, @@ -23858,7 +24634,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-5-2", + "snappedIds": [ + "sou-5-2", + ], "width": 3.75, "x": -24, "z": 0.25, @@ -23871,7 +24649,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-5-4", + "snappedIds": [ + "sou-5-4", + ], "width": 3.75, "x": -24, "z": -2.25, @@ -23884,7 +24664,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-6-2", + "snappedIds": [ + "sou-6-2", + ], "width": 3.75, "x": -24, "z": -4.75, @@ -23897,7 +24679,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-6-4", + "snappedIds": [ + "sou-6-4", + ], "width": 3.75, "x": -24, "z": -7.25, @@ -23910,7 +24694,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-7-2", + "snappedIds": [ + "sou-7-2", + ], "width": 3.75, "x": -24, "z": -9.75, @@ -23923,7 +24709,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-7-4", + "snappedIds": [ + "sou-7-4", + ], "width": 3.75, "x": -24, "z": -12.25, @@ -23936,7 +24724,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-8-2", + "snappedIds": [ + "sou-8-2", + ], "width": 3.75, "x": -24, "z": -14.75, @@ -23949,7 +24739,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-8-4", + "snappedIds": [ + "sou-8-4", + ], "width": 3.75, "x": -24, "z": -17.25, @@ -23962,7 +24754,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-9-2", + "snappedIds": [ + "sou-9-2", + ], "width": 3.75, "x": -24, "z": -19.75, @@ -23975,6 +24769,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 8, @@ -23987,6 +24782,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 5, @@ -23999,6 +24795,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 2, @@ -24011,6 +24808,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -1, @@ -24023,6 +24821,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -4, @@ -24035,6 +24834,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -7, @@ -24047,6 +24847,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 8, @@ -24059,6 +24860,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 5, @@ -24071,6 +24873,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 2, @@ -24083,6 +24886,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -1, @@ -24095,6 +24899,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -4, @@ -24107,6 +24912,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -7, @@ -24119,6 +24925,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 8, @@ -24131,6 +24938,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 5, @@ -24143,6 +24951,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 2, @@ -24155,6 +24964,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -1, @@ -24167,6 +24977,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -4, @@ -24179,10 +24990,33 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -7, }, + { + "angle": 4.71238898038469, + "depth": 40, + "height": 0.1, + "id": "score-1", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [ + "stick-100-1", + "stick-1000-1", + "stick-5000-1", + "stick-10000-1", + ], + "width": 8, + "x": 32, + "z": 0, + }, ], }, "id": "main-board", @@ -33311,9 +34145,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 10, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-100.ktx2", "transform": { @@ -33322,7 +34154,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -27, + "z": -29, }, { "diameter": 0.3, @@ -33358,9 +34190,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 4, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-1000.ktx2", "transform": { @@ -33369,7 +34199,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -28, + "z": -30, }, { "diameter": 0.3, @@ -33405,9 +34235,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 2, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-5000.ktx2", "transform": { @@ -33416,7 +34244,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -29, + "z": -31, }, { "diameter": 0.3, @@ -33452,9 +34280,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 1, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-10000.ktx2", "transform": { @@ -33463,7 +34289,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -30, + "z": -32, }, { "diameter": 0.3, @@ -33499,16 +34325,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 10, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-100.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -27, + "x": -29, "y": 0.15, "z": 0, }, @@ -33546,16 +34370,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 4, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-1000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -28, + "x": -30, "y": 0.15, "z": 0, }, @@ -33593,16 +34415,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 2, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-5000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -29, + "x": -31, "y": 0.15, "z": 0, }, @@ -33640,16 +34460,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 1, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-10000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -30, + "x": -32, "y": 0.15, "z": 0, }, @@ -33687,9 +34505,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 10, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-100.ktx2", "transform": { @@ -33698,7 +34514,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": 27, + "z": 29, }, { "diameter": 0.3, @@ -33734,9 +34550,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 4, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-1000.ktx2", "transform": { @@ -33745,7 +34559,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": 28, + "z": 30, }, { "diameter": 0.3, @@ -33781,9 +34595,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 2, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-5000.ktx2", "transform": { @@ -33792,7 +34604,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": 29, + "z": 31, }, { "diameter": 0.3, @@ -33828,9 +34640,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 1, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-10000.ktx2", "transform": { @@ -33839,7 +34649,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": 30, + "z": 32, }, ], "messages": [], @@ -33953,7 +34763,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-9-4", + "snappedIds": [ + "sou-9-4", + ], "width": 2.5, "x": -20.25, "z": 24, @@ -33966,7 +34778,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-1-2", + "snappedIds": [ + "wind-1-2", + ], "width": 2.5, "x": -17.75, "z": 24, @@ -33979,7 +34793,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-1-4", + "snappedIds": [ + "wind-1-4", + ], "width": 2.5, "x": -15.25, "z": 24, @@ -33992,7 +34808,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-2-2", + "snappedIds": [ + "wind-2-2", + ], "width": 2.5, "x": -12.75, "z": 24, @@ -34005,7 +34823,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-2-4", + "snappedIds": [ + "wind-2-4", + ], "width": 2.5, "x": -10.25, "z": 24, @@ -34018,7 +34838,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-3-2", + "snappedIds": [ + "wind-3-2", + ], "width": 2.5, "x": -7.75, "z": 24, @@ -34031,7 +34853,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-3-4", + "snappedIds": [ + "wind-3-4", + ], "width": 2.5, "x": -5.25, "z": 24, @@ -34044,7 +34868,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-4-2", + "snappedIds": [ + "wind-4-2", + ], "width": 2.5, "x": -2.75, "z": 24, @@ -34057,7 +34883,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "wind-4-4", + "snappedIds": [ + "wind-4-4", + ], "width": 2.5, "x": -0.25, "z": 24, @@ -34070,7 +34898,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-1-2", + "snappedIds": [ + "dragon-1-2", + ], "width": 2.5, "x": 2.25, "z": 24, @@ -34083,7 +34913,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-1-4", + "snappedIds": [ + "dragon-1-4", + ], "width": 2.5, "x": 4.75, "z": 24, @@ -34096,7 +34928,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-2-2", + "snappedIds": [ + "dragon-2-2", + ], "width": 2.5, "x": 7.25, "z": 24, @@ -34109,7 +34943,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-2-4", + "snappedIds": [ + "dragon-2-4", + ], "width": 2.5, "x": 9.75, "z": 24, @@ -34122,7 +34958,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-3-2", + "snappedIds": [ + "dragon-3-2", + ], "width": 2.5, "x": 12.25, "z": 24, @@ -34135,7 +34973,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "dragon-3-4", + "snappedIds": [ + "dragon-3-4", + ], "width": 2.5, "x": 14.75, "z": 24, @@ -34148,7 +34988,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-1-2", + "snappedIds": [ + "man-1-2", + ], "width": 2.5, "x": 17.25, "z": 24, @@ -34161,7 +35003,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-1-4", + "snappedIds": [ + "man-1-4", + ], "width": 2.5, "x": 19.75, "z": 24, @@ -34174,6 +35018,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 11.2, @@ -34186,6 +35031,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 11.2, @@ -34198,6 +35044,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 11.2, @@ -34210,6 +35057,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 11.2, @@ -34222,6 +35070,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 11.2, @@ -34234,6 +35083,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 11.2, @@ -34246,6 +35096,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 15.325, @@ -34258,6 +35109,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 15.325, @@ -34270,6 +35122,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 15.325, @@ -34282,6 +35135,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 15.325, @@ -34294,6 +35148,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 15.325, @@ -34306,6 +35161,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 15.325, @@ -34318,6 +35174,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 19.45, @@ -34330,6 +35187,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 19.45, @@ -34342,6 +35200,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 19.45, @@ -34354,6 +35213,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 19.45, @@ -34366,6 +35226,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 19.45, @@ -34378,10 +35239,33 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 19.45, }, + { + "angle": 0, + "depth": 8, + "height": 0.1, + "id": "score-2", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [ + "stick-100-2", + "stick-1000-2", + "stick-5000-2", + "stick-10000-2", + ], + "width": 40, + "x": 0, + "z": 32, + }, { "angle": 1.5707963267948966, "depth": 2.5, @@ -34390,7 +35274,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-2-2", + "snappedIds": [ + "man-2-2", + ], "width": 3.75, "x": 24, "z": 20.25, @@ -34403,7 +35289,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-2-4", + "snappedIds": [ + "man-2-4", + ], "width": 3.75, "x": 24, "z": 17.75, @@ -34416,7 +35304,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-3-2", + "snappedIds": [ + "man-3-2", + ], "width": 3.75, "x": 24, "z": 15.25, @@ -34429,7 +35319,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-3-4", + "snappedIds": [ + "man-3-4", + ], "width": 3.75, "x": 24, "z": 12.75, @@ -34442,7 +35334,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-4-2", + "snappedIds": [ + "man-4-2", + ], "width": 3.75, "x": 24, "z": 10.25, @@ -34455,7 +35349,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-4-4", + "snappedIds": [ + "man-4-4", + ], "width": 3.75, "x": 24, "z": 7.75, @@ -34468,7 +35364,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-5-2", + "snappedIds": [ + "man-5-2", + ], "width": 3.75, "x": 24, "z": 5.25, @@ -34481,7 +35379,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-5-4", + "snappedIds": [ + "man-5-4", + ], "width": 3.75, "x": 24, "z": 2.75, @@ -34494,7 +35394,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-6-2", + "snappedIds": [ + "man-6-2", + ], "width": 3.75, "x": 24, "z": 0.25, @@ -34507,7 +35409,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-6-4", + "snappedIds": [ + "man-6-4", + ], "width": 3.75, "x": 24, "z": -2.25, @@ -34520,7 +35424,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-7-2", + "snappedIds": [ + "man-7-2", + ], "width": 3.75, "x": 24, "z": -4.75, @@ -34533,7 +35439,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-7-4", + "snappedIds": [ + "man-7-4", + ], "width": 3.75, "x": 24, "z": -7.25, @@ -34546,7 +35454,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-8-2", + "snappedIds": [ + "man-8-2", + ], "width": 3.75, "x": 24, "z": -9.75, @@ -34559,7 +35469,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-8-4", + "snappedIds": [ + "man-8-4", + ], "width": 3.75, "x": 24, "z": -12.25, @@ -34572,7 +35484,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-9-2", + "snappedIds": [ + "man-9-2", + ], "width": 3.75, "x": 24, "z": -14.75, @@ -34585,7 +35499,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "man-9-4", + "snappedIds": [ + "man-9-4", + ], "width": 3.75, "x": 24, "z": -17.25, @@ -34598,7 +35514,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-1-2", + "snappedIds": [ + "pei-1-2", + ], "width": 3.75, "x": 24, "z": -19.75, @@ -34611,6 +35529,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 8, @@ -34623,6 +35542,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 5, @@ -34635,6 +35555,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 2, @@ -34647,6 +35568,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -1, @@ -34659,6 +35581,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -4, @@ -34671,6 +35594,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -7, @@ -34683,6 +35607,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 8, @@ -34695,6 +35620,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 5, @@ -34707,6 +35633,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 2, @@ -34719,6 +35646,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -1, @@ -34731,6 +35659,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -4, @@ -34743,6 +35672,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -7, @@ -34755,6 +35685,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 8, @@ -34767,6 +35698,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 5, @@ -34779,6 +35711,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 2, @@ -34791,6 +35724,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -1, @@ -34803,6 +35737,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -4, @@ -34815,10 +35750,33 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -7, }, + { + "angle": 1.5707963267948966, + "depth": 40, + "height": 0.1, + "id": "score-3", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [ + "stick-100-3", + "stick-1000-3", + "stick-5000-3", + "stick-10000-3", + ], + "width": 8, + "x": -32, + "z": 0, + }, { "angle": 3.141592653589793, "depth": 3.75, @@ -34827,7 +35785,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-1-4", + "snappedIds": [ + "pei-1-4", + ], "width": 2.5, "x": -20.25, "z": -24, @@ -34840,7 +35800,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-2-2", + "snappedIds": [ + "pei-2-2", + ], "width": 2.5, "x": -17.75, "z": -24, @@ -34853,7 +35815,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-2-4", + "snappedIds": [ + "pei-2-4", + ], "width": 2.5, "x": -15.25, "z": -24, @@ -34866,7 +35830,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-3-2", + "snappedIds": [ + "pei-3-2", + ], "width": 2.5, "x": -12.75, "z": -24, @@ -34879,7 +35845,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-3-4", + "snappedIds": [ + "pei-3-4", + ], "width": 2.5, "x": -10.25, "z": -24, @@ -34892,7 +35860,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-4-2", + "snappedIds": [ + "pei-4-2", + ], "width": 2.5, "x": -7.75, "z": -24, @@ -34905,7 +35875,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-4-4", + "snappedIds": [ + "pei-4-4", + ], "width": 2.5, "x": -5.25, "z": -24, @@ -34918,7 +35890,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-5-2", + "snappedIds": [ + "pei-5-2", + ], "width": 2.5, "x": -2.75, "z": -24, @@ -34931,7 +35905,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-5-4", + "snappedIds": [ + "pei-5-4", + ], "width": 2.5, "x": -0.25, "z": -24, @@ -34944,7 +35920,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-6-2", + "snappedIds": [ + "pei-6-2", + ], "width": 2.5, "x": 2.25, "z": -24, @@ -34957,7 +35935,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-6-4", + "snappedIds": [ + "pei-6-4", + ], "width": 2.5, "x": 4.75, "z": -24, @@ -34970,7 +35950,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-7-2", + "snappedIds": [ + "pei-7-2", + ], "width": 2.5, "x": 7.25, "z": -24, @@ -34983,7 +35965,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-7-4", + "snappedIds": [ + "pei-7-4", + ], "width": 2.5, "x": 9.75, "z": -24, @@ -34996,7 +35980,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-8-2", + "snappedIds": [ + "pei-8-2", + ], "width": 2.5, "x": 12.25, "z": -24, @@ -35009,7 +35995,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-8-4", + "snappedIds": [ + "pei-8-4", + ], "width": 2.5, "x": 14.75, "z": -24, @@ -35022,7 +36010,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-9-2", + "snappedIds": [ + "pei-9-2", + ], "width": 2.5, "x": 17.25, "z": -24, @@ -35035,7 +36025,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "pei-9-4", + "snappedIds": [ + "pei-9-4", + ], "width": 2.5, "x": 19.75, "z": -24, @@ -35048,6 +36040,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -11.2, @@ -35060,6 +36053,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -11.2, @@ -35072,6 +36066,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -11.2, @@ -35084,6 +36079,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -11.2, @@ -35096,6 +36092,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -11.2, @@ -35108,6 +36105,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -11.2, @@ -35120,6 +36118,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -15.325, @@ -35132,6 +36131,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -15.325, @@ -35144,6 +36144,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -15.325, @@ -35156,6 +36157,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -15.325, @@ -35168,6 +36170,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -15.325, @@ -35180,6 +36183,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -15.325, @@ -35192,6 +36196,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -19.45, @@ -35204,6 +36209,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -19.45, @@ -35216,6 +36222,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -19.45, @@ -35228,6 +36235,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -19.45, @@ -35240,6 +36248,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -19.45, @@ -35252,10 +36261,33 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -19.45, }, + { + "angle": 3.141592653589793, + "depth": 8, + "height": 0.1, + "id": "score-0", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [ + "stick-100-0", + "stick-1000-0", + "stick-5000-0", + "stick-10000-0", + ], + "width": 40, + "x": 0, + "z": -32, + }, { "angle": 4.71238898038469, "depth": 2.5, @@ -35264,7 +36296,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-1-2", + "snappedIds": [ + "sou-1-2", + ], "width": 3.75, "x": -24, "z": 20.25, @@ -35277,7 +36311,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-1-4", + "snappedIds": [ + "sou-1-4", + ], "width": 3.75, "x": -24, "z": 17.75, @@ -35290,7 +36326,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-2-2", + "snappedIds": [ + "sou-2-2", + ], "width": 3.75, "x": -24, "z": 15.25, @@ -35303,7 +36341,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-2-4", + "snappedIds": [ + "sou-2-4", + ], "width": 3.75, "x": -24, "z": 12.75, @@ -35316,7 +36356,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-3-2", + "snappedIds": [ + "sou-3-2", + ], "width": 3.75, "x": -24, "z": 10.25, @@ -35329,7 +36371,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-3-4", + "snappedIds": [ + "sou-3-4", + ], "width": 3.75, "x": -24, "z": 7.75, @@ -35342,7 +36386,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-4-2", + "snappedIds": [ + "sou-4-2", + ], "width": 3.75, "x": -24, "z": 5.25, @@ -35355,7 +36401,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-4-4", + "snappedIds": [ + "sou-4-4", + ], "width": 3.75, "x": -24, "z": 2.75, @@ -35368,7 +36416,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-5-2", + "snappedIds": [ + "sou-5-2", + ], "width": 3.75, "x": -24, "z": 0.25, @@ -35381,7 +36431,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-5-4", + "snappedIds": [ + "sou-5-4", + ], "width": 3.75, "x": -24, "z": -2.25, @@ -35394,7 +36446,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-6-2", + "snappedIds": [ + "sou-6-2", + ], "width": 3.75, "x": -24, "z": -4.75, @@ -35407,7 +36461,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-6-4", + "snappedIds": [ + "sou-6-4", + ], "width": 3.75, "x": -24, "z": -7.25, @@ -35420,7 +36476,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-7-2", + "snappedIds": [ + "sou-7-2", + ], "width": 3.75, "x": -24, "z": -9.75, @@ -35433,7 +36491,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-7-4", + "snappedIds": [ + "sou-7-4", + ], "width": 3.75, "x": -24, "z": -12.25, @@ -35446,7 +36506,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-8-2", + "snappedIds": [ + "sou-8-2", + ], "width": 3.75, "x": -24, "z": -14.75, @@ -35459,7 +36521,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-8-4", + "snappedIds": [ + "sou-8-4", + ], "width": 3.75, "x": -24, "z": -17.25, @@ -35472,7 +36536,9 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], - "snappedId": "sou-9-2", + "snappedIds": [ + "sou-9-2", + ], "width": 3.75, "x": -24, "z": -19.75, @@ -35485,6 +36551,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 8, @@ -35497,6 +36564,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 5, @@ -35509,6 +36577,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 2, @@ -35521,6 +36590,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -1, @@ -35533,6 +36603,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -4, @@ -35545,6 +36616,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -7, @@ -35557,6 +36629,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 8, @@ -35569,6 +36642,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 5, @@ -35581,6 +36655,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 2, @@ -35593,6 +36668,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -1, @@ -35605,6 +36681,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -4, @@ -35617,6 +36694,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -7, @@ -35629,6 +36707,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 8, @@ -35641,6 +36720,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 5, @@ -35653,6 +36733,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 2, @@ -35665,6 +36746,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -1, @@ -35677,6 +36759,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -4, @@ -35689,10 +36772,33 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -7, }, + { + "angle": 4.71238898038469, + "depth": 40, + "height": 0.1, + "id": "score-1", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [ + "stick-100-1", + "stick-1000-1", + "stick-5000-1", + "stick-10000-1", + ], + "width": 8, + "x": 32, + "z": 0, + }, ], }, "id": "main-board", @@ -44821,9 +45927,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 10, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-100.ktx2", "transform": { @@ -44832,7 +45936,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -27, + "z": -29, }, { "diameter": 0.3, @@ -44868,9 +45972,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 4, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-1000.ktx2", "transform": { @@ -44879,7 +45981,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -28, + "z": -30, }, { "diameter": 0.3, @@ -44915,9 +46017,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 2, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-5000.ktx2", "transform": { @@ -44926,7 +46026,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -29, + "z": -31, }, { "diameter": 0.3, @@ -44962,9 +46062,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 1, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-10000.ktx2", "transform": { @@ -44973,7 +46071,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": -30, + "z": -32, }, { "diameter": 0.3, @@ -45009,16 +46107,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 10, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-100.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -27, + "x": -29, "y": 0.15, "z": 0, }, @@ -45056,16 +46152,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 4, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-1000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -28, + "x": -30, "y": 0.15, "z": 0, }, @@ -45103,16 +46197,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 2, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-5000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -29, + "x": -31, "y": 0.15, "z": 0, }, @@ -45150,16 +46242,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 1, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-10000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": -30, + "x": -32, "y": 0.15, "z": 0, }, @@ -45197,9 +46287,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 10, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-100.ktx2", "transform": { @@ -45208,7 +46296,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": 27, + "z": 29, }, { "diameter": 0.3, @@ -45244,9 +46332,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 4, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-1000.ktx2", "transform": { @@ -45255,7 +46341,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": 28, + "z": 30, }, { "diameter": 0.3, @@ -45291,9 +46377,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 2, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-5000.ktx2", "transform": { @@ -45302,7 +46386,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": 29, + "z": 31, }, { "diameter": 0.3, @@ -45338,9 +46422,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 1, }, - "rotable": { - "angle": 0, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-10000.ktx2", "transform": { @@ -45349,7 +46431,7 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e }, "x": 0, "y": 0.15, - "z": 30, + "z": 32, }, { "diameter": 0.3, @@ -45385,16 +46467,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 10, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-100.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": 27, + "x": 29, "y": 0.15, "z": 0, }, @@ -45432,16 +46512,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 4, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-1000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": 28, + "x": 30, "y": 0.15, "z": 0, }, @@ -45479,16 +46557,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 2, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-5000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": 29, + "x": 31, "y": 0.15, "z": 0, }, @@ -45526,16 +46602,14 @@ exports[`mah-jong game descriptor > askForParameters() + addPlayer() > enrolls e ], "quantity": 1, }, - "rotable": { - "angle": 1.5707963267948966, - }, + "rotable": {}, "shape": "roundToken", "texture": "stick-10000.ktx2", "transform": { "roll": 1.5707963267948966, "scaleZ": 2, }, - "x": 30, + "x": 32, "y": 0.15, "z": 0, }, @@ -45728,6 +46802,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -20.25, "z": 24, @@ -45740,6 +46815,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -17.75, "z": 24, @@ -45752,6 +46828,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -15.25, "z": 24, @@ -45764,6 +46841,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -12.75, "z": 24, @@ -45776,6 +46854,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -10.25, "z": 24, @@ -45788,6 +46867,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -7.75, "z": 24, @@ -45800,6 +46880,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5.25, "z": 24, @@ -45812,6 +46893,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2.75, "z": 24, @@ -45824,6 +46906,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -0.25, "z": 24, @@ -45836,6 +46919,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 2.25, "z": 24, @@ -45848,6 +46932,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4.75, "z": 24, @@ -45860,6 +46945,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7.25, "z": 24, @@ -45872,6 +46958,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 9.75, "z": 24, @@ -45884,6 +46971,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 12.25, "z": 24, @@ -45896,6 +46984,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 14.75, "z": 24, @@ -45908,6 +46997,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 17.25, "z": 24, @@ -45920,6 +47010,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 19.75, "z": 24, @@ -45932,6 +47023,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 11.2, @@ -45944,6 +47036,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 11.2, @@ -45956,6 +47049,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 11.2, @@ -45968,6 +47062,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 11.2, @@ -45980,6 +47075,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 11.2, @@ -45992,6 +47088,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 11.2, @@ -46004,6 +47101,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 15.325, @@ -46016,6 +47114,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 15.325, @@ -46028,6 +47127,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 15.325, @@ -46040,6 +47140,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 15.325, @@ -46052,6 +47153,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 15.325, @@ -46064,6 +47166,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 15.325, @@ -46076,6 +47179,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": 19.45, @@ -46088,6 +47192,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": 19.45, @@ -46100,6 +47205,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": 19.45, @@ -46112,6 +47218,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": 19.45, @@ -46124,6 +47231,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": 19.45, @@ -46136,10 +47244,28 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": 19.45, }, + { + "angle": 0, + "depth": 8, + "height": 0.1, + "id": "score-2", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [], + "width": 40, + "x": 0, + "z": 32, + }, { "angle": 1.5707963267948966, "depth": 2.5, @@ -46148,6 +47274,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": 20.25, @@ -46160,6 +47287,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": 17.75, @@ -46172,6 +47300,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": 15.25, @@ -46184,6 +47313,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": 12.75, @@ -46196,6 +47326,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": 10.25, @@ -46208,6 +47339,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": 7.75, @@ -46220,6 +47352,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": 5.25, @@ -46232,6 +47365,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": 2.75, @@ -46244,6 +47378,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": 0.25, @@ -46256,6 +47391,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": -2.25, @@ -46268,6 +47404,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": -4.75, @@ -46280,6 +47417,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": -7.25, @@ -46292,6 +47430,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": -9.75, @@ -46304,6 +47443,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": -12.25, @@ -46316,6 +47456,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": -14.75, @@ -46328,6 +47469,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": -17.25, @@ -46340,6 +47482,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 24, "z": -19.75, @@ -46352,6 +47495,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 8, @@ -46364,6 +47508,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 5, @@ -46376,6 +47521,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": 2, @@ -46388,6 +47534,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -1, @@ -46400,6 +47547,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -4, @@ -46412,6 +47560,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 11.2, "z": -7, @@ -46424,6 +47573,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 8, @@ -46436,6 +47586,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 5, @@ -46448,6 +47599,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": 2, @@ -46460,6 +47612,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -1, @@ -46472,6 +47625,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -4, @@ -46484,6 +47638,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 15.325, "z": -7, @@ -46496,6 +47651,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 8, @@ -46508,6 +47664,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 5, @@ -46520,6 +47677,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": 2, @@ -46532,6 +47690,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -1, @@ -46544,6 +47703,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -4, @@ -46556,10 +47716,28 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": 19.45, "z": -7, }, + { + "angle": 1.5707963267948966, + "depth": 40, + "height": 0.1, + "id": "score-3", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [], + "width": 8, + "x": -32, + "z": 0, + }, { "angle": 3.141592653589793, "depth": 3.75, @@ -46568,6 +47746,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -20.25, "z": -24, @@ -46580,6 +47759,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -17.75, "z": -24, @@ -46592,6 +47772,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -15.25, "z": -24, @@ -46604,6 +47785,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -12.75, "z": -24, @@ -46616,6 +47798,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -10.25, "z": -24, @@ -46628,6 +47811,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -7.75, "z": -24, @@ -46640,6 +47824,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5.25, "z": -24, @@ -46652,6 +47837,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2.75, "z": -24, @@ -46664,6 +47850,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -0.25, "z": -24, @@ -46676,6 +47863,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 2.25, "z": -24, @@ -46688,6 +47876,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4.75, "z": -24, @@ -46700,6 +47889,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7.25, "z": -24, @@ -46712,6 +47902,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 9.75, "z": -24, @@ -46724,6 +47915,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 12.25, "z": -24, @@ -46736,6 +47928,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 14.75, "z": -24, @@ -46748,6 +47941,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 17.25, "z": -24, @@ -46760,6 +47954,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 19.75, "z": -24, @@ -46772,6 +47967,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -11.2, @@ -46784,6 +47980,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -11.2, @@ -46796,6 +47993,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -11.2, @@ -46808,6 +48006,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -11.2, @@ -46820,6 +48019,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -11.2, @@ -46832,6 +48032,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -11.2, @@ -46844,6 +48045,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -15.325, @@ -46856,6 +48058,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -15.325, @@ -46868,6 +48071,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -15.325, @@ -46880,6 +48084,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -15.325, @@ -46892,6 +48097,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -15.325, @@ -46904,6 +48110,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -15.325, @@ -46916,6 +48123,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -8, "z": -19.45, @@ -46928,6 +48136,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -5, "z": -19.45, @@ -46940,6 +48149,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": -2, "z": -19.45, @@ -46952,6 +48162,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 1, "z": -19.45, @@ -46964,6 +48175,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 4, "z": -19.45, @@ -46976,10 +48188,28 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 2.5, "x": 7, "z": -19.45, }, + { + "angle": 3.141592653589793, + "depth": 8, + "height": 0.1, + "id": "score-0", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [], + "width": 40, + "x": 0, + "z": -32, + }, { "angle": 4.71238898038469, "depth": 2.5, @@ -46988,6 +48218,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": 20.25, @@ -47000,6 +48231,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": 17.75, @@ -47012,6 +48244,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": 15.25, @@ -47024,6 +48257,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": 12.75, @@ -47036,6 +48270,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": 10.25, @@ -47048,6 +48283,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": 7.75, @@ -47060,6 +48296,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": 5.25, @@ -47072,6 +48309,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": 2.75, @@ -47084,6 +48322,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": 0.25, @@ -47096,6 +48335,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": -2.25, @@ -47108,6 +48348,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": -4.75, @@ -47120,6 +48361,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": -7.25, @@ -47132,6 +48374,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": -9.75, @@ -47144,6 +48387,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": -12.25, @@ -47156,6 +48400,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": -14.75, @@ -47168,6 +48413,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": -17.25, @@ -47180,6 +48426,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -24, "z": -19.75, @@ -47192,6 +48439,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 8, @@ -47204,6 +48452,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 5, @@ -47216,6 +48465,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": 2, @@ -47228,6 +48478,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -1, @@ -47240,6 +48491,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -4, @@ -47252,6 +48504,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -11.2, "z": -7, @@ -47264,6 +48517,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 8, @@ -47276,6 +48530,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 5, @@ -47288,6 +48543,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": 2, @@ -47300,6 +48556,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -1, @@ -47312,6 +48569,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -4, @@ -47324,6 +48582,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -15.325, "z": -7, @@ -47336,6 +48595,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 8, @@ -47348,6 +48608,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 5, @@ -47360,6 +48621,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": 2, @@ -47372,6 +48634,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -1, @@ -47384,6 +48647,7 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -4, @@ -47396,10 +48660,28 @@ exports[`mah-jong game descriptor > build() > builds game setup 1`] = ` "kinds": [ "tile", ], + "snappedIds": [], "width": 3.75, "x": -19.45, "z": -7, }, + { + "angle": 4.71238898038469, + "depth": 40, + "height": 0.1, + "id": "score-1", + "kinds": [ + "sticks-100", + "sticks-1000", + "sticks-5000", + "sticks-10000", + ], + "max": 68, + "snappedIds": [], + "width": 8, + "x": 32, + "z": 0, + }, ], }, "id": "main-board", diff --git a/apps/games/mah-jong/index.js b/apps/games/mah-jong/index.js index e51f7e58..73759f1b 100644 --- a/apps/games/mah-jong/index.js +++ b/apps/games/mah-jong/index.js @@ -2,6 +2,7 @@ export { build } from './logic/build.js' export { colors } from './logic/constants.js' export { addPlayer, askForParameters } from './logic/player.js' +export { computeScore } from './logic/score.js' /** @type {import('@tabulous/types').GameDescriptor['locales']} */ export const locales = { diff --git a/apps/games/mah-jong/index.test.js b/apps/games/mah-jong/index.test.js index af1966b5..d07aebe8 100644 --- a/apps/games/mah-jong/index.test.js +++ b/apps/games/mah-jong/index.test.js @@ -1,6 +1,162 @@ // @ts-check -import { buildDescriptorTestSuite } from '@tabulous/game-utils/tests/game.js' +import { findMesh, snapTo, unsnap } from '@tabulous/game-utils' +import { + buildDescriptorTestSuite, + buildParameters, + enroll, + toEngineState +} from '@tabulous/game-utils/tests/game.js' +import { beforeEach, describe, expect, it } from 'vitest' import * as descriptor from '.' +import { ids } from './logic/constants.js' -buildDescriptorTestSuite('mah-jong', descriptor) +buildDescriptorTestSuite('mah-jong', descriptor, utils => { + describe('computeScore', () => { + const player = utils.makePlayer(0) + const player2 = utils.makePlayer(1) + /** @type {import('@tabulous/types').StartedGame} */ + let game + + beforeEach(async () => { + game = await utils.buildGame({ + ...descriptor, + name: utils.name + }) + game = await enroll( + descriptor, + game, + player, + buildParameters(await descriptor.askForParameters({ game, player })) + ) + game = await enroll( + descriptor, + game, + player2, + buildParameters( + await descriptor.askForParameters({ game, player: player2 }) + ) + ) + }) + + it('computes initial score', async () => { + expect( + descriptor.computeScore( + null, + toEngineState(game), + [player, player2], + [] + ) + ).toEqual({ + [player.id]: { total: '25k' }, + [player2.id]: { total: '25k' } + }) + }) + + it('computes on snap', async () => { + const stick = unsnap(`${ids.score}0`, game.meshes) + const anchorId = `${ids.score}1` + snapTo(anchorId, stick, game.meshes) + expect( + descriptor.computeScore( + { + fn: 'snap', + args: [stick.id, anchorId, true], + fromHand: false, + meshId: stick.id + }, + toEngineState(game), + [player, player2], + [] + ) + ).toEqual({ + [player.id]: { total: '24k' }, + [player2.id]: { total: '26k' } + }) + }) + + it('computes on unsnap', async () => { + const anchorId = `${ids.score}0` + const stick = unsnap(anchorId, game.meshes) + expect( + descriptor.computeScore( + { + fn: 'unsnap', + args: [stick.id, anchorId, true], + fromHand: false, + meshId: stick.id + }, + toEngineState(game), + [player], + [] + ) + ).toEqual({ [player.id]: { total: '24k' } }) + }) + + it('computes on increment', async () => { + const stick1 = unsnap(`${ids.score}0`, game.meshes) + const stick2 = findMesh(`stick-100-1`, game.meshes) + // @ts-expect-error -- can not use ! operator in JS. + stick2.quantifiable.quantity += stick1.quantifiable?.quantity ?? 1 + expect( + descriptor.computeScore( + { + fn: 'increment', + args: [[], true], + fromHand: false, + meshId: stick2.id + }, + toEngineState(game), + [player, player2], + [] + ) + ).toEqual({ + [player.id]: { total: '24k' }, + [player2.id]: { total: '26k' } + }) + }) + + it('computes on decrement', async () => { + const stick = findMesh(`stick-1000-0`, game.meshes) + // @ts-expect-error -- can not use ! operator in JS. + stick.quantifiable.quantity -= 2 + expect( + descriptor.computeScore( + { + fn: 'decrement', + args: [2, true, `stick-1000-0-whatever`], + fromHand: false, + meshId: stick.id + }, + toEngineState(game), + [player], + [] + ) + ).toEqual({ + [player.id]: { total: '23k' } + }) + }) + + it.each([ + { fn: /** @type {const} */ ('snap') }, + { fn: /** @type {const} */ ('unsnap') } + ])('ignores $fn outside the score anchor', async ({ fn }) => { + const tile = findMesh(`man-1-1`, game.meshes) + const anchorId = `river-east-1-1` + snapTo(anchorId, tile, game.meshes) + expect( + descriptor.computeScore( + { + fn, + args: [tile.id, anchorId, true], + fromHand: false, + meshId: tile.id + }, + toEngineState(game), + [player], + [] + ) + ).toBeUndefined() + }) + }) +}) diff --git a/apps/games/mah-jong/logic/builders/boards.js b/apps/games/mah-jong/logic/builders/boards.js index feaa07fa..0076fb92 100644 --- a/apps/games/mah-jong/logic/builders/boards.js +++ b/apps/games/mah-jong/logic/builders/boards.js @@ -1,5 +1,14 @@ // @ts-check -import { kinds, riverSize, shapes, walls, wallSize } from '../constants.js' +import { + ids, + kinds, + positions, + riverSize, + shapes, + stickQuantities, + walls, + wallSize +} from '../constants.js' /** @returns {import('@tabulous/types').Mesh} */ export function buildMainBoard() { @@ -11,19 +20,50 @@ export function buildMainBoard() { y: 0.01, anchorable: { anchors: [ - ...buildWallAnchors({ wall: north, isHorizontal: true, angle: 0 }), - ...buildRiverAnchors({ wall: north, isHorizontal: true, angle: 0 }), - ...buildWallAnchors({ wall: east, isHorizontal: false, angle: 0.5 }), - ...buildRiverAnchors({ wall: east, isHorizontal: false, angle: 0.5 }), - ...buildWallAnchors({ wall: south, isHorizontal: true, angle: 1 }), - ...buildRiverAnchors({ wall: south, isHorizontal: true, angle: 1 }), - ...buildWallAnchors({ wall: west, isHorizontal: false, angle: 1.5 }), - ...buildRiverAnchors({ wall: west, isHorizontal: false, angle: 1.5 }) + ...buildPlayerAnchors({ + wall: north, + isHorizontal: true, + angle: 0, + rank: 2 + }), + ...buildPlayerAnchors({ + wall: east, + isHorizontal: false, + angle: 0.5, + rank: 3 + }), + ...buildPlayerAnchors({ + wall: south, + isHorizontal: true, + angle: 1, + rank: 0 + }), + ...buildPlayerAnchors({ + wall: west, + isHorizontal: false, + angle: 1.5, + rank: 1 + }) ] } } } +function buildPlayerAnchors( + /** @type {{ wall: import('../constants').Wall, isHorizontal: boolean, angle: number, rank: number }} */ { + wall, + isHorizontal, + angle, + rank + } +) { + return [ + ...buildWallAnchors({ wall, isHorizontal, angle }), + ...buildRiverAnchors({ wall, isHorizontal, angle }), + buildScoreAnchor({ rank, isHorizontal, angle }) + ] +} + function buildWallAnchors( /** @type {{ wall: import('../constants').Wall, isHorizontal: boolean, angle: number }} */ { wall, @@ -52,7 +92,8 @@ function buildWallAnchors( height, width: isHorizontal ? width : depth, depth: isHorizontal ? depth : width, - angle: Math.PI * angle + angle: Math.PI * angle, + snappedIds: [] }) } return anchors @@ -92,9 +133,44 @@ function buildRiverAnchors( height, width: isHorizontal ? width : depth, depth: isHorizontal ? depth : width, - angle: Math.PI * angle + angle: Math.PI * angle, + snappedIds: [] }) } } return anchors } + +function buildScoreAnchor( + /** @type {{ rank: number, isHorizontal: boolean, angle: number }} */ { + rank, + isHorizontal, + angle + } +) { + const { height, width, depth } = shapes.score + const invertX = angle === 0.5 ? -1 : 1 + const invertZ = angle === 1 ? -1 : 1 + const { start, offset } = positions.score + /** @type {import('@tabulous/types').Anchor} */ + return { + id: `${ids.score}${rank}`, + kinds: [ + kinds.sticks100, + kinds.sticks1000, + kinds.sticks5000, + kinds.sticks10000 + ], + x: start * invertX + (isHorizontal ? 0 : offset * invertX), + z: start * invertZ + (isHorizontal ? offset * invertZ : 0), + height, + width: isHorizontal ? width : depth, + depth: isHorizontal ? depth : width, + angle: Math.PI * angle, + snappedIds: [], + max: + Object.values(stickQuantities).reduce( + (total, quantity) => total + quantity + ) * 4 + } +} diff --git a/apps/games/mah-jong/logic/builders/sticks.js b/apps/games/mah-jong/logic/builders/sticks.js index 1f0727cc..348279a1 100644 --- a/apps/games/mah-jong/logic/builders/sticks.js +++ b/apps/games/mah-jong/logic/builders/sticks.js @@ -1,30 +1,54 @@ // @ts-check -import { kinds } from '../constants.js' +import { findAnchor } from '@tabulous/game-utils' -export function buildSticks(/** @type {number} */ playerRank) { +import { ids, kinds, positions, shapes, stickQuantities } from '../constants.js' + +export function buildSticks( + /** @type {number} */ playerRank, + /** @type {import('@tabulous/types').Mesh[]} */ meshes +) { /** @type {import('@tabulous/types').Mesh[]} */ const sticks = [] + const anchor = findAnchor(`${ids.score}${playerRank}`, meshes) + const start = 3 const { x, z, angle, offset } = playerRank === 0 - ? { x: 0, z: -27, angle: 0, offset: -1 } + ? { + x: positions.score.start, + z: -positions.score.offset + start, + angle: 0, + offset: -1 + } : playerRank === 1 - ? { x: -27, z: 0, angle: Math.PI * 0.5, offset: -1 } + ? { + x: -positions.score.offset + start, + z: positions.score.start, + angle: Math.PI * 0.5, + offset: -1 + } : playerRank === 2 - ? { x: 0, z: 27, angle: 0, offset: 1 } - : { x: 27, z: 0, angle: Math.PI * 0.5, offset: 1 } - for (const [rank, { name, quantity }] of [ - { name: 100, quantity: 10 }, - { name: 1000, quantity: 4 }, - { name: 5000, quantity: 2 }, - { name: 10000, quantity: 1 } - ].entries()) { + ? { + x: positions.score.start, + z: positions.score.offset - start, + angle: 0, + offset: 1 + } + : { + x: positions.score.offset - start, + z: positions.score.start, + angle: Math.PI * 0.5, + offset: 1 + } + for (const [rank, [name, quantity]] of Object.entries( + stickQuantities + ).entries()) { const kind = kinds[/** @type {'sticks100'} */ (`sticks${name}`)] + const id = `stick-${name}-${playerRank}` sticks.push({ - id: `stick-${name}-${playerRank}`, + id, shape: 'roundToken', texture: `stick-${name}.ktx2`, - diameter: 0.3, - height: 7, + ...shapes.stick, x: x + (angle ? rank * offset : 0), y: 0.15, z: z + (angle ? 0 : rank * offset), @@ -35,10 +59,11 @@ export function buildSticks(/** @type {number} */ playerRank) { ], transform: { roll: Math.PI * 0.5, scaleZ: 2 }, movable: { kind }, - quantifiable: { quantity, kinds: [kind] }, + quantifiable: { quantity: +quantity, kinds: [kind] }, flippable: {}, - rotable: { angle } + rotable: {} }) + anchor.snappedIds.push(id) } return sticks } diff --git a/apps/games/mah-jong/logic/constants.js b/apps/games/mah-jong/logic/constants.js index 9d382593..8bdc74dc 100644 --- a/apps/games/mah-jong/logic/constants.js +++ b/apps/games/mah-jong/logic/constants.js @@ -22,19 +22,37 @@ export const wallSize = 17 export const riverSize = 6 +export const ids = { + stick: 'sticks-', + score: 'score-' +} + export const kinds = { tile: 'tile', - sticks100: 'sticks-100', - sticks1000: 'sticks-1000', - sticks5000: 'sticks-5000', - sticks10000: 'sticks-10000' + sticks100: `${ids.stick}100`, + sticks1000: `${ids.stick}1000`, + sticks5000: `${ids.stick}5000`, + sticks10000: `${ids.stick}10000` +} + +export const stickQuantities = { + 100: 10, + 1000: 4, + 5000: 2, + 10000: 1 } export const shapes = { // Size 6: https://en.wikipedia.org/wiki/Mahjong_tiles#Construction tile: { width: 2.4, height: 1.9, depth: 3.6, borderRadius: 0.55 }, anchor: { width: 2.5, height: 0.1, depth: 3.75 }, - dealerMark: { width: 5, depth: 2.5, height: 0.3 } + dealerMark: { width: 5, height: 0.3, depth: 2.5 }, + stick: { diameter: 0.3, height: 7 }, + score: { width: 40, height: 0.1, depth: 8 } +} + +export const positions = { + score: { start: 0, offset: 32 } } export const faceUVs = { diff --git a/apps/games/mah-jong/logic/player.js b/apps/games/mah-jong/logic/player.js index 35a23bd6..28303c2e 100644 --- a/apps/games/mah-jong/logic/player.js +++ b/apps/games/mah-jong/logic/player.js @@ -40,7 +40,7 @@ export function addPlayer(game, player) { const angle = Math.PI * 0.5 * rank Object.assign(game.preferences[rank], { angle }) - game.meshes.push(...buildSticks(rank)) + game.meshes.push(...buildSticks(rank, game.meshes)) game.cameras.push( buildCameraPosition({ diff --git a/apps/games/mah-jong/logic/score.js b/apps/games/mah-jong/logic/score.js new file mode 100644 index 00000000..6ebed9e7 --- /dev/null +++ b/apps/games/mah-jong/logic/score.js @@ -0,0 +1,39 @@ +// @ts-check +import { findAnchor, findMesh } from '@tabulous/game-utils' + +import { ids } from './constants.js' + +/** @type {import('@tabulous/types').ComputeScore} */ +export function computeScore(action, state, players) { + if ( + action === null || + action.fn === 'increment' || + action.fn === 'decrement' || + ((action.fn === 'snap' || action.fn === 'unsnap') && + action.args[1]?.startsWith(ids.score)) + ) { + return compute(state, players) + } +} + +function compute( + /** @type {import('@tabulous/types').EngineState} */ state, + /** @type {Pick[]} */ players +) { + /** @type {import('@tabulous/types').Scores} */ + const scores = {} + for (const [rank, { id }] of players.entries()) { + const anchor = findAnchor(`${ids.score}${rank}`, state.meshes) + const total = anchor.snappedIds.reduce(sumPoints(state.meshes), 0) ?? 0 + scores[id] = { total: `${total / 1000}k` } + } + return scores +} + +function sumPoints(/** @type {import('@tabulous/types').Mesh[]} */ meshes) { + return (/** @type {number} */ total, /** @type {string} */ snappedId) => { + const mesh = findMesh(snappedId, meshes) + const points = Number(mesh.movable?.kind?.slice(ids.stick.length)) + return total + points * (mesh.quantifiable?.quantity ?? 1) + } +} diff --git a/apps/games/playground/logic/build.js b/apps/games/playground/logic/build.js index 6129580f..9dfd3540 100644 --- a/apps/games/playground/logic/build.js +++ b/apps/games/playground/logic/build.js @@ -26,8 +26,18 @@ export function buildCards(full = false) { }, anchorable: { anchors: [ - { id: `${id}-bottom`, z: spacing.cardAnchor.z, ...sizes.card }, - { id: `${id}-top`, z: -spacing.cardAnchor.z, ...sizes.card } + { + id: `${id}-bottom`, + z: spacing.cardAnchor.z, + ...sizes.card, + snappedIds: [] + }, + { + id: `${id}-top`, + z: -spacing.cardAnchor.z, + ...sizes.card, + snappedIds: [] + } ] }, flippable: { isFlipped: true }, diff --git a/apps/server/migrations/006-multiple-anchors.js b/apps/server/migrations/006-multiple-anchors.js new file mode 100644 index 00000000..bb6c2701 --- /dev/null +++ b/apps/server/migrations/006-multiple-anchors.js @@ -0,0 +1,27 @@ +// @ts-check +import { iteratePage } from './utils.js' + +/** @type {import('.').Apply} */ +export async function apply({ games }) { + await iteratePage(games, async game => { + const { id, meshes, hands } = game + migrateMeshes(meshes) + for (const hand of hands) { + migrateMeshes(hand.meshes) + } + await games.save({ id, meshes, hands }) + }) +} + +function migrateMeshes(/** @type {import('@tabulous/types').Mesh[]} */ meshes) { + for (const mesh of meshes) { + for (const anchor of mesh.anchorable?.anchors ?? []) { + if (!Array.isArray(anchor.snappedIds)) { + // @ts-expect-error -- snappedIds used to be string|null and is now replaced by snappedIds + anchor.snappedIds = anchor.snappedId ? [anchor.snappedId] : [] + // @ts-expect-error -- snappedId is the legacy field + delete anchor.snappedId + } + } + } +} diff --git a/apps/server/nodemon.json b/apps/server/nodemon.json index 0253003f..69539294 100644 --- a/apps/server/nodemon.json +++ b/apps/server/nodemon.json @@ -1,4 +1,10 @@ { - "ignore": ["data/*", "catalog/en", "images/en", "textures/en"], + "ignore": [ + "data/*", + "catalog/en", + "images/en", + "textures/en", + "engine.min.js" + ], "watch": ["src", "../games"] } diff --git a/apps/server/package.json b/apps/server/package.json index 48be4f6e..a30b64f9 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -22,6 +22,7 @@ "ajv": "^8.12.0", "deepmerge": "^4.3.1", "dotenv": "^16.3.1", + "esbuild": "^0.19.4", "fast-jwt": "^3.2.0", "fastify": "^4.23.2", "fastify-plugin": "^4.5.1", diff --git a/apps/server/src/graphql/games-resolver.js b/apps/server/src/graphql/games-resolver.js index 30d4894c..800b8c72 100644 --- a/apps/server/src/graphql/games-resolver.js +++ b/apps/server/src/graphql/games-resolver.js @@ -255,12 +255,25 @@ export default { } }, + Game: { + /** + * Serializer for preferences. + * @param {import('@tabulous/types').GameData} obj - serialized game. + */ + preferencesString: obj => JSON.stringify(obj.preferences) + }, + GameParameters: { /** * Serializer for schema. - * @param {import('@tabulous/types').GameParameters} obj - serialized game parameter schema + * @param {import('@tabulous/types').GameParameters} obj - serialized game parameters. + */ + schemaString: obj => JSON.stringify(obj.schema), + /** + * Serializer for preferences. + * @param {import('@tabulous/types').GameParameters} obj - serialized game parameters. */ - schemaString: obj => JSON.stringify(obj.schema) + preferencesString: obj => JSON.stringify(obj.preferences) }, HistoryRecord: { diff --git a/apps/server/src/graphql/games.graphql b/apps/server/src/graphql/games.graphql index 3116fa53..1f31a311 100644 --- a/apps/server/src/graphql/games.graphql +++ b/apps/server/src/graphql/games.graphql @@ -19,11 +19,12 @@ type Game { game properties """ kind: String + engineScript: String locales: ItemLocales meshes: [Mesh] cameras: [CameraPosition] hands: [Hand] - preferences: [PlayerPreference] + preferencesString: String rulesBookPageCount: Int availableSeats: Int zoomSpec: ZoomSpec @@ -101,7 +102,7 @@ type RandomizableState { } type Anchor { - id: ID + id: ID! x: Float y: Float z: Float @@ -109,10 +110,12 @@ type Anchor { height: Float depth: Float diameter: Float + enabled: Boolean extent: Float priority: Float kinds: [String] - snappedId: ID + max: Float + snappedIds: [ID]! playerId: ID ignoreParts: Boolean angle: Float @@ -212,12 +215,6 @@ type Hand { meshes: [Mesh]! } -type PlayerPreference { - playerId: ID! - color: String - angle: Float -} - type ZoomSpec { min: Float max: Float @@ -367,7 +364,7 @@ input RandomizableStateInput { } input AnchorInput { - id: ID + id: ID! x: Float y: Float z: Float @@ -375,10 +372,12 @@ input AnchorInput { height: Float depth: Float diameter: Float + enabled: Boolean extent: Float priority: Float kinds: [String] - snappedId: ID + max: Float + snappedIds: [ID]! playerId: ID ignoreParts: Boolean angle: Float @@ -488,7 +487,7 @@ type GameParameters { kind: String locales: ItemLocales players: [GamePlayer]! - preferences: [PlayerPreference] + preferencesString: String rulesBookPageCount: Int availableSeats: Int colors: ColorSpec diff --git a/apps/server/src/graphql/index.d.ts b/apps/server/src/graphql/index.d.ts index 018a1a27..f7b1307b 100644 --- a/apps/server/src/graphql/index.d.ts +++ b/apps/server/src/graphql/index.d.ts @@ -45,6 +45,7 @@ export type Game = Pick< | 'id' | 'created' | 'kind' + | 'engineScript' | 'rulesBookPageCount' | 'zoomSpec' | 'tableSpec' @@ -59,11 +60,10 @@ export type Game = Pick< | 'meshes' | 'cameras' | 'hands' - | 'preferences' | 'availableSeats' | 'history' > - > & { players?: GamePlayer[] } + > & { preferencesString?: string; players?: GamePlayer[] } export type GameParameters = Pick< FullGameParameters, @@ -72,13 +72,13 @@ export type GameParameters = Pick< Partial< Pick< FullGameParameters, - | 'locales' - | 'preferences' - | 'rulesBookPageCount' - | 'availableSeats' - | 'colors' + 'locales' | 'rulesBookPageCount' | 'availableSeats' | 'colors' > - > & { schemaString?: string; players?: GamePlayer[] } + > & { + preferencesString?: string + schemaString?: string + players?: GamePlayer[] + } export interface CreateGameArgs { kind?: string // created game kind (omit to create a lobby). diff --git a/apps/server/src/repositories/catalog-items.js b/apps/server/src/repositories/catalog-items.js index 9b07b7a6..ea2a05bb 100644 --- a/apps/server/src/repositories/catalog-items.js +++ b/apps/server/src/repositories/catalog-items.js @@ -1,7 +1,9 @@ // @ts-check -import { readdir } from 'node:fs/promises' +import { readdir, readFile, stat } from 'node:fs/promises' import { pathToFileURL } from 'node:url' +import { build } from 'esbuild' + import { makeLogger } from '../utils/index.js' class CatalogItemRepository { @@ -15,6 +17,8 @@ class CatalogItemRepository { this.models = [] /** @type {Map} */ this.modelsByName = new Map() + /** @private @type {string} */ + this.root = '' this.logger = makeLogger(`${this.name}-repository`, { ctx: { name: this.name } }) @@ -28,8 +32,8 @@ class CatalogItemRepository { * @throws {Error} when provided path is not a readable folder. */ async connect({ path }) { - const root = pathToFileURL(path).pathname - const ctx = { root } + this.root = pathToFileURL(path).pathname + const ctx = { root: this.root } this.logger.trace({ ctx }, 'initializing repository') let entries this.models = [] @@ -45,27 +49,47 @@ class CatalogItemRepository { } for (const entry of entries) { if (entry.isDirectory() || entry.isSymbolicLink()) { - const descriptor = `${root}/${entry.name}/index.js` + const { name } = entry + const descriptor = `${this.root}/${name}/index.js` + const rules = `${this.root}/${name}/engine.min.js` + try { - const { name } = entry + /** @type {import('@tabulous/types').GameDescriptor} */ const item = { name, ...(await import(descriptor)) } + + this.logger.debug( + { ctx, gameName: name }, + `bundling rule engine for ${name}` + ) + const buildResult = await build({ + entryPoints: [descriptor], + bundle: true, + minify: true, + globalName: 'engine', + sourcemap: 'inline', + outfile: rules + }) + if (buildResult.errors.length) { + throw new Error( + `can not bundle rule engine: ${buildResult.errors + .map(({ text }) => text) + .join('\n')}` + ) + } + this.models.push(item) this.modelsByName.set(name, item) } catch (err) { - /* c8 ignore start */ // ignore folders with no index.js or invalid symbolic links - // Since recently (https://github.com/vitest-dev/vitest/commit/58ee8e9b6300fd6899072e34feb766805be1593c), - // it can not be tested under vitest because an uncatchable rejection will be thrown if ( err instanceof Error && !err.message.includes(`Cannot find module '${descriptor}'`) ) { throw new Error(`Failed to load game ${entry.name}: ${err.message}`) } - /* c8 ignore stop */ } } } @@ -137,6 +161,21 @@ class CatalogItemRepository { async deleteById() { throw new Error(`Catalog items can not be deleted`) } + + /** + * Reads the content of the rule engine file of a given catalog item. + * @param {string|undefined} name - name of the catalog item. + * @returns content of the engine script, if any. + */ + async getEngineScript(name) { + if (name) { + const rules = `${this.root}/${name}/engine.min.js` + const engineScript = await stat(rules) + .then(() => readFile(rules, 'utf-8')) + .catch(() => undefined) + return engineScript + } + } } /** diff --git a/apps/server/src/server.js b/apps/server/src/server.js index 1cd9b893..fc714126 100644 --- a/apps/server/src/server.js +++ b/apps/server/src/server.js @@ -26,8 +26,14 @@ export async function startServer(config) { }) app.decorate('conf', config) - await repositories.players.connect(config.data) - await repositories.games.connect(config.data) + await repositories.players.connect({ + ...config.data, + isProduction: config.isProduction + }) + await repositories.games.connect({ + ...config.data, + isProduction: config.isProduction + }) await repositories.catalogItems.connect(config.games) app.register(import('./plugins/cors.js'), config.plugins.cors) diff --git a/apps/server/src/services/games.js b/apps/server/src/services/games.js index f98c57a5..2e8dc24e 100644 --- a/apps/server/src/services/games.js +++ b/apps/server/src/services/games.js @@ -187,6 +187,7 @@ export async function promoteGame(gameId, kind, player) { ) reportReusedIds(game) notifyAllPeers(game) + game.engineScript = await repositories.catalogItems.getEngineScript(kind) logger.debug( { ctx, res: serializeForLogs(game) }, 'promotted looby into game' @@ -281,8 +282,11 @@ export async function joinGame(gameId, player, parameters) { if (!maybeGame) { return null } + const engineScript = await repositories.catalogItems.getEngineScript( + maybeGame.kind + ) if (isPlayer(maybeGame, player.id)) { - return maybeGame + return { ...maybeGame, engineScript } } if (!isGuest(maybeGame, player?.id)) { return null @@ -331,7 +335,7 @@ export async function joinGame(gameId, player, parameters) { reportReusedIds(savedGame) notifyAllPeers(savedGame) logger.debug({ ctx, res: serializeForLogs(savedGame) }, 'joined game') - return savedGame + return { ...savedGame, engineScript } } catch (error) { logger.warn( { ctx, error }, diff --git a/apps/server/tests/fixtures/unbuildable-games/import/index.js b/apps/server/tests/fixtures/unbuildable-games/import/index.js new file mode 100644 index 00000000..695a5ca6 --- /dev/null +++ b/apps/server/tests/fixtures/unbuildable-games/import/index.js @@ -0,0 +1,6 @@ +export function build() { + import('does-not-exist') + return { + mesh: [] + } +} diff --git a/apps/server/tests/graphql/games-resolver.test.js b/apps/server/tests/graphql/games-resolver.test.js index 97934528..f558bf43 100644 --- a/apps/server/tests/graphql/games-resolver.test.js +++ b/apps/server/tests/graphql/games-resolver.test.js @@ -216,7 +216,7 @@ describe('given a started server', () => { const playerId = players[0].id const kind = faker.helpers.arrayElement(['coinche', 'tarot', 'belote']) // @ts-expect-error: missing properties - const game = /** @type {GameData} */ ({ + const game = /** @type {import('@tabulous/types').GameData} */ ({ id: faker.string.uuid(), kind, created: Date.now(), @@ -296,7 +296,7 @@ describe('given a started server', () => { const playerId = players[0].id const kind = faker.helpers.arrayElement(['coinche', 'tarot', 'belote']) // @ts-expect-error: missing properties - const game = /** @type {GameData} */ ({ + const game = /** @type {import('@tabulous/types').GameData} */ ({ id: faker.string.uuid(), kind, created: Date.now(), @@ -353,6 +353,63 @@ describe('given a started server', () => { ) expect(services.promoteGame).toHaveBeenCalledOnce() }) + + it('serializes preferences', async () => { + const playerId = players[0].id + const id = faker.string.uuid() + const kind = faker.helpers.arrayElement(['coinche', 'tarot', 'belote']) + const preferences = [{ playerId, color: 'red' }] + services.getPlayerById.mockResolvedValueOnce(players[0]) + services.promoteGame.mockResolvedValueOnce({ + id, + name: kind, + kind, + locales: { en: { title: kind }, fr: { title: kind } }, + preferences, + created: Date.now(), + ownerId: playerId, + guestIds: [], + playerIds: [playerId], + availableSeats: 3, + meshes: [], + messages: [], + history: [], + cameras: [], + hands: [] + }) + + const response = await server.inject({ + method: 'POST', + url: 'graphql', + headers: { + authorization: `Bearer ${signToken( + playerId, + configuration.auth.jwt.key + )}` + }, + payload: { + query: `mutation { + promoteGame(gameId: "${id}", kind: "${kind}") { + ...on Game { + id + preferencesString + } + } +}` + } + }) + + expect(response.json()).toEqual({ + data: { + promoteGame: { id, preferencesString: JSON.stringify(preferences) } + } + }) + expect(response.statusCode).toEqual(200) + expect(services.getPlayerById).toHaveBeenCalledWith(playerId) + expect(services.getPlayerById).toHaveBeenCalledOnce() + expect(services.promoteGame).toHaveBeenCalledWith(id, kind, players[0]) + expect(services.promoteGame).toHaveBeenCalledOnce() + }) }) describe('joinGame mutation', () => { @@ -380,7 +437,8 @@ describe('given a started server', () => { created: faker.date.past().getTime(), ownerId: player.id, playerIds: players.map(({ id }) => id), - guestIds: guests.map(({ id }) => id) + guestIds: guests.map(({ id }) => id), + preferences: [{ playerId: player.id, color: 'red' }] }) services.getPlayerById .mockResolvedValueOnce(players[0]) @@ -404,6 +462,7 @@ describe('given a started server', () => { id kind created + preferencesString players { id username @@ -425,6 +484,8 @@ describe('given a started server', () => { ownerId: undefined, playerIds: undefined, guestIds: undefined, + preferences: undefined, + preferencesString: JSON.stringify(game.preferences), players: [...players, ...guests].map(obj => ({ ...obj, isGuest: guests.includes(obj), @@ -446,13 +507,18 @@ describe('given a started server', () => { it('loads game parameters', async () => { const [player] = players + /** @type {import('@tabulous/types').GameParameters} */ const gameParameters = - /** @type {import('@tabulous/types').GameParameters} */ ({ + /** @type {?} */ + ({ id: faker.string.uuid(), - schema: {}, ownerId: player.id, playerIds: players.map(({ id }) => id), - guestIds: guests.map(({ id }) => id) + guestIds: guests.map(({ id }) => id), + preferences: [{ playerId: player.id, color: 'red' }], + schema: { + color: { type: 'string', enum: ['red', 'green', 'blue'] } + } }) const value = faker.lorem.words() services.getPlayerById @@ -478,6 +544,8 @@ describe('given a started server', () => { } ... on GameParameters { id + preferencesString + schemaString players { id username @@ -496,6 +564,10 @@ describe('given a started server', () => { id: gameParameters.id, playerIds: undefined, guestIds: undefined, + preferences: undefined, + schema: undefined, + preferencesString: JSON.stringify(gameParameters.preferences), + schemaString: JSON.stringify(gameParameters.schema), players: [...players, ...guests].map(obj => ({ ...obj, isGuest: guests.includes(obj), @@ -665,7 +737,7 @@ describe('given a started server', () => { } ] // @ts-expect-error: missing properties - const game = /** @type {GameData} */ ({ + const game = /** @type {import('@tabulous/types').GameData} */ ({ id: faker.string.uuid(), kind, created: faker.date.past().getTime(), @@ -754,7 +826,7 @@ describe('given a started server', () => { const playerId = players[0].id const peerIds = players.slice(1, 3).map(({ id }) => id) // @ts-expect-error: missing properties - const game = /** @type {GameData} */ ({ + const game = /** @type {import('@tabulous/types').GameData} */ ({ id: faker.string.uuid(), kind: 'belote', created: faker.date.past().getTime(), @@ -834,7 +906,7 @@ describe('given a started server', () => { const playerId = players[0].id const kickedId = players[1].id // @ts-expect-error: missing properties - const game = /** @type {GameData} */ ({ + const game = /** @type {import('@tabulous/types').GameData} */ ({ id: faker.string.uuid(), kind: 'belote', created: faker.date.past().getTime(), @@ -913,7 +985,7 @@ describe('given a started server', () => { it('deletes an existing game and resolves player objects', async () => { const playerId = players[0].id // @ts-expect-error: missing properties - const game = /** @type {GameData} */ ({ + const game = /** @type {import('@tabulous/types').GameData} */ ({ id: faker.string.uuid(), kind: 'coinche', created: faker.date.past().getTime(), diff --git a/apps/server/tests/repositories/catalog-items.test.js b/apps/server/tests/repositories/catalog-items.test.js index b96dcad7..03aa1972 100644 --- a/apps/server/tests/repositories/catalog-items.test.js +++ b/apps/server/tests/repositories/catalog-items.test.js @@ -1,4 +1,5 @@ // @ts-check +import { readdir, rm, stat } from 'node:fs/promises' import { join } from 'node:path' import { faker } from '@faker-js/faker' @@ -41,16 +42,22 @@ describe('Catalog Items repository', () => { ] const fixtures = join('tests', 'fixtures', 'games') + const unbuildable = join('tests', 'fixtures', 'unbuildable-games') - afterEach(() => catalogItems.release()) + async function rmEngines(/** @type {string} */ folder) { + for (const file of await readdir(folder)) { + await rm(join(folder, file, 'engine.min.js'), { force: true }) + } + } - describe('connect()', () => { - it('throws an error on unreadable folder', async () => { - await expect( - catalogItems.connect({ path: faker.system.filePath() }) - ).rejects.toThrow('Failed to connect Catalog Items repository') - }) + afterEach(async () => { + // delete engine.min.js files inside fictures folder + await rmEngines(fixtures) + await rmEngines(unbuildable) + await catalogItems.release() + }) + describe('connect()', () => { it('throws an error on unreadable folder', async () => { await expect( catalogItems.connect({ path: faker.system.filePath() }) @@ -59,7 +66,7 @@ describe('Catalog Items repository', () => { // Since recently (https://github.com/vitest-dev/vitest/commit/58ee8e9b6300fd6899072e34feb766805be1593c), // it can not be tested under vitest because an uncatchable rejection will be thrown - it.skip('throws an invalid game descriptor', async () => { + it('throws an invalid game descriptor', async () => { await expect( catalogItems.connect({ path: join('tests', 'fixtures', 'broken-games') @@ -67,8 +74,8 @@ describe('Catalog Items repository', () => { ).rejects.toThrow(`Failed to load game broken`) }) - it.skip('handles an folder without game descriptors', async () => { - await catalogItems.connect({ path: join('tests', 'fixtures') }) + it('handles an folder without game descriptors', async () => { + await catalogItems.connect({ path: join('tests', 'graphql') }) expect(await catalogItems.list()).toEqual({ total: 0, from: 0, @@ -76,6 +83,34 @@ describe('Catalog Items repository', () => { results: [] }) }) + + it('builds rule engines', async () => { + await catalogItems.connect({ path: fixtures }) + const engines = [] + for (const file of await readdir(fixtures)) { + if ( + await stat(join(fixtures, file, 'engine.min.js')) + .then(() => true) + .catch(() => false) + ) { + engines.push(file) + } + } + expect(engines).toEqual([ + '6-takes', + 'belote', + 'draughts', + 'klondike', + 'splendor' + ]) + }) + + it('throws on un-buildable rule engines', async () => { + await expect(catalogItems.connect({ path: unbuildable })).rejects.toThrow( + `Failed to load game import: Build failed with 1 error: +tests/fixtures/unbuildable-games/import/index.js:2:9: ERROR: Could not resolve "does-not-exist"` + ) + }) }) describe('given a connected repository on mocked data', () => { @@ -121,6 +156,26 @@ describe('Catalog Items repository', () => { }) }) + describe('getEngineScript()', () => { + it('returns a script content by id', async () => { + expect( + (await catalogItems.getEngineScript('klondike'))?.split('\n')[0] + ).toEqual( + `"use strict";var engine=(()=>{var o=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var r=Object.getOwnPropertyNames;var i=Object.prototype.hasOwnProperty;var u=(t,e)=>{for(var n in e)o(t,n,{get:e[n],enumerable:!0})},p=(t,e,n,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of r(e))!i.call(t,s)&&s!==n&&o(t,s,{get:()=>e[s],enumerable:!(a=d(e,s))||a.enumerable});return t};var x=t=>p(o({},"__esModule",{value:!0}),t);var c={};u(c,{build:()=>b});function b(){return{meshes:[{shape:"card",id:"one-of-diamonds",texture:"test.ktx2"}],bags:new Map,slots:[]}}return x(c);})();` + ) + }) + + it('returns undefined on unknown id', async () => { + expect( + await catalogItems.getEngineScript(faker.string.uuid()) + ).toBeUndefined() + }) + + it('returns undefined on missing id', async () => { + expect(await catalogItems.getEngineScript(undefined)).toBeUndefined() + }) + }) + describe('save()', () => { it('throws error as it is not supported', async () => { await expect(catalogItems.save()).rejects.toThrow( diff --git a/apps/server/tests/server.test.js b/apps/server/tests/server.test.js index c6f62536..093ec595 100644 --- a/apps/server/tests/server.test.js +++ b/apps/server/tests/server.test.js @@ -85,9 +85,15 @@ describe('startServer()', () => { response = await app.inject({ url: 'splendor/index.js' }) expect(response.statusCode).toEqual(200) - expect(repositories.games.connect).toHaveBeenCalledWith({ url: 'data' }) + expect(repositories.games.connect).toHaveBeenCalledWith({ + url: 'data', + isProduction: true + }) expect(repositories.games.connect).toHaveBeenCalledOnce() - expect(repositories.players.connect).toHaveBeenCalledWith({ url: 'data' }) + expect(repositories.players.connect).toHaveBeenCalledWith({ + url: 'data', + isProduction: true + }) expect(repositories.players.connect).toHaveBeenCalledOnce() expect(repositories.catalogItems.connect).toHaveBeenCalledWith({ path: 'games' diff --git a/apps/server/tests/services/games.test.js b/apps/server/tests/services/games.test.js index 1c4a3709..8fbd52e4 100644 --- a/apps/server/tests/services/games.test.js +++ b/apps/server/tests/services/games.test.js @@ -291,7 +291,10 @@ describe('given a subscription to game lists and an initialized repository', () preferences: [{ playerId: player.id, color: expect.any(String) }] } const joinedGame = await joinGame(game.id, player, null) - expect(joinedGame).toEqual(expectedGame) + expect(joinedGame).toEqual({ + ...expectedGame, + engineScript: expect.any(String) + }) expect(joinedGame?.preferences[0].color).toMatch( new RegExp(game?.colors?.players?.join('|') ?? '') ) @@ -347,8 +350,10 @@ describe('given a subscription to game lists and an initialized repository', () hands: [{ playerId: player.id, meshes: [] }], preferences: [{ playerId: player.id, color: expect.any(String) }], zoomSpec: { min: 5, max: 50 }, - colors: { players: ['red', 'green', 'blue'] } + colors: { players: ['red', 'green', 'blue'] }, + engineScript: expect.any(String) }) + delete joinedGame?.engineScript await setTimeout(50) expect(updates).toEqual([ { playerId: player.id, games: expect.arrayContaining([joinedGame]) } @@ -381,6 +386,7 @@ describe('given a subscription to game lists and an initialized repository', () game = /** @type {import('@tabulous/types').GameData} */ ( await joinGame(game.id, player) ) + delete game?.engineScript lobby = /** @type {import('@tabulous/types').GameData} */ ( await joinGame(lobby.id, player) ) @@ -390,7 +396,10 @@ describe('given a subscription to game lists and an initialized repository', () describe('joinGame()', () => { it('returns owned game', async () => { - expect(await joinGame(game.id, player)).toEqual(game) + expect(await joinGame(game.id, player)).toEqual({ + ...game, + engineScript: expect.any(String) + }) }) it(`adds guest id to game's player id and preference lists, and trigger list updates`, async () => { @@ -411,8 +420,10 @@ describe('given a subscription to game lists and an initialized repository', () preferences: [ { playerId: player.id, color: expect.any(String) }, { playerId: peer.id, color: expect.any(String) } - ] + ], + engineScript: expect.any(String) }) + delete updated?.engineScript await setTimeout(50) expect(updates).toEqual( expect.arrayContaining([ @@ -527,12 +538,14 @@ describe('given a subscription to game lists and an initialized repository', () playerIds: [], zoomSpec: game.zoomSpec, preferences: [], - colors: { players: ['red', 'green', 'blue'] } + colors: { players: ['red', 'green', 'blue'] }, + engineScript: expect.any(String) } expect( await promoteGame(lobby.id, /** @type {string} */ (kind), player) ).toEqual(expectedGame) await setTimeout(50) + delete expectedGame.engineScript expect(updates).toEqual([ { playerId: player.id, @@ -550,12 +563,14 @@ describe('given a subscription to game lists and an initialized repository', () availableSeats: 2, guestIds: [player.id], playerIds: [], - preferences: [] + preferences: [], + engineScript: expect.any(String) } expect(await promoteGame(lobby.id, kind, player)).toEqual( expectedGame ) await setTimeout(50) + delete expectedGame.engineScript expect(updates).toEqual([ { playerId: player.id, @@ -578,12 +593,15 @@ describe('given a subscription to game lists and an initialized repository', () playerIds: [], zoomSpec: game.zoomSpec, preferences: [], - colors: { players: ['red', 'green', 'blue'] } + colors: { players: ['red', 'green', 'blue'] }, + engineScript: expect.any(String) } expect( await promoteGame(lobby.id, /** @type {string} */ (kind), player) ).toEqual(expectedGame) + await setTimeout(50) + delete expectedGame.engineScript expect(updates).toEqual( expect.arrayContaining([ { playerId: peer.id, games: [expectedGame] }, @@ -611,12 +629,14 @@ describe('given a subscription to game lists and an initialized repository', () playerIds: [], zoomSpec: game.zoomSpec, preferences: [], - colors: { players: ['red', 'green', 'blue'] } + colors: { players: ['red', 'green', 'blue'] }, + engineScript: expect.any(String) } expect( await promoteGame(lobby.id, /** @type {string} */ (kind), peer2) ).toEqual(expectedGame) await setTimeout(50) + delete expectedGame.engineScript expect(updates).toEqual( expect.arrayContaining([ { playerId: peer2.id, games: [expectedGame] }, @@ -667,12 +687,14 @@ describe('given a subscription to game lists and an initialized repository', () playerIds: [], zoomSpec: game.zoomSpec, preferences: [], - colors: { players: ['red', 'green', 'blue'] } + colors: { players: ['red', 'green', 'blue'] }, + engineScript: expect.any(String) } expect(await promoteGame(lobby.id, kind, player)).toEqual( expectedGame ) await setTimeout(50) + delete expectedGame.engineScript expect(updates).toEqual([ { playerId: player.id, @@ -852,6 +874,7 @@ describe('given a subscription to game lists and an initialized repository', () }) await setTimeout(50) updates.splice(0) + delete updatedGame?.engineScript expect(await invite(game.id, [peer.id], player.id)).toEqual( updatedGame @@ -969,6 +992,7 @@ describe('given a subscription to game lists and an initialized repository', () game = /** @type {import('@tabulous/types').GameData} */ ( await joinGame(game.id, peer) ) + delete game.engineScript await invite(lobby.id, [peer.id], player.id) lobby = /** @type {import('@tabulous/types').GameData} */ ( await joinGame(lobby.id, peer) @@ -1219,6 +1243,7 @@ describe('given a subscription to game lists and an initialized repository', () await joinGame(game1.id, peer) ) ) + delete games[games.length - 1]?.engineScript const game2 = await createGame('belote', peer2) await joinGame(game2.id, peer2) await invite(game2.id, [player.id], peer2.id) @@ -1227,6 +1252,7 @@ describe('given a subscription to game lists and an initialized repository', () await joinGame(game2.id, player) ) ) + delete games[games.length - 1]?.engineScript await setTimeout(50) updates.splice(0, updates.length) @@ -1257,6 +1283,7 @@ describe('given a subscription to game lists and an initialized repository', () await joinGame(game1.id, peer) ) ) + delete games[games.length - 1]?.engineScript const game2 = await createGame('belote', player) await joinGame(game2.id, player) await invite(game2.id, [peer.id], player.id) @@ -1265,6 +1292,7 @@ describe('given a subscription to game lists and an initialized repository', () await joinGame(game2.id, peer) ) ) + delete games[games.length - 1]?.engineScript await setTimeout(50) updates.splice(0, updates.length) @@ -1350,7 +1378,8 @@ describe('given a subscription to game lists and an initialized repository', () availableSeats: 1, guestIds: [], playerIds: [player.id], - preferences: [{ playerId: player.id, color, side }] + preferences: [{ playerId: player.id, color, side }], + engineScript: expect.any(String) }) }) }) diff --git a/apps/types/index.d.ts b/apps/types/index.d.ts index 2b9d8071..0e7e0745 100644 --- a/apps/types/index.d.ts +++ b/apps/types/index.d.ts @@ -66,6 +66,8 @@ declare module '.' { addPlayer?: AddPlayer /** function invoked to generate a joining player's parameters. */ askForParameters?: AskForParameters + /** function invoked to compute score on an action */ + computeScore?: ComputeScore } /** All the localized data for a catalog item. */ @@ -161,14 +163,11 @@ declare module '.' { export type Build = () => GameSetup | Promise /** Function invoked when adding a player to the game. */ - export type AddPlayer< - Parameters extends Record, - Game extends GameData = GameData - > = ( - game: Game, + export type AddPlayer> = ( + game: StartedGame, guest: Player, parameters: Parameters - ) => Game | Promise + ) => StartedGame | Promise export type Schema> = JSONSchemaType @@ -188,6 +187,14 @@ declare module '.' { player: Player }) => ?(Schema | Promise>) + /** Function invoked to compute scores after a given action */ + export type ComputeScore = ( + action: ?Action, + state: EngineState, + players: Pick[], + preferences: PlayerPreference[] + ) => Promise | Scores | undefined + /** * Setup for a given game instance, including meshes, bags and slots. * Meshes could be cards, round tokens, rounded tiles... They must have an id. @@ -263,6 +270,8 @@ declare module '.' { preferences: PlayerPreference[] /** player actions and move history. */ history: HistoryRecord[] + /** bundled rule engine sent to the client, if any */ + engineScript?: string } /** Data of a started game. */ @@ -412,20 +421,22 @@ declare module '.' { duration?: number } - /** A rectangular anchor definition (coordinates are relative to the parent mesh). */ + /** An anchor definition (coordinates are relative to the parent mesh). */ export type Anchor = { /** this anchor id. */ id: string - /** id of the mesh currently snapped to this anchor. */ - snappedId?: ?string + /** ids of meshes currently snapped to this anchor. */ + snappedIds: string[] /** when set, only this player can snap meshes to this anchor. */ playerId?: string - /** angle applied to any rotable mesh snapped to the anchor. */ + /** when set, angle applied to any rotable mesh snapped to the anchor. */ angle?: number - /** flip state applied to any flippable mesh snapped to the anchor. */ + /** when set, flip state applied to any flippable mesh snapped to the anchor. */ flip?: boolean /** when set, and when snapping a multi-part mesh, takes it barycenter into account. */ ignoreParts?: boolean + /** maximum number of snapped meshes, defaults to 1 */ + max?: number } & Point & Dimension & _Targetable @@ -568,6 +579,59 @@ declare module '.' { /** required to connect. */ credentials: string } + + /** Local game engine serialized state. */ + export type EngineState = { + meshes: Mesh[] + handMeshes: Mesh[] + history: HistoryRecord[] + } + + /** applied action to a given mesh. */ + export interface Action { + /** name of the applied action. */ + fn: ActionName + /** modified mesh id. */ + meshId: string + /** indicates whether this action comes from hand or main scene. */ + fromHand: boolean + /** modified mesh id. */ + meshId: string + /** indicates whether this action comes from hand or main scene. */ + fromHand: boolean + /** argument array for this action. */ + args: any[] + /** when action can't be reverted with the same args, specific data required. */ + revert?: any[] + /** optional animation duration, in milliseconds. */ + duration?: number + /** indicates a local action that should not be re-recorded nor sent to peers. */ + isLocal?: boolean + } + + /** applied move to a given mesh: */ + export interface Move { + /** absolute position. */ + pos: number[] + /** absolute position before the move. */ + prev: number[] + /** optional animation duration, in milliseconds. */ + duration?: number + /** modified mesh id. */ + meshId: string + /** indicates whether this action comes from hand or main scene. */ + fromHand: boolean + } + + export type ActionOrMove = Action | Move + + /** A given player's score, with optional components */ + export type Score = Record & { + total: string | number + } + + /** All player scores */ + export type Scores = Record } /** Common properties for targets (stacks, anchors, quantifiable...) */ diff --git a/apps/web/package.json b/apps/web/package.json index 71a9a2bb..1d824237 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -40,6 +40,7 @@ "@sveltejs/adapter-vercel": "^3.0.3", "@sveltejs/kit": "^1.25.0", "@sveltejs/vite-plugin-svelte": "^2.4.6", + "@tabulous/game-utils": "workspace:*", "@tabulous/types": "workspace:*", "@testing-library/dom": "^9.3.1", "@testing-library/jest-dom": "^6.1.3", diff --git a/apps/web/src/3d/behaviors/anchorable.js b/apps/web/src/3d/behaviors/anchorable.js index 29bee760..21601193 100644 --- a/apps/web/src/3d/behaviors/anchorable.js +++ b/apps/web/src/3d/behaviors/anchorable.js @@ -12,7 +12,8 @@ import { getPositionAboveZone, getTargetableBehavior } from '../utils/behaviors' -import { AnchorBehaviorName } from './names' +import { getAltitudeAfterGravity } from '../utils/gravity' +import { AnchorBehaviorName, FlipBehaviorName } from './names' import { TargetBehavior } from './targetable' /** @typedef {import('@tabulous/types').AnchorableState & Required>} RequiredAnchorableState */ @@ -34,7 +35,7 @@ export class AnchorBehavior extends TargetBehavior { this.dropObserver = null /** @protected @type {?import('@babylonjs/core').Observer} */ this.moveObserver = null - /** @protected @type {?import('@babylonjs/core').Observer}} */ + /** @protected @type {?import('@babylonjs/core').Observer}} */ this.actionObserver = null /** @internal @type {Map} */ this.zoneBySnappedId = new Map() @@ -96,7 +97,7 @@ export class AnchorBehavior extends TargetBehavior { const { meshId, fn, args } = actionOrMove if (meshId === this.mesh.id && fn === actionNames.draw) { for (const snappedId of this.getSnappedIds()) { - internalUnsnap(this, snappedId, undefined, true) + internalUnsnap(this, snappedId, true) } } else { const zone = this.zoneBySnappedId.get(meshId) @@ -115,7 +116,7 @@ export class AnchorBehavior extends TargetBehavior { } } } else if (fn === actionNames.draw) { - internalUnsnap(this, meshId, undefined, true) + internalUnsnap(this, meshId, true) } } } @@ -139,7 +140,7 @@ export class AnchorBehavior extends TargetBehavior { */ enable() { for (const zone of [...this.zones]) { - if (!this.state.anchors[zone.anchorIndex].snappedId) { + if (canEnableAnchor(this, zone.anchorIndex)) { zone.enabled = true } } @@ -223,8 +224,15 @@ export class AnchorBehavior extends TargetBehavior { async revert(action, args = []) { if (action === actionNames.snap && args.length === 4) { const [snappedId, position, angle, isFlipped] = args - const released = await internalUnsnap(this, snappedId, isFlipped, true) + const released = getMeshList(this.mesh?.getScene(), snappedId)?.[0] if (released) { + if ( + isFlipped != undefined && + released.metadata.isFlipped !== isFlipped + ) { + await this.managers.control.invokeLocal(released, actionNames.flip) + } + await internalUnsnap(this, snappedId, true) await animateMove( released, Vector3.FromArray(position), @@ -257,7 +265,7 @@ export class AnchorBehavior extends TargetBehavior { this.state = { anchors, duration } for (const [ i, - { id, x, y, z, width, depth, height, diameter, snappedId, ...zoneProps } + { id, x, y, z, width, depth, height, diameter, snappedIds, ...zoneProps } ] of this.state.anchors.entries()) { const dropZone = buildTargetMesh( id, @@ -279,7 +287,7 @@ export class AnchorBehavior extends TargetBehavior { }) // relates the created zone with the anchor zone.anchorIndex = i - if (snappedId) { + for (const snappedId of snappedIds ?? []) { snapToAnchor(this, snappedId, zone, true) } } @@ -310,13 +318,6 @@ async function internalSnap( } const position = snapped.position.asArray() const angle = snapped.metadata.angle - behavior.managers.indicator.registerFeedback({ - action: actionNames.snap, - position: zone.mesh.absolutePosition.asArray() - }) - behavior.managers.move.notifyMove(snapped) - await snapToAnchor(behavior, snappedId, zone, immediate) - // record after so flippable could flip on demand, after the mesh was snapped. behavior.managers.control.record({ mesh: behavior.mesh, fn: actionNames.snap, @@ -325,25 +326,21 @@ async function internalSnap( revert: [snappedId, position, angle, snapped.metadata.isFlipped], isLocal }) - const isFlipped = behavior.getZoneFlip(anchorId) - if (isFlipped != undefined && snapped.metadata.isFlipped !== isFlipped) { - await behavior.managers.control.invokeLocal(snapped, actionNames.flip) - } + behavior.managers.indicator.registerFeedback({ + action: actionNames.snap, + position: zone.mesh.absolutePosition.asArray() + }) + behavior.managers.move.notifyMove(snapped) + await snapToAnchor(behavior, snappedId, zone, immediate) } /** * Internal implementation of the unsnap/revertSnap methods. * @param {AnchorBehavior} behavior - concerned behavior. * @param {string} releasedId - the unsnapped mesh id. - * @param {boolean} [isFlipped] - new flip status to enforce, if any. * @param {boolean} [isLocal] - locality for this action. */ -async function internalUnsnap( - behavior, - releasedId, - isFlipped = undefined, - isLocal = false -) { +async function internalUnsnap(behavior, releasedId, isLocal = false) { const released = getMeshList(behavior.mesh?.getScene(), releasedId)?.[0] if (behavior.mesh && released) { @@ -355,14 +352,10 @@ async function internalUnsnap( { mesh: behavior.mesh, snappedId, zone }, `release snapped ${snappedId} from ${behavior.mesh.id}, zone ${zone.mesh.id}` ) - if (isFlipped != undefined && released.metadata.isFlipped !== isFlipped) { - await behavior.managers.control.invokeLocal(released, actionNames.flip) - } behavior.managers.control.record({ mesh: behavior.mesh, fn: actionNames.unsnap, - args: [releasedId], - revert: [releasedId, zone.mesh.id], + args: [releasedId, zone.mesh.id], isLocal }) unsetAnchor(behavior, zone, released) @@ -388,7 +381,7 @@ async function snapToAnchor(behavior, snappedId, zone, loading = false) { } = behavior if (!mesh) return const meshId = mesh.id - anchors[zone.anchorIndex].snappedId = undefined + const anchor = anchors[zone.anchorIndex] const snapped = getMeshList(mesh.getScene(), snappedId)?.[0] if (snapped) { @@ -399,7 +392,10 @@ async function snapToAnchor(behavior, snappedId, zone, loading = false) { setAnchor(behavior, zone, snapped) // moves it to the final position - const position = getPositionAboveZone(snapped, zone) + const position = + (anchor.max ?? 1) === 1 + ? getPositionAboveZone(snapped, zone) + : getAltitudeAfterGravity(snapped) const partCenters = getMeshAbsolutePartCenters(snapped) if (!zone.ignoreParts && partCenters) { // always use first part as a reference @@ -420,6 +416,22 @@ async function snapToAnchor(behavior, snappedId, zone, loading = false) { if (!loading) { await move } + const isFlipped = anchor.flip + const flippable = snapped.getBehaviorByName(FlipBehaviorName) + if ( + isFlipped != undefined && + flippable && + flippable?.state.isFlipped !== isFlipped + ) { + if (loading) { + flippable.fromState({ ...flippable.state, isFlipped }) + } else { + await behavior.managers.control.invokeLocal(snapped, actionNames.flip) + } + } + } else { + // removes invalid snappedIds + anchor.snappedIds.splice(anchor.snappedIds.indexOf(snappedId), 1) } } @@ -437,8 +449,10 @@ function setAnchor(behavior, zone, snapped) { snapped.setParent(mesh) zoneBySnappedId.set(snapped.id, zone) const anchor = anchors[zone.anchorIndex] - anchor.snappedId = snapped.id - zone.enabled = false + if (!anchor.snappedIds.includes(snapped.id)) { + anchor.snappedIds.push(snapped.id) + } + zone.enabled = anchor.snappedIds.length < (anchor.max ?? 1) } /** @@ -454,8 +468,9 @@ function unsetAnchor(behavior, zone, snapped) { snapped.setParent(null) zoneBySnappedId.delete(snapped.id) const anchor = anchors[zone.anchorIndex] - anchor.snappedId = undefined + anchor.snappedIds.splice(anchor.snappedIds.indexOf(snapped.id), 1) zone.enabled = true + applyGravityToSnapped(behavior, anchor) } /** @@ -473,3 +488,24 @@ function getMeshList(scene, meshId) { ) return stackable?.stack ? [...stackable.stack] : [mesh] } + +function canEnableAnchor( + /** @type {AnchorBehavior} */ { state }, + /** @type {number} */ anchorIndex +) { + const anchor = state.anchors[anchorIndex] + return (anchor.max ?? 1) > 1 || anchor.snappedIds.length === 0 +} + +function applyGravityToSnapped( + /** @type {AnchorBehavior} */ { mesh }, + /** @type {import('@tabulous/types').Anchor} */ anchor +) { + const scene = mesh?.getScene() + for (const snappedId of anchor.snappedIds) { + const snapped = getMeshList(scene, snappedId)?.[0] + if (snapped) { + snapped.setAbsolutePosition(getAltitudeAfterGravity(snapped)) + } + } +} diff --git a/apps/web/src/3d/behaviors/drawable.js b/apps/web/src/3d/behaviors/drawable.js index 80003bb4..6c3c17af 100644 --- a/apps/web/src/3d/behaviors/drawable.js +++ b/apps/web/src/3d/behaviors/drawable.js @@ -188,7 +188,7 @@ export class DrawBehavior extends AnimateBehavior { * @returns {Promise<{ fadeKeys: import('../utils').FloatKeyFrame[], moveKeys: import('../utils').Vector3KeyFrame[] }>} generated key frames. */ async function buildAnimationKeys(mesh, invert = false) { - // delay so that all observer of onAction to perform: we need the mesh to be have not parents before getting its position + // delay to let onAction obervers finish: we need the mesh to be have not parents before getting its position await Promise.resolve() const { x, y, z } = mesh.position return { diff --git a/apps/web/src/3d/behaviors/quantifiable.js b/apps/web/src/3d/behaviors/quantifiable.js index 3bbbca69..14603c66 100644 --- a/apps/web/src/3d/behaviors/quantifiable.js +++ b/apps/web/src/3d/behaviors/quantifiable.js @@ -131,22 +131,21 @@ export class QuantityBehavior extends TargetBehavior { * * @param {number} [count=1] - amount to decrement. * @param {boolean} [withMove=false] - when set to true, moves the created meshes aside this one. + * @param {string} [id] - id of the created mesh. * @returns the created mesh, if any. */ - async decrement(count = 1, withMove = false) { + async decrement(count = 1, withMove = false, id = undefined) { const { mesh, state } = this /** @type {?import('@babylonjs/core').Mesh} */ let created = null if (!mesh || state.quantity === 1) return created - const createdId = makeId(mesh) + const createdId = id ?? makeId(mesh) const duration = withMove ? state.duration : undefined this.managers.control.record({ mesh, fn: actionNames.decrement, - args: [count, withMove], + args: [count, withMove, createdId], duration, - // undo by incrementing created id with potential animation - revert: [createdId, withMove], isLocal: false }) @@ -191,7 +190,7 @@ export class QuantityBehavior extends TargetBehavior { * @param {any[]} [args] - reverted arguments. */ async revert(action, args = []) { - if (!this.mesh || args.length !== 2) { + if (!this.mesh || args.length < 2) { return } if (action === actionNames.increment) { @@ -208,9 +207,8 @@ export class QuantityBehavior extends TargetBehavior { this.managers.control.record({ mesh, fn: actionNames.decrement, - args: [count, withMove], + args: [count, withMove, state.id], duration, - revert: [state.id, withMove], isLocal: true }) this.state.quantity -= count diff --git a/apps/web/src/3d/behaviors/stackable.js b/apps/web/src/3d/behaviors/stackable.js index 4bcd4493..00a60846 100644 --- a/apps/web/src/3d/behaviors/stackable.js +++ b/apps/web/src/3d/behaviors/stackable.js @@ -58,7 +58,7 @@ export class StackBehavior extends TargetBehavior { this.moveObserver = null /** @protected @type {?import('@babylonjs/core').Observer} */ this.dropObserver = null - /** @protected @type {?import('@babylonjs/core').Observer} */ + /** @protected @type {?import('@babylonjs/core').Observer} */ this.actionObserver = null /** @internal @type {boolean} */ this.isReordering = false diff --git a/apps/web/src/3d/behaviors/targetable.js b/apps/web/src/3d/behaviors/targetable.js index 0a15244d..b6804986 100644 --- a/apps/web/src/3d/behaviors/targetable.js +++ b/apps/web/src/3d/behaviors/targetable.js @@ -3,7 +3,7 @@ import { Observable } from '@babylonjs/core/Misc/observable.js' import { TargetBehaviorName } from './names' -/** @typedef {Required> & Pick} ZoneProps properties of a drop zone */ +/** @typedef {Required> & Pick} ZoneProps properties of a drop zone */ export class TargetBehavior { /** @@ -77,7 +77,8 @@ export class TargetBehavior { ...properties, ignoreParts: properties.ignoreParts ?? false, enabled: properties.enabled ?? true, - priority: properties.priority ?? 0 + priority: properties.priority ?? 0, + max: properties.max ?? 1 } if (properties.playerId) { const id = `${properties.playerId}.drop-zone.${mesh.id}` diff --git a/apps/web/src/3d/engine.js b/apps/web/src/3d/engine.js index 99e0044d..031bb484 100644 --- a/apps/web/src/3d/engine.js +++ b/apps/web/src/3d/engine.js @@ -23,6 +23,7 @@ import { MaterialManager, MoveManager, ReplayManager, + RuleManager, SelectionManager, TargetManager } from './managers' @@ -134,7 +135,8 @@ function initEngineAnScenes( replay: new ReplayManager({ engine, moveDuration: isSimulation ? 0 : 200 - }) + }), + rule: new RuleManager({ engine }) } engine.start = () => @@ -160,7 +162,7 @@ function initEngineAnScenes( engine.load = async ( gameData, - { playerId, preferences, colorByPlayerId }, + { playerId, preference, colorByPlayerId }, initial ) => { const game = removeNulls(gameData) @@ -198,10 +200,15 @@ function initEngineAnScenes( managers.hand.init({ managers, playerId, - angleOnPlay: preferences?.angle + angleOnPlay: preference?.angle }) if (!isSimulation) { + await managers.rule.init({ + managers: managers, + engineScript: game.engineScript, + ...gameData + }) managers.input.init({ managers }) createLights({ scene, handScene }) } @@ -216,6 +223,7 @@ function initEngineAnScenes( } managers.selection.init({ managers, playerId, colorByPlayerId }) await managers.customShape.init(game) + managers.rule.update(gameData) await loadMeshes(scene, game.meshes ?? [], managers) if (managers.hand.enabled) { @@ -253,7 +261,7 @@ function initEngineAnScenes( } engine.applyRemoteAction = async ( - /** @type {import('@src/3d/managers').ActionOrMove} */ actionOrMove, + /** @type {import('@tabulous/types').ActionOrMove} */ actionOrMove, /** @type {string} */ playerId ) => { managers.replay.record(actionOrMove, playerId) diff --git a/apps/web/src/3d/managers/control.js b/apps/web/src/3d/managers/control.js index b7646090..f3b4d6f4 100644 --- a/apps/web/src/3d/managers/control.js +++ b/apps/web/src/3d/managers/control.js @@ -6,20 +6,6 @@ import { actionNames } from '../utils/actions' import { animateMove } from '../utils/behaviors' /** - * @typedef {object} _Action - * @property {string} meshId - modified mesh id. - * @property {boolean} fromHand - indicates whether this action comes from hand or main scene. - * - * @typedef {Omit & _Action} Action applied action to a given mesh. - * - * @typedef {object} _Move applied move to a given mesh: - * @property {string} meshId - modified mesh id. - * @property {boolean} fromHand - indicates whether this action comes from hand or main scene. - * - * @typedef {Omit & _Move} Move applied move to a given mesh. - * - * @typedef {Action|Move} ActionOrMove - * * @typedef {object} RecordedAction applied action to a given mesh: * @property {import('@babylonjs/core').Mesh} mesh - modified mesh. * @property {import('@tabulous/types').ActionName} fn - name of the applied action. @@ -51,7 +37,7 @@ export class ControlManager { * @param {import('@babylonjs/core').Scene} params.handScene - scene for meshes in hand. */ constructor({ scene, handScene }) { - /** @type {Observable} emits applied actions. */ + /** @type {Observable} */ this.onActionObservable = new Observable() /** @type {Observable} emits when displaying details of a given mesh. */ this.onDetailedObservable = new Observable() @@ -171,7 +157,7 @@ export class ControlManager { /** * Reverts an action by calling a mesh's behavior revert() function. * Reverts a move by positioning it back to its previous position. - * @param {Omit|Omit} actionOrMove - reverted action or move. + * @param {Omit|Omit} actionOrMove - reverted action or move. */ async revert(actionOrMove) { const mesh = this.controlables.get(actionOrMove.meshId) @@ -208,7 +194,7 @@ export class ControlManager { * Applies an actions to a controlled meshes (`fn` in its metadatas), or changes its position (action.pos is defined). * Does nothing if the target mesh is not controlled. * Returns when the action is fully applied. - * @param {Omit|Omit} action - applied action. + * @param {Omit|Omit} action - applied action. */ async apply(action) { const mesh = this.controlables.get(action?.meshId) @@ -235,6 +221,8 @@ export class ControlManager { } } -function getKey(/** @type {Partial>} */ action) { +function getKey( + /** @type {Partial>} */ action +) { return `${action?.meshId}-${action.fn?.toString() || 'pos'}` } diff --git a/apps/web/src/3d/managers/hand.js b/apps/web/src/3d/managers/hand.js index d4457270..2f8b8e60 100644 --- a/apps/web/src/3d/managers/hand.js +++ b/apps/web/src/3d/managers/hand.js @@ -156,8 +156,9 @@ export class HandManager { }, { observable: this.managers.control.onActionObservable, - handle: (/** @type {import('.').ActionOrMove} */ action) => - handleAction(this, action) + handle: ( + /** @type {import('@tabulous/types').ActionOrMove} */ action + ) => handleAction(this, action) }, { observable: this.managers.input.onDragObservable, @@ -373,7 +374,7 @@ export class HandManager { /** * @param {HandManager} manager - manager instance. - * @param {import('.').ActionOrMove} action - applied action. + * @param {import('@tabulous/types').ActionOrMove} action - applied action. */ function handleAction(manager, action) { if ( diff --git a/apps/web/src/3d/managers/index.js b/apps/web/src/3d/managers/index.js index b0ee8a19..f7d821ed 100644 --- a/apps/web/src/3d/managers/index.js +++ b/apps/web/src/3d/managers/index.js @@ -10,6 +10,7 @@ * @property {import('@src/3d/managers/material').MaterialManager} material * @property {import('@src/3d/managers/move').MoveManager} move * @property {import('@src/3d/managers/replay').ReplayManager} replay + * @property {import('@src/3d/managers/rule').RuleManager} rule * @property {import('@src/3d/managers/selection').SelectionManager} selection * @property {import('@src/3d/managers/target').TargetManager} target */ @@ -22,5 +23,6 @@ export * from './input' export * from './material' export * from './move' export * from './replay' +export * from './rule' export * from './selection' export * from './target' diff --git a/apps/web/src/3d/managers/replay.js b/apps/web/src/3d/managers/replay.js index a0603d89..c2c9a7bd 100644 --- a/apps/web/src/3d/managers/replay.js +++ b/apps/web/src/3d/managers/replay.js @@ -25,7 +25,7 @@ export class ReplayManager { this.onHistoryObservable = new Observable() /** @type {Observable} emits when the replay ranks is modified. */ this.onReplayRankObservable = new Observable() - /** @type {import('@babylonjs/core').Observer?} */ + /** @type {import('@babylonjs/core').Observer?} */ this.actionObserver /** @internal avoid concurrent replays */ this.inhibitReplay = false @@ -81,7 +81,7 @@ export class ReplayManager { * Record a new action or move into history. * It collapses redundant moves together and notifies listeners. * Only updates current rank if it was on last. - * @param {import('.').ActionOrMove} record - received record. + * @param {import('@tabulous/types').ActionOrMove} record - received record. * @param {string} [playerId] - id of the player who sent the record. */ record(record, playerId = this.playerId) { diff --git a/apps/web/src/3d/managers/rule.js b/apps/web/src/3d/managers/rule.js new file mode 100644 index 00000000..c8df59c8 --- /dev/null +++ b/apps/web/src/3d/managers/rule.js @@ -0,0 +1,117 @@ +// @ts-check +import { Observable } from '@babylonjs/core/Misc/observable.js' + +import { makeLogger } from '../../utils' + +const logger = makeLogger('rule') + +export class RuleManager { + /** + * Builds a manager to orchestrate replaying, backward and forward, history records. + * Invokes init() before any other function. + * @param {object} params - parameters, including: + * @param {import('@babylonjs/core').Engine} params.engine - 3d engine. + */ + constructor({ engine }) { + /** game engine. */ + this.engine = engine + /** @type {import('@src/graphql').LightPlayer[]} ordered list of player. */ + this.players = [] + /** @type {import('@tabulous/types').PlayerPreference[]} preferences for all players. */ + this.preferences = [] + /** @type {Observable} emits on score changes. */ + this.onScoreUpdateObservable = new Observable() + /** @internal @type {import('@babylonjs/core').Observer?} */ + this.actionObserver = null + /** @internal @type {?{ computeScore: import('@tabulous/types').ComputeScore }} */ + this.ruleEngine = null + + this.engine.onDisposeObservable.addOnce(() => this.dispose()) + } + + /** + * Initializes with game rules, by loading the rule script into the document's head. + * Connects to the control manager to evaluate rules. + * @param {object} params - parameters, including: + * @param {import('@src/3d/managers').Managers} params.managers - current managers. + * @param {string} [params.engineScript] - bundled rule engine, if any. + * @param {import('@src/graphql').LightPlayer[]} [params.players] - list of players. + * @param {import('@tabulous/types').PlayerPreference[]} [params.preferences] - list of player preferences. + */ + async init({ managers, engineScript, ...updateParams }) { + this.dispose() + this.managers = managers + this.update(updateParams) + + if (engineScript) { + logger.debug({ engineScript }, 'loading rules script') + this.ruleEngine = new Function(`${engineScript};return engine`)() + this.actionObserver = managers.control.onActionObservable.add(action => + evaluateScore(this, action) + ) + this.engine.onLoadingObservable.addOnce(() => evaluateScore(this, null)) + this.engine.onDisposeObservable.addOnce(() => { + this.players = [] + this.preferences = [] + }) + } + logger.info({ ruleEngine: this.ruleEngine }, 'rule manager initialized') + } + + /** + * Update the list of player, only retaining the active ones. + * @param {object} params - parameters, including + * @param {import('@src/graphql').LightPlayer[]} [params.players] - list of players. + * @param {import('@tabulous/types').PlayerPreference[]} [params.preferences] - list of player preferences. + */ + update({ players, preferences }) { + logger.trace({ players, preferences }, 'updating rule manager') + if (Array.isArray(players)) { + this.players = players.filter(({ isGuest }) => !isGuest) + } + if (Array.isArray(preferences)) { + this.preferences = preferences + } + } + + /** + * Unregisters from other managers, and removes added script. + */ + dispose() { + if (this.actionObserver) { + this.managers?.control.onActionObservable.remove(this.actionObserver) + this.actionObserver = null + } + this.ruleEngine = null + } +} + +async function evaluateScore( + /** @type {RuleManager} */ { + ruleEngine, + engine, + players, + preferences, + onScoreUpdateObservable + }, + /** @type {?import('@tabulous/types').ActionOrMove} */ action +) { + if (action && !('fn' in action)) { + return + } + const state = engine.serialize() + logger.debug({ action, state }, `evaluate score`) + try { + const scores = await ruleEngine?.computeScore?.( + action, + state, + players, + preferences + ) + if (scores) { + onScoreUpdateObservable.notifyObservers(scores) + } + } catch (error) { + logger.warn({ error, action }, `failed to evaluate score`) + } +} diff --git a/apps/web/src/3d/managers/score.js b/apps/web/src/3d/managers/score.js deleted file mode 100644 index 77883a07..00000000 --- a/apps/web/src/3d/managers/score.js +++ /dev/null @@ -1,93 +0,0 @@ -import { makeLogger } from '../../utils' -import { actionNames } from '../utils/actions' - -const logger = makeLogger('rules') - -const { snap, unsnap } = actionNames - -export class ScoreManager { - /** - * Builds a manager to orchestrate replaying, backward and forward, history records. - * Invokes init() before any other function. - * @param {object} params - parameters, including: - * @param {import('@babylonjs/core').Engine} params.engine - 3d engine. - */ - constructor({ engine }) { - /** game engin. */ - this.engine = engine - /** @type {import('@babylonjs/core').Observer?} */ - this.actionObserver - this.engine.onDisposeObservable.addOnce(() => { - this.managers?.control.onActionObservable.remove(this.actionObserver) - }) - } - - /** - * Initializes with game rules. - * Connects to the control manager to evaluate rules. - * @param {object} params - parameters, including: - * @param {import('@src/3d/managers').Managers} params.managers - current managers. - */ - init({ managers }) { - this.managers = managers - this.actionObserver = managers.control.onActionObservable.add(action => - evaluate(this, action) - ) - logger.debug('rules manager initialized') - } -} - -async function evaluate( - /** @type {ScoreManager} */ { engine }, - /** @type {import('@src/3d/managers').ActionOrMove} */ action -) { - if (!('fn' in action)) { - return - } - const state = engine.serialize() - // 6-takes - if ( - (action.fn === snap || action.fn === unsnap) && - action.meshId === 'board' - ) { - const { snappedId } = findAnchor('score-player-1', state.meshes) - const snapped = findMesh(snappedId, state.meshes, false) - console.log('player 1', snapped?.id) - } -} - -function findAnchor(path, meshes, throwOnMiss = true) { - let candidates = [...(meshes ?? [])] - let anchor - for (let leg of path.split('.')) { - const match = findMeshAndAnchor(leg, candidates, throwOnMiss) - if (!match) { - return null - } - candidates = meshes.filter(({ id }) => id === match.anchor.snappedId) - anchor = match.anchor - } - return anchor ?? null -} - -function findMeshAndAnchor(anchorId, meshes, throwOnMiss = true) { - for (const mesh of meshes) { - for (const anchor of mesh.anchorable?.anchors ?? []) { - if (anchor.id === anchorId) { - return { mesh, anchor } - } - } - } - if (throwOnMiss) { - throw new Error(`No anchor with id ${anchorId}`) - } - return null -} - -function findMesh(id, meshes, throwOnMiss = true) { - const mesh = meshes?.find(mesh => mesh.id === id) ?? null - if (throwOnMiss && !mesh) { - throw new Error(`No mesh with id ${id}`) - } - return mesh -} diff --git a/apps/web/src/3d/managers/selection.js b/apps/web/src/3d/managers/selection.js index 66abb176..04676317 100644 --- a/apps/web/src/3d/managers/selection.js +++ b/apps/web/src/3d/managers/selection.js @@ -328,8 +328,8 @@ function findSnapped(mesh) { const scene = mesh.getScene() /** @type {Mesh[]} */ const anchored = [] - for (const { snappedId } of mesh.metadata.anchors ?? []) { - if (snappedId) { + for (const { snappedIds } of mesh.metadata.anchors ?? []) { + for (const snappedId of snappedIds) { const mesh = scene.getMeshById(snappedId) if (mesh) { anchored.push(mesh, ...findSnapped(mesh)) diff --git a/apps/web/src/3d/managers/target.js b/apps/web/src/3d/managers/target.js index b9a3e107..4e7c4733 100644 --- a/apps/web/src/3d/managers/target.js +++ b/apps/web/src/3d/managers/target.js @@ -9,6 +9,7 @@ import { getTargetableBehavior } from '../utils/behaviors' import { isAbove } from '../utils/gravity' +import { isContaining } from '../utils/mesh' const logger = makeLogger('target') @@ -25,8 +26,8 @@ const logger = makeLogger('target') * @property {import('../behaviors').TargetBehavior} targetable - the enclosing targetable behavior. * @property {import('@babylonjs/core').Mesh} mesh - invisible, unpickable mesh acting as drop zone. * - * @typedef {Record & Omit & - * Required> & + * @typedef {Record & Omit & + * Required> & * _SingleDropZone} SingleDropZone definition of a target drop zone */ @@ -343,6 +344,9 @@ function sortCandidates(candidates) { * @returns whether this zone is close to the mesh or one of its part. */ function isAPartCenterClose(mesh, partCenters, zone) { + if (zone.max > 1) { + return isContaining(zone.mesh, mesh) + } if (zone.ignoreParts) { return isCloseTo(mesh.absolutePosition, zone) } diff --git a/apps/web/src/3d/utils/behaviors.js b/apps/web/src/3d/utils/behaviors.js index bab00181..dd3f4d34 100644 --- a/apps/web/src/3d/utils/behaviors.js +++ b/apps/web/src/3d/utils/behaviors.js @@ -398,7 +398,7 @@ export function detachFromParent(mesh, detachChildren = true) { } /** - * Computes the final position of a given above a drop zone + * Computes the final position of a given mesh above a drop zone, aligning their centers. * @param {import('@babylonjs/core').Mesh} droppedMesh - mesh dropped above zone. * @param {import('../managers').DropZone} zone - drop zone. * @returns absolute position for this mesh. diff --git a/apps/web/src/3d/utils/gravity.js b/apps/web/src/3d/utils/gravity.js index 50924cee..12391abd 100644 --- a/apps/web/src/3d/utils/gravity.js +++ b/apps/web/src/3d/utils/gravity.js @@ -60,6 +60,18 @@ export function getCenterAltitudeAbove(meshBelow, meshAbove) { * @returns the mesh's new absolute position */ export function applyGravity(mesh) { + mesh.setAbsolutePosition(getAltitudeAfterGravity(mesh)) + return mesh.absolutePosition +} + +/** + * Compute a mesh's Y coordinate so it lies on mesh below, or on the ground. + * It'll check all other meshes in the same scene to identify the ones below (partial overlap is supported). + * Does not alter any mesh + * @param {import('@babylonjs/core').Mesh} mesh - tested mesh. + * @returns new vector. + */ +export function getAltitudeAfterGravity(mesh) { logger.info( { y: mesh.absolutePosition.y, mesh }, `gravity for ${mesh.id} y: ${mesh.absolutePosition.y}` @@ -78,10 +90,8 @@ export function applyGravity(mesh) { `${mesh.id} is above ${ordered.map(({ id }) => id)}` ) } - logger.info({ y, mesh }, `${mesh.id} assigned to y: ${y}`) const { x, z } = mesh.absolutePosition - mesh.setAbsolutePosition(new Vector3(x, y, z)) - return mesh.absolutePosition + return new Vector3(x, y, z) } /** diff --git a/apps/web/src/3d/utils/mesh.js b/apps/web/src/3d/utils/mesh.js index a1cf9032..ef8f94d0 100644 --- a/apps/web/src/3d/utils/mesh.js +++ b/apps/web/src/3d/utils/mesh.js @@ -38,6 +38,7 @@ export function isAnimationInProgress(mesh) { /** * Indicates whether a given container completely contain the tested mesh, using their bounding boxes. + * Does not consider Y axis, only X and Z. * @template {import('@babylonjs/core').AbstractMesh} M * @param {M} container - container that may contain the mesh. * @param {M} mesh - tested mesh. @@ -53,8 +54,6 @@ export function isContaining(container, mesh) { return ( containerMin.x <= meshMin.x && meshMax.x <= containerMax.x && - containerMin.y <= meshMin.y && - meshMax.y <= containerMax.y && containerMin.z <= meshMin.z && meshMax.z <= containerMax.z ) diff --git a/apps/web/src/3d/utils/scene-loader.js b/apps/web/src/3d/utils/scene-loader.js index c824f8de..992c7e76 100644 --- a/apps/web/src/3d/utils/scene-loader.js +++ b/apps/web/src/3d/utils/scene-loader.js @@ -139,7 +139,9 @@ export async function loadMeshes(scene, meshes, managers) { } const anchorBehavior = mesh.getBehaviorByName(AnchorBehaviorName) if (anchorable && anchorBehavior) { - if ((anchorable.anchors ?? []).find(({ snappedId }) => snappedId)) { + if ( + (anchorable.anchors ?? []).find(({ snappedIds }) => snappedIds.length) + ) { // stores for later anchorables.push({ anchorBehavior, diff --git a/apps/web/src/graphql/games.graphql b/apps/web/src/graphql/games.graphql index d1af23cc..9532a74d 100644 --- a/apps/web/src/graphql/games.graphql +++ b/apps/web/src/graphql/games.graphql @@ -71,14 +71,16 @@ fragment mesh on Mesh { height depth diameter + enabled extent kinds priority - snappedId + snappedIds playerId ignoreParts angle flip + max } duration } @@ -108,6 +110,7 @@ fragment mesh on Mesh { fragment fullGame on Game { id kind + engineScript players { ...lightPlayer } @@ -135,11 +138,7 @@ fragment fullGame on Game { ...mesh } } - preferences { - playerId - color - angle - } + preferencesString rulesBookPageCount zoomSpec { min @@ -207,11 +206,7 @@ fragment gameData on GameOrParameters { players { ...lightPlayer } - preferences { - playerId - color - angle - } + preferencesString rulesBookPageCount availableSeats colors { diff --git a/apps/web/src/routes/[[lang=lang]]/(auth)/game/[gameId]/GameMenu.svelte b/apps/web/src/routes/[[lang=lang]]/(auth)/game/[gameId]/GameMenu.svelte index 3928bd40..cd8faefd 100644 --- a/apps/web/src/routes/[[lang=lang]]/(auth)/game/[gameId]/GameMenu.svelte +++ b/apps/web/src/routes/[[lang=lang]]/(auth)/game/[gameId]/GameMenu.svelte @@ -105,7 +105,7 @@ } &::after { - @apply absolute inset-y-0 w-12 -z-1 bg-$base-darker; + @apply absolute inset-y-0 w-16 -z-1 bg-$base-darker; left: calc(100% - var(--corner-overlap) - 1px); content: ''; clip-path: var(--corner); diff --git a/apps/web/src/routes/[[lang=lang]]/(auth)/game/[gameId]/LoadingScreen/Screen.svelte b/apps/web/src/routes/[[lang=lang]]/(auth)/game/[gameId]/LoadingScreen/Screen.svelte index 1e97a89c..ec682404 100644 --- a/apps/web/src/routes/[[lang=lang]]/(auth)/game/[gameId]/LoadingScreen/Screen.svelte +++ b/apps/web/src/routes/[[lang=lang]]/(auth)/game/[gameId]/LoadingScreen/Screen.svelte @@ -54,8 +54,7 @@ diff --git a/apps/web/src/stores/game-engine.js b/apps/web/src/stores/game-engine.js index 98defd28..7fd7e4ad 100644 --- a/apps/web/src/stores/game-engine.js +++ b/apps/web/src/stores/game-engine.js @@ -24,9 +24,9 @@ const engine$ = new BehaviorSubject( /** @type {?import('@babylonjs/core').Engine} */ (null) ) const fps$ = new BehaviorSubject('0') -/** @type {Subject} */ +/** @type {Subject} */ const localAction$ = new Subject() -/** @type {Subject} */ +/** @type {Subject} */ const remoteAction$ = new Subject() /** @type {Subject} */ const remoteSelection$ = new Subject() @@ -55,6 +55,9 @@ const history$ = new BehaviorSubject( /** @type {import('@tabulous/types').HistoryRecord[]} */ ([]) ) const replayRank$ = new BehaviorSubject(0) +const scores$ = new BehaviorSubject( + /** @type {?import('@tabulous/types').Scores} */ (null) +) /** * Emits 3D engine when available. @@ -155,6 +158,11 @@ export const history = history$.asObservable() */ export const replayRank = replayRank$.asObservable() +/** + * Stores and emits player scores. + */ +export const scores = scores$.asObservable() + /** * @typedef {object} EngineParams * @property {number} pointerThrottle - number of milliseconds during which pointer will be ignored before being shared with peers. @@ -190,6 +198,8 @@ export function initEngine({ pointerThrottle, longTapDelay, ...engineProps }) { cameraSaves$.next(engine.managers.camera.saves) currentCamera$.next(engine.managers.camera.saves[0]) + scores$.next(null) + /** @type {import('@src/types').BabylonToRxMapping[]} */ const mappings = [ { @@ -256,6 +266,11 @@ export function initEngine({ pointerThrottle, longTapDelay, ...engineProps }) { observable: engine.managers.replay.onReplayRankObservable, subject: replayRank$, observer: null + }, + { + observable: engine.managers.rule.onScoreUpdateObservable, + subject: scores$, + observer: null } ] // exposes Babylon observables as RX subjects diff --git a/apps/web/src/stores/game-manager.js b/apps/web/src/stores/game-manager.js index 30c83d2e..51c0b5a9 100644 --- a/apps/web/src/stores/game-manager.js +++ b/apps/web/src/stores/game-manager.js @@ -25,11 +25,11 @@ import { toastInfo } from '@src/stores/toaster' import { buildPlayerColors, findPlayerColor, - findPlayerPreferences, isLobby, makeLogger, sleep } from '@src/utils' +import { findPlayerPreferences } from '@tabulous/game-utils' import { BehaviorSubject, combineLatest, @@ -125,7 +125,7 @@ export const gamePlayerById = merge(hostId$, playingIds$, currentGame$).pipe( for (const player of game.players ?? []) { playerById.set(player.id, { ...player, - ...findPlayerPreferences(game, player.id), + ...findPlayerPreferences(game.preferences, player.id), playing: playingIds$.value.includes(player.id), isHost: isCurrentHost(player.id) }) @@ -426,7 +426,7 @@ async function load( /** @type {import('@src/graphql').Game} */ (game), { playerId, - preferences: findPlayerPreferences(game, playerId), + preference: findPlayerPreferences(game.preferences, playerId), colorByPlayerId: buildPlayerColors(game) }, firstLoad @@ -775,5 +775,5 @@ function isCurrentHost(/** @type {string} */ playerId) { function isGameParameter( /** @type {import('@src/graphql').GameOrGameParameters} */ game ) { - return 'schemaString' in game && Boolean(game.schemaString) + return 'schema' in game && Boolean(game.schema) } diff --git a/apps/web/src/stores/graphql-client.js b/apps/web/src/stores/graphql-client.js index f7820398..da9cf0b5 100644 --- a/apps/web/src/stores/graphql-client.js +++ b/apps/web/src/stores/graphql-client.js @@ -102,7 +102,7 @@ export async function runMutation(query, variables) { if (!data || keys.length !== 1) { throw new Error('graphQL mutation returned no results') } - return data[keys[0]] + return deserialize(data[keys[0]]) } /** @@ -134,7 +134,7 @@ export async function runQuery(query, variables, cache = true) { if (!data || keys.length !== 1) { throw new Error('graphQL mutation returned no results') } - return data[keys[0]] + return deserialize(data[keys[0]]) } /** @@ -164,7 +164,7 @@ export function runSubscription(subscription, variables) { logger.error({ error }, `Error received on subscription`) } const keys = Object.keys(data || {}) - return keys.length !== 1 ? data : data[keys[0]] + return keys.length !== 1 ? data : deserialize(data[keys[0]]) }) ) } @@ -185,3 +185,25 @@ function processErrors( throw new Error(combinedError.graphQLErrors[0].message) } } + +/** + * Parses schemaString into schema and preferencesString into preferences. + * @template T + * @param {T} value - serialized value. + * @returns deserialized value. + */ +function deserialize(value) { + if (value) { + const obj = + /** @type {Record & {schema?: ?, preferences?: ?}} */ (value) + if ('schemaString' in obj) { + obj.schema = JSON.parse(obj.schemaString) + delete obj.schemaString + } + if ('preferencesString' in obj) { + obj.preferences = JSON.parse(obj.preferencesString) + delete obj.preferencesString + } + } + return value +} diff --git a/apps/web/src/types/@babylonjs.d.ts b/apps/web/src/types/@babylonjs.d.ts index bd01994b..748bba9d 100644 --- a/apps/web/src/types/@babylonjs.d.ts +++ b/apps/web/src/types/@babylonjs.d.ts @@ -19,6 +19,7 @@ import type { Behavior } from '@src/3d/utils' import type { Game } from '@src/graphql' import type { GameWithSelections } from '@src/stores' import type { MeshMetadata } from '@src/types' +import type { EngineState } from '@tabulous/types' import type { ActionName, ButtonName, @@ -30,7 +31,7 @@ interface LoadPlayerData { /** current player id (to determine their hand)). */ playerId: string /** current player's preferences. */ - preferences: Omit + preference: Omit /** map of hexadecimal color string for each player Id. */ colorByPlayerId: Map } @@ -70,11 +71,7 @@ declare module '@babylonjs/core' { /** * serializes all meshes rendered in the game engines. */ - serialize(): { - meshes: SerializedMesh[] - handMeshes: SerializedMesh[] - history: HistoryRecord[] - } + serialize(): EngineState /** * Applies a remote mesh selection from a peer player, unless replaying history. * @param selectedIds - selected mesh ids. diff --git a/apps/web/src/types/graphql.d.ts b/apps/web/src/types/graphql.d.ts index 5dcb2cd3..19999578 100644 --- a/apps/web/src/types/graphql.d.ts +++ b/apps/web/src/types/graphql.d.ts @@ -25,6 +25,7 @@ import type { TargetedPlayerArgs, UpdateCurrentPlayerArgs } from '@tabulous/server/graphql' +import type { PlayerPreference, Schema } from '@tabulous/types' import type { TypedDocumentNode } from '@urql/core' declare module '@src/graphql' { @@ -41,21 +42,26 @@ declare module '@src/graphql' { GamePlayer, 'id' | 'username' | 'avatar' | 'currentGameId' | 'isGuest' | 'isOwner' > - type Game = Omit & { players?: LightPlayer[] } + type Game = Omit & { + preferences?: PlayerPreference[] + players?: LightPlayer[] + } type LightGame = Pick type GameOrGameParameters = | Game | (Pick< GameParameters, - | 'schemaString' | 'error' | 'id' | 'kind' - | 'preferences' | 'rulesBookPageCount' | 'availableSeats' | 'colors' - > & { players?: LightPlayer[] }) + > & { + preferences?: PlayerPreference[] + schema?: Schema + players?: LightPlayer[] + }) const createGame: TypedDocumentNode<{ createGame: LightGame }, CreateGameArgs> const deleteGame: TypedDocumentNode< { deleteGame: Pick | null }, diff --git a/apps/web/src/types/index.d.ts b/apps/web/src/types/index.d.ts index 26c33d93..31b271ff 100644 --- a/apps/web/src/types/index.d.ts +++ b/apps/web/src/types/index.d.ts @@ -1,25 +1,27 @@ /* eslint-disable no-unused-vars */ import type { Observable, Observer } from '@babylonjs/core' import type { - AnchorableState, AnchorBehavior, - DetailableState, DetailBehavior, DrawBehavior, FlipBehavior, - FlippableState, - LockableState, LockBehavior, - MovableState, - QuantifiableState, QuantityBehavior, RandomBehavior, - RandomizableState, - RotableState, RotateBehavior, StackBehavior } from '@src/3d/behaviors' -import type { Mesh as SerializedMesh } from '@tabulous/types' +import type { + AnchorableState, + DetailableState, + FlippableState, + LockableState, + Mesh as SerializedMesh, + MovableState, + QuantifiableState, + RandomizableState, + RotableState +} from '@tabulous/types' import type { Observable as RxObservable, Subject } from 'rxjs' import type { ComponentType } from 'svelte' diff --git a/apps/web/src/utils/game-interaction.js b/apps/web/src/utils/game-interaction.js index 10f9e139..ec72c8cd 100644 --- a/apps/web/src/utils/game-interaction.js +++ b/apps/web/src/utils/game-interaction.js @@ -116,7 +116,7 @@ export function attachInputs({ engine, hoverDelay, actionMenuProps$ }) { const hovers$ = new Subject() /** @type {Subject} */ const details$ = new Subject() - /** @type {Subject} */ + /** @type {Subject} */ const behaviorAction$ = new Subject() /** @type {Subject} */ const replayRank$ = new Subject() diff --git a/apps/web/src/utils/game.js b/apps/web/src/utils/game.js index 38147611..a5435c56 100644 --- a/apps/web/src/utils/game.js +++ b/apps/web/src/utils/game.js @@ -1,31 +1,17 @@ // @ts-check +import { findPlayerPreferences } from '@tabulous/game-utils' import chroma from 'chroma-js' import { setCssVariables } from './dom' -/** - * Find game preferences of a given player. - * @param {?import('@src/graphql').GameOrGameParameters|undefined} game - game date, including preferences and players arrays. - * @param {string} playerId - desired player - * @returns {Record & Omit} found preferences, or an empty object. - */ -export function findPlayerPreferences(game, playerId) { - // playerId is unused, and simply ommitted from returned preferences. - // eslint-disable-next-line no-unused-vars - const { playerId: _unused, ...preferences } = game?.preferences?.find( - preferences => preferences.playerId === playerId - ) ?? { color: undefined, angle: undefined } - return preferences -} - /** * Returns player's color, or orange red. * @param {?import('@src/graphql').GameOrGameParameters} game - game date, including preferences and players arrays. * @param {string} playerId - desired player - * @returns {string} player's color. + * @returns player's color. */ export function findPlayerColor(game, playerId) { - return findPlayerPreferences(game, playerId)?.color ?? '#ff4500' + return findPlayerPreferences(game?.preferences, playerId)?.color ?? '#ff4500' } /** diff --git a/apps/web/src/utils/logger.js b/apps/web/src/utils/logger.js index fc0bdb46..9d51c0ac 100644 --- a/apps/web/src/utils/logger.js +++ b/apps/web/src/utils/logger.js @@ -38,6 +38,7 @@ const levels = { randomizable: 'warn', replay: 'warn', rotable: 'warn', + rule: 'warn', selection: 'warn', 'scene-loader': 'warn', stackable: 'warn', diff --git a/apps/web/tests/3d/behaviors/anchorable.test.js b/apps/web/tests/3d/behaviors/anchorable.test.js index be4e4dcd..816fbe84 100644 --- a/apps/web/tests/3d/behaviors/anchorable.test.js +++ b/apps/web/tests/3d/behaviors/anchorable.test.js @@ -143,7 +143,7 @@ describe('AnchorBehavior', () => { beforeEach(() => { mesh = createBox('box', { width: 5, depth: 5 }) mesh.addBehavior(new AnimateBehavior(), true) - meshes = Array.from({ length: 2 }, (_, rank) => { + meshes = Array.from({ length: 3 }, (_, rank) => { const box = createBox(`box${rank + 1}`, {}) box.addBehavior(new AnimateBehavior(), true) box.addBehavior(new DrawBehavior({}, managers), true) @@ -162,9 +162,10 @@ describe('AnchorBehavior', () => { z: 0.5, width: 1, height: 1.5, - depth: 0.5 + depth: 0.5, + snappedIds: [] }, - { id: '2', width: 1.5, height: 1, depth: 0.25 } + { id: '2', width: 1.5, height: 1, depth: 0.25, snappedIds: [] } ] }, managers @@ -183,9 +184,10 @@ describe('AnchorBehavior', () => { z: 0.5, width: 1, height: 1.5, - depth: 0.5 + depth: 0.5, + snappedIds: [] }, - { id: '2', width: 1.5, height: 1, depth: 0.25 } + { id: '2', width: 1.5, height: 1, depth: 0.25, snappedIds: [] } ]) expect(mesh.metadata).toEqual( expect.objectContaining({ @@ -209,29 +211,67 @@ describe('AnchorBehavior', () => { behavior.fromState({ duration, anchors: [ - { id: '4', x: 1, y: 1, z: 0, width: 1, height: 2, depth: 0.5 }, - { id: '5', x: -1, y: 1, z: 0, width: 2, height: 1, depth: 0.25 }, + { + id: '4', + x: 1, + y: 1, + z: 0, + width: 1, + height: 2, + depth: 0.5, + snappedIds: [] + }, + { + id: '5', + x: -1, + y: 1, + z: 0, + width: 2, + height: 1, + depth: 0.25, + snappedIds: [] + }, { id: '6', width: 1, height: 1, depth: 1, kinds: ['cards'], - priority: 1 + priority: 1, + snappedIds: [] } ] }) expect(behavior.state.duration).toEqual(duration) expect(behavior.state.anchors).toEqual([ - { id: '4', x: 1, y: 1, z: 0, width: 1, height: 2, depth: 0.5 }, - { id: '5', x: -1, y: 1, z: 0, width: 2, height: 1, depth: 0.25 }, + { + id: '4', + x: 1, + y: 1, + z: 0, + width: 1, + height: 2, + depth: 0.5, + snappedIds: [] + }, + { + id: '5', + x: -1, + y: 1, + z: 0, + width: 2, + height: 1, + depth: 0.25, + snappedIds: [] + }, { id: '6', width: 1, height: 1, depth: 1, kinds: ['cards'], - priority: 1 + priority: 1, + snappedIds: [] } ]) expect(mesh.metadata).toEqual( @@ -253,7 +293,16 @@ describe('AnchorBehavior', () => { it('can hydrate cylindric anchors', () => { behavior.fromState({ anchors: [ - { id: '1', x: 1, y: 1, z: 0, diameter: 4, height: 0.5, extent: 1.2 }, + { + id: '1', + x: 1, + y: 1, + z: 0, + diameter: 4, + height: 0.5, + extent: 1.2, + snappedIds: [] + }, { id: '2', x: 1, @@ -261,12 +310,22 @@ describe('AnchorBehavior', () => { z: 0, diameter: 2, height: 0.1, - extent: undefined + extent: undefined, + snappedIds: [] } ] }) expect(behavior.state.anchors).toEqual([ - { id: '1', x: 1, y: 1, z: 0, diameter: 4, height: 0.5, extent: 1.2 }, + { + id: '1', + x: 1, + y: 1, + z: 0, + diameter: 4, + height: 0.5, + extent: 1.2, + snappedIds: [] + }, { id: '2', x: 1, @@ -274,7 +333,8 @@ describe('AnchorBehavior', () => { z: 0, diameter: 2, height: 0.1, - extent: undefined + extent: undefined, + snappedIds: [] } ]) expectAnchor(0, behavior.state.anchors[0]) @@ -301,10 +361,30 @@ describe('AnchorBehavior', () => { ) mesh.addBehavior(behavior, true) behavior.fromState({ - anchors: [{ id: '1', x: 0, y: -2, z: 0, width: 1, height: 2, depth: 3 }] + anchors: [ + { + id: '1', + x: 0, + y: -2, + z: 0, + width: 1, + height: 2, + depth: 3, + snappedIds: [] + } + ] }) expect(behavior.state.anchors).toEqual([ - { id: '1', x: 0, y: -2, z: 0, width: 1, height: 2, depth: 3 } + { + id: '1', + x: 0, + y: -2, + z: 0, + width: 1, + height: 2, + depth: 3, + snappedIds: [] + } ]) expectAnchor(0, { ...behavior.state.anchors[0], x: 4, y: 3, z: -4 }) expect(actionRecorded).not.toHaveBeenCalled() @@ -318,11 +398,11 @@ describe('AnchorBehavior', () => { const previousAnchors = behavior.zones.map(({ mesh }) => mesh) behavior.fromState({ - anchors: [{ id: '1', width: 1, height: 2, depth: 0.5 }] + anchors: [{ id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [] }] }) expect(behavior.state.duration).toEqual(100) expect(behavior.state.anchors).toEqual([ - { id: '1', width: 1, height: 2, depth: 0.5 } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [] } ]) expect(mesh.metadata).toEqual( expect.objectContaining({ @@ -341,12 +421,12 @@ describe('AnchorBehavior', () => { it('discards unknown snapped meshes when hydrating', () => { behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: 'unknown' } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: ['unknown'] } ] }) expect(behavior.state.duration).toEqual(100) expect(behavior.state.anchors).toEqual([ - { id: '1', width: 1, height: 2, depth: 0.5 } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [] } ]) expect(mesh.metadata).toEqual( expect.objectContaining({ @@ -365,12 +445,36 @@ describe('AnchorBehavior', () => { it('snaps meshes when hydrating', () => { behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: meshes[0].id } + { + id: '1', + width: 1, + height: 2, + depth: 0.5, + snappedIds: [meshes[0].id] + }, + { + id: '2', + x: -2, + width: 1, + height: 2, + depth: 0.5, + max: 2, + snappedIds: [meshes[1].id, meshes[2].id] + } ] }) expect(behavior.state.duration).toEqual(100) expect(behavior.state.anchors).toEqual([ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: 'box1' } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: ['box1'] }, + { + id: '2', + x: -2, + width: 1, + height: 2, + depth: 0.5, + snappedIds: ['box2', 'box3'], + max: 2 + } ]) expect(mesh.metadata).toEqual( expect.objectContaining({ @@ -381,8 +485,13 @@ describe('AnchorBehavior', () => { ) expect(behavior.zones).toHaveLength(behavior.state.anchors.length) expectAnchor(0, behavior.state.anchors[0], false) - expectSnapped(mesh, meshes[0], 0) - expect(behavior.getSnappedIds()).toEqual([meshes[0].id]) + expectSnapped(mesh, meshes.slice(0, 1), 0) + expectSnapped(mesh, meshes.slice(1, 3), 1) + expect(behavior.getSnappedIds()).toEqual([ + meshes[0].id, + meshes[1].id, + meshes[2].id + ]) expect(actionRecorded).not.toHaveBeenCalled() expectMoveRecorded(moveRecorded) expect(registerFeedbackSpy).not.toHaveBeenCalled() @@ -397,12 +506,12 @@ describe('AnchorBehavior', () => { expectFlipped(mesh) behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: snapped.id } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [snapped.id] } ] }) expect(behavior.state.duration).toEqual(100) expect(behavior.state.anchors).toEqual([ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: 'box1' } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: ['box1'] } ]) expect(mesh.metadata).toEqual( expect.objectContaining({ @@ -414,7 +523,7 @@ describe('AnchorBehavior', () => { expect(behavior.zones).toHaveLength(behavior.state.anchors.length) expectAnchor(0, behavior.state.anchors[0], false) expectFlipped(mesh) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expectFlipped(snapped) expect(behavior.getSnappedIds()).toEqual([snapped.id]) expect(actionRecorded).not.toHaveBeenCalled() @@ -431,12 +540,12 @@ describe('AnchorBehavior', () => { expectRotated(mesh, angle) behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: snapped.id } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [snapped.id] } ] }) expect(behavior.state.duration).toEqual(100) expect(behavior.state.anchors).toEqual([ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: 'box1' } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: ['box1'] } ]) expect(mesh.metadata).toEqual( expect.objectContaining({ @@ -448,7 +557,7 @@ describe('AnchorBehavior', () => { expect(behavior.zones).toHaveLength(behavior.state.anchors.length) expectAnchor(0, behavior.state.anchors[0], false) expectRotated(mesh, angle) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expectRotated(snapped, angle) expect(behavior.getSnappedIds()).toEqual([snapped.id]) expect(actionRecorded).not.toHaveBeenCalled() @@ -471,23 +580,69 @@ describe('AnchorBehavior', () => { width: 1, height: 2, depth: 0.5, - snappedId: snapped.id, + snappedIds: [snapped.id], angle } ] }) expect(behavior.state.duration).toEqual(100) expect(behavior.state.anchors).toEqual([ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: 'box1', angle } + { + id: '1', + width: 1, + height: 2, + depth: 0.5, + snappedIds: ['box1'], + angle + } ]) expectAnchor(0, behavior.state.anchors[0], false) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expectRotated(snapped, angle) expect(behavior.getSnappedIds()).toEqual([snapped.id]) expect(actionRecorded).not.toHaveBeenCalled() expect(registerFeedbackSpy).not.toHaveBeenCalled() }) + it('can flip snapped mesh when hydrating', () => { + const snapped = meshes[0] + snapped.addBehavior( + new FlipBehavior({ isFlipped: false }, managers), + true + ) + expectFlipped(snapped, false) + + behavior.fromState({ + anchors: [ + { + id: '1', + width: 1, + height: 2, + depth: 0.5, + snappedIds: [snapped.id], + flip: true + } + ] + }) + expect(behavior.state.duration).toEqual(100) + expect(behavior.state.anchors).toEqual([ + { + id: '1', + width: 1, + height: 2, + depth: 0.5, + snappedIds: ['box1'], + flip: true + } + ]) + expectAnchor(0, behavior.state.anchors[0], false) + expectSnapped(mesh, [snapped], 0) + expectFlipped(snapped) + expect(behavior.getSnappedIds()).toEqual([snapped.id]) + expect(actionRecorded).not.toHaveBeenCalled() + expect(registerFeedbackSpy).not.toHaveBeenCalled() + }) + it('can reset snapped mesh rotation when hydrating', () => { const snapped = meshes[0] snapped.addBehavior( @@ -504,17 +659,24 @@ describe('AnchorBehavior', () => { width: 1, height: 2, depth: 0.5, - snappedId: snapped.id, + snappedIds: [snapped.id], angle } ] }) expect(behavior.state.duration).toEqual(100) expect(behavior.state.anchors).toEqual([ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: 'box1', angle } + { + id: '1', + width: 1, + height: 2, + depth: 0.5, + snappedIds: ['box1'], + angle + } ]) expectAnchor(0, behavior.state.anchors[0], false) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expectRotated(snapped, angle) expect(behavior.getSnappedIds()).toEqual([snapped.id]) expect(actionRecorded).not.toHaveBeenCalled() @@ -529,7 +691,7 @@ describe('AnchorBehavior', () => { /** @type {[string, string, boolean]} */ const args = [snapped.id, behavior.zones[0].mesh.id, false] await mesh.metadata.snap?.(...args) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expect(behavior.getSnappedIds()).toEqual([meshes[0].id]) expect(actionRecorded).toHaveBeenCalledTimes(1) expect(actionRecorded).toHaveBeenCalledWith({ @@ -545,6 +707,56 @@ describe('AnchorBehavior', () => { expectMeshFeedback(registerFeedbackSpy, 'snap', behavior.zones[0].mesh) }) + it('snaps multiple meshes', async () => { + behavior.state.anchors[0].max = 2 + const [snapped1, snapped2] = meshes + expect(snapped1.absolutePosition.asArray()).toEqual([10, 10, 10]) + expect(behavior.snappedZone(snapped1.id)).toBeNull() + expect(snapped2.absolutePosition.asArray()).toEqual([11, 11, 11]) + expect(behavior.snappedZone(snapped2.id)).toBeNull() + + /** @type {[string, string, boolean]} */ + let args = [snapped1.id, behavior.zones[0].mesh.id, false] + await mesh.metadata.snap?.(...args) + expectZoneEnabled(mesh, 0) + expect(behavior?.snappedZone(snapped1.id)?.mesh.id).toEqual( + behavior?.zones[0]?.mesh.id + ) + expect(actionRecorded).toHaveBeenCalledTimes(1) + expect(actionRecorded).toHaveBeenCalledWith({ + fn: 'snap', + meshId: mesh.id, + args, + duration: behavior.state.duration, + revert: [snapped1.id, [10, 10, 10], undefined, undefined], + fromHand: false, + isLocal: false + }) + expectMoveRecorded(moveRecorded, snapped1) + expectMeshFeedback(registerFeedbackSpy, 'snap', behavior.zones[0].mesh) + expectPosition(snapped1, [10, 0.5, 10]) + registerFeedbackSpy.mockClear() + moveRecorded.mockClear() + + args = [snapped2.id, behavior.zones[0].mesh.id, false] + await mesh.metadata.snap?.(...args) + expectSnapped(mesh, [snapped1, snapped2], 0) + + expect(actionRecorded).toHaveBeenCalledTimes(2) + expect(actionRecorded).toHaveBeenCalledWith({ + fn: 'snap', + meshId: mesh.id, + args, + duration: behavior.state.duration, + revert: [snapped2.id, [11, 11, 11], undefined, undefined], + fromHand: false, + isLocal: false + }) + expectMoveRecorded(moveRecorded, snapped2) + expectMeshFeedback(registerFeedbackSpy, 'snap', behavior.zones[0].mesh) + expectPosition(snapped2, [11, 1.5, 11]) + }) + it('snaps a stack of mesh', async () => { const snapped = meshes[0] const stacked = makeStack(managers, snapped) @@ -559,7 +771,7 @@ describe('AnchorBehavior', () => { /** @type {[string, string, boolean]} */ const args = [snapped.id, behavior.zones[0].mesh.id, false] await mesh.metadata.snap?.(...args) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expect(behavior.getSnappedIds()).toEqual([meshes[0].id]) expectPosition(stacked, [ snapped.absolutePosition.x, @@ -585,16 +797,16 @@ describe('AnchorBehavior', () => { snapped.addBehavior( new AnchorBehavior( { - anchors: [{ id: '10', z: -1, snappedId: snappedOfSnapped.id }] + anchors: [{ id: '10', z: -1, snappedIds: [snappedOfSnapped.id] }] }, managers ) ) - expectSnapped(snapped, snappedOfSnapped, 0) + expectSnapped(snapped, [snappedOfSnapped], 0) await mesh.metadata.snap?.(snapped.id, behavior.zones[0].mesh.id) - expectSnapped(mesh, snapped, 0) - expectSnapped(snapped, snappedOfSnapped, 0) + expectSnapped(mesh, [snapped], 0) + expectSnapped(snapped, [snappedOfSnapped], 0) expect(behavior.getSnappedIds()).toEqual([meshes[0].id]) expect(actionRecorded).toHaveBeenCalledTimes(1) expect(actionRecorded).toHaveBeenCalledWith({ @@ -620,7 +832,7 @@ describe('AnchorBehavior', () => { }) await sleep(behavior.state.duration * 2) - expectSnapped(mesh, snapped, 1) + expectSnapped(mesh, [snapped], 1) expect(actionRecorded).toHaveBeenCalledTimes(1) expect(actionRecorded).toHaveBeenCalledWith({ fn: 'snap', @@ -648,7 +860,7 @@ describe('AnchorBehavior', () => { /** @type {[string, string, boolean]} */ const args = [snapped.id, behavior.zones[0].mesh.id, false] await mesh.metadata.snap?.(...args) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expect(behavior.getSnappedIds()).toEqual([meshes[0].id]) expectRotated(mesh, angle) expectRotated(snapped, angle) @@ -676,7 +888,9 @@ describe('AnchorBehavior', () => { expectRotated(mesh, angle) const snapped = meshes[0] behavior.fromState({ - anchors: [{ id: '1', width: 1, height: 2, depth: 0.5, angle }] + anchors: [ + { id: '1', width: 1, height: 2, depth: 0.5, angle, snappedIds: [] } + ] }) snapped.addBehavior( new RotateBehavior({ angle: Math.PI * -0.5 }, managers), @@ -689,7 +903,7 @@ describe('AnchorBehavior', () => { /** @type {[string, string, boolean]} */ const args = [snapped.id, behavior.zones[0].mesh.id, false] await mesh.metadata.snap?.(...args) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expect(behavior.getSnappedIds()).toEqual([meshes[0].id]) expectRotated(snapped, -angle) expectFlipped(snapped) @@ -714,7 +928,16 @@ describe('AnchorBehavior', () => { it('can flip snapped mesh', async () => { const snapped = meshes[0] behavior.fromState({ - anchors: [{ id: '1', width: 1, height: 2, depth: 0.5, flip: true }] + anchors: [ + { + id: '1', + width: 1, + height: 2, + depth: 0.5, + flip: true, + snappedIds: [] + } + ] }) const flippable = new FlipBehavior({}, managers) snapped.addBehavior(flippable, true) @@ -724,7 +947,7 @@ describe('AnchorBehavior', () => { /** @type {[string, string, boolean]} */ const args = [snapped.id, behavior.zones[0].mesh.id, false] await mesh.metadata.snap?.(...args) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expect(behavior.getSnappedIds()).toEqual([meshes[0].id]) expectFlipped(snapped) expect(actionRecorded).toHaveBeenCalledTimes(2) @@ -756,7 +979,15 @@ describe('AnchorBehavior', () => { const snapped = meshes[0] behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, angle, flip: false } + { + id: '1', + width: 1, + height: 2, + depth: 0.5, + angle, + flip: false, + snappedIds: [] + } ] }) const snappedAngle = Math.PI * -0.5 @@ -770,7 +1001,7 @@ describe('AnchorBehavior', () => { expectRotated(snapped, snappedAngle) expect(snapped.absolutePosition.asArray()).toEqual(position) await mesh.metadata.snap?.(snapped.id, behavior.zones[0].mesh.id, false) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expectFlipped(snapped, false) expectRotated(snapped, -angle) const { revert } = @@ -798,8 +1029,7 @@ describe('AnchorBehavior', () => { expect(actionRecorded).toHaveBeenNthCalledWith(2, { fn: 'unsnap', meshId: mesh.id, - args: [snapped.id], - revert: [snapped.id, behavior.zones[0].mesh.id], + args: [snapped.id, behavior.zones[0].mesh.id], fromHand: false, isLocal: true }) @@ -811,15 +1041,15 @@ describe('AnchorBehavior', () => { const stacked = makeStack(managers, snapped) behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: snapped.id } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [snapped.id] } ] }) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expectStacked(managers, [snapped, stacked], true, mesh.id) await stacked.metadata.reorder?.([stacked.id, snapped.id]) expectStacked(managers, [stacked, snapped], true, mesh.id) - expectSnapped(mesh, stacked, 0) + expectSnapped(mesh, [stacked], 0) expect(registerFeedbackSpy).not.toHaveBeenCalled() }) @@ -828,15 +1058,15 @@ describe('AnchorBehavior', () => { const stacked = makeStack(managers, snapped) behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: snapped.id } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [snapped.id] } ] }) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expectStacked(managers, [snapped, stacked], true, mesh.id) await stacked.metadata.flipAll?.() expectStacked(managers, [stacked, snapped], true, mesh.id) - expectSnapped(mesh, stacked, 0) + expectSnapped(mesh, [stacked], 0) expect(registerFeedbackSpy).not.toHaveBeenCalled() }) @@ -844,13 +1074,13 @@ describe('AnchorBehavior', () => { const snapped = meshes[0] behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: snapped.id } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [snapped.id] } ] }) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) await mesh.metadata.snap?.(meshes[1].id, behavior.zones[0].mesh.id) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expect(actionRecorded).not.toHaveBeenCalled() expect(registerFeedbackSpy).not.toHaveBeenCalled() }) @@ -859,10 +1089,10 @@ describe('AnchorBehavior', () => { const snapped = meshes[0] behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: snapped.id } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [snapped.id] } ] }) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) mesh.metadata.unsnap?.(snapped.id) expectUnsnapped(mesh, snapped, 0) @@ -871,8 +1101,7 @@ describe('AnchorBehavior', () => { expect(actionRecorded).toHaveBeenCalledWith({ fn: 'unsnap', meshId: mesh.id, - args: [snapped.id], - revert: [snapped.id, '1'], + args: [snapped.id, '1'], fromHand: false, isLocal: false }) @@ -885,10 +1114,10 @@ describe('AnchorBehavior', () => { const stacked = makeStack(managers, snapped) behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: snapped.id } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [snapped.id] } ] }) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) mesh.metadata.unsnap?.(stacked.id) expect(behavior.getSnappedIds()).toEqual([]) @@ -897,8 +1126,7 @@ describe('AnchorBehavior', () => { expect(actionRecorded).toHaveBeenCalledWith({ fn: 'unsnap', meshId: mesh.id, - args: [stacked.id], - revert: [stacked.id, '1'], + args: [stacked.id, '1'], fromHand: false, isLocal: false }) @@ -909,10 +1137,10 @@ describe('AnchorBehavior', () => { const snapped = meshes[1] behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: snapped.id } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [snapped.id] } ] }) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) managers.move.notifyMove(snapped) expectUnsnapped(mesh, snapped, 0) @@ -921,8 +1149,7 @@ describe('AnchorBehavior', () => { expect(actionRecorded).toHaveBeenCalledWith({ fn: 'unsnap', meshId: mesh.id, - args: [snapped.id], - revert: [snapped.id, '1'], + args: [snapped.id, '1'], fromHand: false, isLocal: false }) @@ -939,14 +1166,14 @@ describe('AnchorBehavior', () => { height: 2, depth: 0.5, flip: true, - snappedId: snapped.id + snappedIds: [snapped.id] } ] }) const flippable = new FlipBehavior({}, managers) snapped.addBehavior(flippable, true) expectFlipped(snapped, false) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) await mesh.metadata.unsnap?.(snapped.id) expectUnsnapped(mesh, snapped, 0) @@ -956,8 +1183,7 @@ describe('AnchorBehavior', () => { expect(actionRecorded).toHaveBeenCalledWith({ fn: 'unsnap', meshId: mesh.id, - args: [snapped.id], - revert: [snapped.id, behavior.state.anchors[0].id], + args: [snapped.id, behavior.state.anchors[0].id], fromHand: false, isLocal: false }) @@ -968,10 +1194,10 @@ describe('AnchorBehavior', () => { const snapped = meshes[1] behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: snapped.id } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [snapped.id] } ] }) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) await snapped.metadata.draw?.() expectUnsnapped(mesh, snapped, 0) @@ -986,14 +1212,52 @@ describe('AnchorBehavior', () => { expect(actionRecorded).toHaveBeenNthCalledWith(2, { fn: 'unsnap', meshId: mesh.id, - args: [snapped.id], - revert: [snapped.id, behavior.state.anchors[0].id], + args: [snapped.id, behavior.state.anchors[0].id], fromHand: false, isLocal: true }) expectMeshFeedback(registerFeedbackSpy, 'unsnap', behavior.zones[0].mesh) }) + it('apply gravity when unsnapping from multiple anchor', async () => { + const [snapped1, snapped2] = meshes + behavior.fromState({ + anchors: [ + { + id: '1', + width: 1, + height: 2, + depth: 0.5, + snappedIds: [snapped1.id, snapped2.id], + max: 2 + } + ] + }) + expectPosition(snapped1, [10, 0.5, 10]) + expectPosition(snapped2, [11, 1.5, 11]) + expectSnapped(mesh, [snapped1, snapped2], 0) + + snapped1.setAbsolutePosition(new Vector3(10, 1, 10)) + managers.move.notifyMove(snapped1) + + expectPosition(snapped2, [11, 0.5, 11]) + expectZoneEnabled(mesh, 0) + expect(behavior?.snappedZone(snapped1.id)?.mesh.id).toBeUndefined() + expect(behavior?.snappedZone(snapped2.id)?.mesh.id).toEqual( + behavior?.zones[0]?.mesh.id + ) + expect(behavior.getSnappedIds()).toEqual([snapped2.id]) + expect(actionRecorded).toHaveBeenCalledTimes(1) + expect(actionRecorded).toHaveBeenCalledWith({ + fn: 'unsnap', + meshId: mesh.id, + args: [snapped1.id, '1'], + fromHand: false, + isLocal: false + }) + expectMeshFeedback(registerFeedbackSpy, 'unsnap', behavior.zones[0].mesh) + }) + it('unsnaps all', async () => { const [snapped1, snapped2] = meshes behavior.fromState({ @@ -1004,21 +1268,21 @@ describe('AnchorBehavior', () => { height: 2, depth: 0.5, x: -1, - snappedId: snapped1.id + snappedIds: [snapped1.id] }, - { id: '2', width: 1, height: 2, depth: 0.5 }, + { id: '2', width: 1, height: 2, depth: 0.5, snappedIds: [] }, { id: '3', width: 1, height: 2, depth: 0.5, x: 1, - snappedId: snapped2.id + snappedIds: [snapped2.id] } ] }) - expectSnapped(mesh, snapped1, 0) - expectSnapped(mesh, snapped2, 2) + expectSnapped(mesh, [snapped1], 0) + expectSnapped(mesh, [snapped2], 2) mesh.metadata.unsnapAll?.() @@ -1028,16 +1292,14 @@ describe('AnchorBehavior', () => { expect(actionRecorded).toHaveBeenCalledWith({ fn: 'unsnap', meshId: mesh.id, - args: [snapped1.id], - revert: [snapped1.id, '1'], + args: [snapped1.id, '1'], fromHand: false, isLocal: false }) expect(actionRecorded).toHaveBeenCalledWith({ fn: 'unsnap', meshId: mesh.id, - args: [snapped2.id], - revert: [snapped2.id, '3'], + args: [snapped2.id, '3'], fromHand: false, isLocal: false }) @@ -1058,7 +1320,7 @@ describe('AnchorBehavior', () => { mesh.addBehavior(new RotateBehavior({ angle }, managers), true) expectRotated(mesh, angle) await mesh.metadata.snap?.(snapped.id, behavior.zones[0].mesh.id) - expectSnapped(mesh, snapped) + expectSnapped(mesh, [snapped]) registerFeedbackSpy.mockClear() mesh.metadata.unsnap?.(snapped.id) @@ -1072,24 +1334,24 @@ describe('AnchorBehavior', () => { const snapped = meshes[0] behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: snapped.id } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [snapped.id] } ] }) - expectSnapped(mesh, snapped, 0) + expectSnapped(mesh, [snapped], 0) expect(behavior.getSnappedIds()).toEqual([snapped.id]) mesh.metadata.unsnap?.(snapped.id) const position = snapped.absolutePosition.asArray() expectUnsnapped(mesh, snapped, 0) expect(behavior.getSnappedIds()).toEqual([]) - const { revert } = + const { args } = /** @type {Required} */ ( actionRecorded.mock.calls[0][0] ) actionRecorded.mockClear() registerFeedbackSpy.mockClear() - await behavior.revert('unsnap', revert) - expectSnapped(mesh, snapped, 0) + await behavior.revert('unsnap', args) + expectSnapped(mesh, [snapped], 0) expect(behavior.getSnappedIds()).toEqual([snapped.id]) expect(actionRecorded).toHaveBeenCalledTimes(1) expect(actionRecorded).toHaveBeenCalledWith({ @@ -1107,21 +1369,27 @@ describe('AnchorBehavior', () => { it('moves all when dragging current mesh', async () => { behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: meshes[0].id }, + { + id: '1', + width: 1, + height: 2, + depth: 0.5, + snappedIds: [meshes[0].id] + }, { id: '2', width: 1, height: 2, depth: 0.5, - snappedId: meshes[1].id, + snappedIds: [meshes[1].id], x: 2, y: 3, z: 1 } ] }) - expectSnapped(mesh, meshes[0], 0) - expectSnapped(mesh, meshes[1], 1) + expectSnapped(mesh, meshes.slice(0, 1), 0) + expectSnapped(mesh, meshes.slice(1, 2), 1) expectPosition(meshes[0], [0, getCenterAltitudeAbove(mesh, meshes[0]), 0]) expectPosition(meshes[1], [2, getCenterAltitudeAbove(mesh, meshes[1]), 1]) const x = 5 @@ -1132,8 +1400,8 @@ describe('AnchorBehavior', () => { animateMove(mesh, new Vector3(x, y, z), null) expectPosition(mesh, [x, y, z]) - expectSnapped(mesh, meshes[0], 0) - expectSnapped(mesh, meshes[1], 1) + expectSnapped(mesh, meshes.slice(0, 1), 0) + expectSnapped(mesh, meshes.slice(1, 2), 1) expectPosition(meshes[0], [x, getCenterAltitudeAbove(mesh, meshes[0]), z]) expectPosition(meshes[1], [ x + 2, @@ -1147,28 +1415,34 @@ describe('AnchorBehavior', () => { it('moves all when dragging selection with current mesh', () => { behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5, snappedId: meshes[0].id }, + { + id: '1', + width: 1, + height: 2, + depth: 0.5, + snappedIds: [meshes[0].id] + }, { id: '2', width: 1, height: 2, depth: 0.5, - snappedId: meshes[1].id, + snappedIds: [meshes[1].id], x: 2, y: 3, z: 1 } ] }) - expectSnapped(mesh, meshes[0], 0) - expectSnapped(mesh, meshes[1], 1) + expectSnapped(mesh, meshes.slice(0, 1), 0) + expectSnapped(mesh, meshes.slice(1, 2), 1) managers.selection.select([mesh, meshes[0]]) managers.move.notifyMove(mesh) - expectSnapped(mesh, meshes[0], 0) - expectSnapped(mesh, meshes[1], 1) + expectSnapped(mesh, meshes.slice(0, 1), 0) + expectSnapped(mesh, meshes.slice(1, 2), 1) expect(actionRecorded).not.toHaveBeenCalled() expect(registerFeedbackSpy).not.toHaveBeenCalled() }) @@ -1192,7 +1466,7 @@ describe('AnchorBehavior', () => { it('does not enable anchors with snapped meshes', async () => { await mesh.metadata.snap?.(meshes[1].id, behavior.zones[1].mesh.id) - expectSnapped(mesh, meshes[1], 1) + expectSnapped(mesh, meshes.slice(1, 2), 1) behavior.disable() behavior.enable() @@ -1204,8 +1478,17 @@ describe('AnchorBehavior', () => { const [snapped1, snapped2] = meshes behavior.fromState({ anchors: [ - { id: '1', width: 1, height: 2, depth: 0.5 }, - { id: '2', width: 1, height: 2, depth: 0.5, x: 2, y: 3, z: 1 } + { id: '1', width: 1, height: 2, depth: 0.5, snappedIds: [] }, + { + id: '2', + width: 1, + height: 2, + depth: 0.5, + x: 2, + y: 3, + z: 1, + snappedIds: [] + } ] }) expectUnsnapped(mesh, snapped1, 0) @@ -1218,8 +1501,8 @@ describe('AnchorBehavior', () => { mesh.metadata.snap?.(snapped2.id, behavior.zones[1].mesh.id) ]) - expectSnapped(mesh, snapped1, 0) - expectSnapped(mesh, snapped2, 1) + expectSnapped(mesh, [snapped1], 0) + expectSnapped(mesh, [snapped2], 1) expect(behavior.getSnappedIds()).toEqual([snapped1.id, snapped2.id]) expect(actionRecorded).toHaveBeenCalledTimes(2) expectMoveRecorded(moveRecorded, snapped1, snapped2) diff --git a/apps/web/tests/3d/behaviors/flippable.test.js b/apps/web/tests/3d/behaviors/flippable.test.js index cc1dae2f..4c0bfc97 100644 --- a/apps/web/tests/3d/behaviors/flippable.test.js +++ b/apps/web/tests/3d/behaviors/flippable.test.js @@ -306,7 +306,7 @@ describe('FlipBehavior', () => { child.addBehavior( new AnchorBehavior( { - anchors: [{ id: 'child-1', snappedId: 'granChild' }] + anchors: [{ id: 'child-1', snappedIds: ['granChild'] }] }, managers ), @@ -314,7 +314,7 @@ describe('FlipBehavior', () => { ) mesh.addBehavior( new AnchorBehavior( - { anchors: [{ id: 'mesh-1', snappedId: 'child' }] }, + { anchors: [{ id: 'mesh-1', snappedIds: ['child'] }] }, managers ), true diff --git a/apps/web/tests/3d/behaviors/quantifiable.test.js b/apps/web/tests/3d/behaviors/quantifiable.test.js index 5bed411c..ad219f8c 100644 --- a/apps/web/tests/3d/behaviors/quantifiable.test.js +++ b/apps/web/tests/3d/behaviors/quantifiable.test.js @@ -328,8 +328,7 @@ describe('QuantityBehavior', () => { fn: 'decrement', meshId: mesh.id, duration: behavior.state.duration, - args: [state2.quantifiable?.quantity ?? 1, false], - revert: [reverted2.id, false], + args: [state2.quantifiable?.quantity ?? 1, false, reverted2.id], fromHand: false, isLocal: true }) @@ -337,8 +336,7 @@ describe('QuantityBehavior', () => { fn: 'decrement', meshId: mesh.id, duration: behavior.state.duration, - args: [state1.quantifiable?.quantity ?? 1, false], - revert: [reverted1.id, false], + args: [state1.quantifiable?.quantity ?? 1, false, reverted1.id], fromHand: false, isLocal: true }) @@ -376,8 +374,7 @@ describe('QuantityBehavior', () => { fn: 'decrement', meshId: mesh.id, duration: behavior.state.duration, - args: [state1.quantifiable?.quantity ?? 1, true], - revert: [other1.id, true], + args: [state1.quantifiable?.quantity ?? 1, true, other1.id], fromHand: false, isLocal: true }) @@ -456,20 +453,19 @@ describe('QuantityBehavior', () => { expect(actionRecorded).toHaveBeenNthCalledWith(1, { fn: 'decrement', meshId: mesh.id, - args: [1, false], - revert: [created1.id, false], + args: [1, false, created1.id], fromHand: false, isLocal: false }) + const id = `box-${faker.string.alphanumeric(5)}}` const created2 = /** @type {import('@babylonjs/core').Mesh} */ ( - await mesh.metadata.decrement?.() + await mesh.metadata.decrement?.(1, false, id) ) - expect(created2.id).not.toBe(mesh.id) - expect(created2.id).not.toBe(created1.id) + expect(created2.id).toBe(id) expectPosition(created2, mesh.absolutePosition.asArray()) expect(created2.metadata.serialize()).toMatchObject({ - id: expect.stringMatching(/^box0-/), + id, shape: 'box', movable: { snapDistance: 0.25, duration: 100 }, quantifiable: { ...behavior.state, quantity: 1 } @@ -480,8 +476,7 @@ describe('QuantityBehavior', () => { expect(actionRecorded).toHaveBeenNthCalledWith(2, { fn: 'decrement', meshId: mesh.id, - args: [1, false], - revert: [created2.id, false], + args: [1, false, id], fromHand: false, isLocal: false }) @@ -515,9 +510,8 @@ describe('QuantityBehavior', () => { expect(actionRecorded).toHaveBeenCalledWith({ fn: 'decrement', meshId: mesh.id, - args: [1, true], + args: [1, true, created.id], duration: behavior.state.duration, - revert: [created.id, true], fromHand: false, isLocal: false }) @@ -547,9 +541,8 @@ describe('QuantityBehavior', () => { expect(actionRecorded).toHaveBeenCalledWith({ fn: 'decrement', meshId: mesh.id, - args: [2, true], + args: [2, true, created.id], duration: behavior.state.duration, - revert: [created.id, true], fromHand: false, isLocal: false }) @@ -584,8 +577,7 @@ describe('QuantityBehavior', () => { expect(actionRecorded).toHaveBeenNthCalledWith(1, { fn: 'decrement', meshId: mesh.id, - args: [1, false], - revert: [created.id, false], + args: [1, false, created.id], fromHand: false, isLocal: false }) diff --git a/apps/web/tests/3d/behaviors/rotable.test.js b/apps/web/tests/3d/behaviors/rotable.test.js index 2edec823..285da8c9 100644 --- a/apps/web/tests/3d/behaviors/rotable.test.js +++ b/apps/web/tests/3d/behaviors/rotable.test.js @@ -377,7 +377,7 @@ describe('RotateBehavior', () => { child.addBehavior( new AnchorBehavior( { - anchors: [{ id: 'child-1', snappedId: 'granChild' }] + anchors: [{ id: 'child-1', snappedIds: ['granChild'] }] }, managers ), @@ -385,7 +385,7 @@ describe('RotateBehavior', () => { ) mesh.addBehavior( new AnchorBehavior( - { anchors: [{ id: 'mesh-1', snappedId: 'child' }] }, + { anchors: [{ id: 'mesh-1', snappedIds: ['child'] }] }, managers ), true diff --git a/apps/web/tests/3d/behaviors/stackable.test.js b/apps/web/tests/3d/behaviors/stackable.test.js index 6ebb0f33..f68c9377 100644 --- a/apps/web/tests/3d/behaviors/stackable.test.js +++ b/apps/web/tests/3d/behaviors/stackable.test.js @@ -166,8 +166,8 @@ describe('StackBehavior', () => { new AnchorBehavior( { anchors: [ - { id: `${id}-1`, x: -0.5 }, - { id: `${id}-2`, x: 0.5 } + { id: `${id}-1`, x: -0.5, snappedIds: [] }, + { id: `${id}-2`, x: 0.5, snappedIds: [] } ] }, managers @@ -979,15 +979,15 @@ describe('StackBehavior', () => { it('keeps reordered stack snapped to an anchor', async () => { behavior.fromState({ stackIds: ['box1', 'box2'] }) box3.getBehaviorByName(AnchorBehaviorName)?.fromState({ - anchors: [{ id: '1', x: -0.5, snappedId: 'box0' }] + anchors: [{ id: '1', x: -0.5, snappedIds: ['box0'] }] }) - expectSnapped(box3, mesh) + expectSnapped(box3, [mesh]) expectStacked(managers, [mesh, box1, box2], true, 'box3') await mesh.metadata.reorder?.(['box1', 'box0', 'box2']) expectStacked(managers, [box1, mesh, box2], true, 'box3') - expectSnapped(box3, box1) + expectSnapped(box3, [box1]) expect(actionRecorded).toHaveBeenCalledWith({ fn: 'reorder', @@ -1239,13 +1239,13 @@ describe('StackBehavior', () => { it('keeps flipped stack snapped to an anchor', async () => { behavior.fromState({ stackIds: ['box1', 'box2'] }) box3.getBehaviorByName(AnchorBehaviorName)?.fromState({ - anchors: [{ id: '1', x: -0.5, snappedId: 'box0' }] + anchors: [{ id: '1', x: -0.5, snappedIds: ['box0'] }] }) - expectSnapped(box3, mesh) + expectSnapped(box3, [mesh]) await mesh.metadata.flipAll?.() expectStacked(managers, [box2, box1, mesh], true, 'box3') - expectSnapped(box3, box2) + expectSnapped(box3, [box2]) expect(actionRecorded).toHaveBeenNthCalledWith(1, { fn: 'flipAll', meshId: mesh.id, diff --git a/apps/web/tests/3d/behaviors/targetable.test.js b/apps/web/tests/3d/behaviors/targetable.test.js index 3ad05d19..b54baaa1 100644 --- a/apps/web/tests/3d/behaviors/targetable.test.js +++ b/apps/web/tests/3d/behaviors/targetable.test.js @@ -40,7 +40,8 @@ describe('TargetBehavior', () => { extent: 1.2, enabled: true, priority: 0, - ignoreParts: false + ignoreParts: false, + max: 1 }) ) @@ -60,7 +61,8 @@ describe('TargetBehavior', () => { kinds: ['box', 'card'], enabled: false, priority: 10, - ignoreParts: true + ignoreParts: true, + max: 1 }) ) expect(mesh1.isDisposed()).toBe(false) @@ -109,7 +111,8 @@ describe('TargetBehavior', () => { extent: 1, priority: 0, ignoreParts: true, - targetable: behavior + targetable: behavior, + max: 1 }) expect(behavior.zones).toEqual([]) }) diff --git a/apps/web/tests/3d/engine.test.js b/apps/web/tests/3d/engine.test.js index f46ed243..3b2b7dc7 100644 --- a/apps/web/tests/3d/engine.test.js +++ b/apps/web/tests/3d/engine.test.js @@ -111,7 +111,7 @@ describe('createEngine()', () => { const applySelection = vi.spyOn(engine.managers.selection, 'apply') await engine.load( { id: '', created: Date.now(), meshes: [mesh], hands: [], selections }, - { playerId, colorByPlayerId, preferences: { playerId } }, + { playerId, colorByPlayerId, preference: { playerId } }, false ) engine.scenes[1].onDataLoadedObservable.notifyObservers(engine.scenes[1]) @@ -147,7 +147,7 @@ describe('createEngine()', () => { meshes: [{ id, texture, shape: 'card' }], hands: [] }, - { playerId, colorByPlayerId, preferences: { playerId } }, + { playerId, colorByPlayerId, preference: { playerId } }, false ) expect(engine.serialize()).toEqual({ @@ -195,7 +195,7 @@ describe('createEngine()', () => { const applySelection = vi.spyOn(engine.managers.selection, 'apply') await engine.load( { id: '', created: Date.now(), meshes: [mesh], hands: [], selections }, - { playerId, colorByPlayerId, preferences: { playerId } }, + { playerId, colorByPlayerId, preference: { playerId } }, true ) expect(engine.isLoading).toBe(true) @@ -237,7 +237,7 @@ describe('createEngine()', () => { { playerId, colorByPlayerId, - preferences: { playerId, angle: angleOnPlay } + preference: { playerId, angle: angleOnPlay } }, true ) @@ -275,7 +275,7 @@ describe('createEngine()', () => { hands: [], actions: { button1: ['rotate', 'pop'], button2: ['random'] } }, - { playerId, colorByPlayerId, preferences: { playerId } }, + { playerId, colorByPlayerId, preference: { playerId } }, true ) @@ -287,6 +287,49 @@ describe('createEngine()', () => { ) }) + it('configures rule engine on initial load', async () => { + const engineScript = `"use strict";var engine={computeScore:()=>({'${playerId}':{total:10}})}` + const players = [ + { id: playerId, username: 'Jane', currentGameId: '' }, + { id: peerId1, username: 'John', isOwner: true, currentGameId: '' }, + { id: peerId2, username: 'Jack', isGuest: true, currentGameId: '' } + ] + const preferences = [ + { playerId, color: 'red' }, + { playerId: peerId1, color: 'blue' } + ] + await engine.load( + { + id: '', + created: Date.now(), + meshes: [], + hands: [], + selections: [], + players, + preferences, + engineScript + }, + { playerId, colorByPlayerId, preference: { playerId } }, + true + ) + expect(receiveLoading).toHaveBeenCalledWith(true, expect.anything()) + if (canvas) { + expect(engine.managers.rule.ruleEngine).not.toBeNull() + expect( + engine.managers.rule.ruleEngine?.computeScore( + null, + { meshes: [], handMeshes: [], history: [] }, + [], + [] + ) + ).toEqual({ [playerId]: { total: 10 } }) + } else { + expect(engine.managers.rule.ruleEngine).toBeNull() + } + expect(engine.managers.rule.players).toEqual(players.slice(0, 2)) + expect(engine.managers.rule.preferences).toEqual(preferences) + }) + describe('given some loaded meshes', () => { const duration = 200 /** @type {import('@babylonjs/core').Engine} */ @@ -334,7 +377,7 @@ describe('createEngine()', () => { ], hands: [] }, - { playerId, colorByPlayerId, preferences: { playerId } }, + { playerId, colorByPlayerId, preference: { playerId } }, true ) engine.start() @@ -365,24 +408,39 @@ describe('createEngine()', () => { expect(getIds(game.handMeshes)).toEqual(['card2']) }) - it('updates color on subsequent loads', async () => { + it('updates colors, players and preferences on subsequent loads', async () => { const newColor = '#123456' const newPlayerId = faker.string.uuid() const updatedColorByPlayerId = new Map([ ...colorByPlayerId.entries(), [newPlayerId, newColor] ]) + const players = [ + { id: playerId, username: 'Jane', currentGameId: '' }, + { id: peerId1, username: 'John', isOwner: true, currentGameId: '' }, + { id: peerId2, username: 'Jack', isGuest: true, currentGameId: '' } + ] + const preferences = [{ playerId, color: 'red' }] await engine.load( - { id: '', created: Date.now(), ...engine.serialize(), hands: [] }, + { + id: '', + created: Date.now(), + ...engine.serialize(), + players, + preferences, + hands: [] + }, { playerId, colorByPlayerId: updatedColorByPlayerId, - preferences: { playerId } + preference: preferences[0] } ) expect( engine.managers.selection.colorByPlayerId.get(newPlayerId) ).toEqual(Color4.FromHexString(newColor)) + expect(engine.managers.rule.players).toEqual(players.slice(0, 2)) + expect(engine.managers.rule.preferences).toEqual(preferences) }) it('has action names mapped by button and shortcut', () => { @@ -404,7 +462,7 @@ describe('createEngine()', () => { }) it('applies remote action', async () => { - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const action = { fn: 'flip', meshId: 'card3', @@ -441,7 +499,7 @@ describe('createEngine()', () => { }) it('does not apply remote action', async () => { - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const action = { fn: 'flip', meshId: 'card3', @@ -463,7 +521,7 @@ describe('createEngine()', () => { if (canvas) { it('updates simulation on remote action', async () => { const state = engine.serialize() - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const action = { fn: 'flip', meshId: 'card3', @@ -515,7 +573,7 @@ describe('createEngine()', () => { it('updates simulation on local action', async () => { const state = engine.serialize() - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const action = { fn: 'flip', meshId: 'card3', @@ -615,7 +673,7 @@ describe('createEngine()', () => { ], hands: [] }, - { playerId, colorByPlayerId, preferences: { playerId } } + { playerId, colorByPlayerId, preference: { playerId } } ) expect(engine.scenes[1].getMeshById('card3')).toBeNull() expect(simulation.scenes[1].getMeshById('card3')).toBeNull() @@ -636,7 +694,7 @@ describe('createEngine()', () => { it('updates simulation on remote action', async () => { const state = engine.serialize() - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const action = { fn: 'flip', meshId: 'card3', @@ -732,7 +790,7 @@ describe('createEngine()', () => { ], hands: [] }, - { playerId, colorByPlayerId, preferences: { playerId } } + { playerId, colorByPlayerId, preference: { playerId } } ) expect(engine.scenes[1].getMeshById('card3')).toBeDefined() expect(simulation.scenes[1].getMeshById('card3')).toBeNull() diff --git a/apps/web/tests/3d/managers/control.test.js b/apps/web/tests/3d/managers/control.test.js index 6a642a78..9ab3f562 100644 --- a/apps/web/tests/3d/managers/control.test.js +++ b/apps/web/tests/3d/managers/control.test.js @@ -10,7 +10,7 @@ describe('ControlManager', () => { let scene /** @type {import('@babylonjs/core').Scene} */ let handScene - /** @type {import('@src/3d/managers').ActionOrMove[]} */ + /** @type {import('@tabulous/types').ActionOrMove[]} */ let actions /** @type {import('@babylonjs/core').Mesh} */ let mesh @@ -55,7 +55,9 @@ describe('ControlManager', () => { anchorable = createBox('box2', {}) const anchorableBehavior = new AnchorBehavior( { - anchors: [{ id: 'anchor-0', width: 1, height: 1, depth: 0.5 }] + anchors: [ + { id: 'anchor-0', width: 1, height: 1, depth: 0.5, snappedIds: [] } + ] }, managers ) diff --git a/apps/web/tests/3d/managers/hand.test.js b/apps/web/tests/3d/managers/hand.test.js index 174ac6be..2b7f801e 100644 --- a/apps/web/tests/3d/managers/hand.test.js +++ b/apps/web/tests/3d/managers/hand.test.js @@ -339,14 +339,14 @@ describe('HandManager', () => { const card2 = createMesh( { id: 'box6', - anchorable: { anchors: [{ id: 'box6-1', snappedId: 'box3' }] } + anchorable: { anchors: [{ id: 'box6-1', snappedIds: ['box3'] }] } }, scene ) const card1 = createMesh( { id: 'box5', - anchorable: { anchors: [{ id: 'box5-1', snappedId: 'box6' }] } + anchorable: { anchors: [{ id: 'box5-1', snappedIds: ['box6'] }] } }, scene ) @@ -378,8 +378,7 @@ describe('HandManager', () => { { meshId: card1.id, fn: 'unsnap', - args: [card2.id], - revert: [card2.id, 'box5-1'], + args: [card2.id, 'box5-1'], fromHand: false, isLocal: true }, @@ -1519,7 +1518,7 @@ describe('HandManager', () => { z: -10, movable: {}, anchorable: { - anchors: [{ id: 'anchor-0', playerId }], + anchors: [{ id: 'anchor-0', playerId, snappedIds: [] }], duration: anchorDuration } }, @@ -1574,7 +1573,7 @@ describe('HandManager', () => { ) expect(managers.control.isManaging(newMesh)).toBe(true) expect(managers.move.isManaging(newMesh)).toBe(true) - expectSnapped(dropZone, newMesh) + expectSnapped(dropZone, [newMesh]) expectCloseVector( extractDrawnState(), newMesh.absolutePosition.asArray() @@ -1658,7 +1657,7 @@ describe('HandManager', () => { expect(managers.move.isManaging(newMesh1)).toBe(true) expect(managers.control.isManaging(newMesh2)).toBe(true) expect(managers.move.isManaging(newMesh2)).toBe(true) - expectSnapped(dropZone, newMesh1) + expectSnapped(dropZone, [newMesh1]) expectStacked(managers, [newMesh1, newMesh2], true, dropZone.id) }) @@ -1727,7 +1726,7 @@ describe('HandManager', () => { ) expect(managers.control.isManaging(newMesh)).toBe(true) expect(managers.move.isManaging(newMesh)).toBe(true) - expectSnapped(dropZone, newMesh) + expectSnapped(dropZone, [newMesh]) expectCloseVector( extractDrawnState(), newMesh.absolutePosition.asArray() @@ -1828,7 +1827,7 @@ describe('HandManager', () => { expect(managers.move.isManaging(newMesh1)).toBe(true) expect(managers.control.isManaging(newMesh2)).toBe(true) expect(managers.move.isManaging(newMesh2)).toBe(true) - expectSnapped(dropZone, newMesh1) + expectSnapped(dropZone, [newMesh1]) expectStacked(managers, [newMesh1, newMesh2], true, dropZone.id) expectCloseVector( extractDrawnState(), diff --git a/apps/web/tests/3d/managers/input.test.js b/apps/web/tests/3d/managers/input.test.js index ad58ba0b..6afb5667 100644 --- a/apps/web/tests/3d/managers/input.test.js +++ b/apps/web/tests/3d/managers/input.test.js @@ -18,7 +18,7 @@ const pointerMove = 'pointermove' const wheel = 'wheel' const keyDown = 'keydown' -describe('managers.Input', () => { +describe('InputManager', () => { /** @type {import('@babylonjs/core').Scene} */ let scene /** @type {import('@babylonjs/core').Scene} */ diff --git a/apps/web/tests/3d/managers/move.test.js b/apps/web/tests/3d/managers/move.test.js index 25ca48ff..431b32f3 100644 --- a/apps/web/tests/3d/managers/move.test.js +++ b/apps/web/tests/3d/managers/move.test.js @@ -31,10 +31,10 @@ import { sleep } from '../../test-utils' -describe('managers.Move', () => { +describe('MoveManager', () => { const centerX = 1024 const centerY = 512 - /** @type {import('vitest').Mock<[import('@src/3d/managers').ActionOrMove], void>} */ + /** @type {import('vitest').Mock<[import('@tabulous/types').ActionOrMove], void>} */ const actionRecorded = vi.fn() /** @type {import('vitest').Mock<[import('@src/3d/managers').MoveDetails], void>} */ const moveRecorded = vi.fn() @@ -564,7 +564,7 @@ describe('managers.Move', () => { ) expectCloseVector( Vector3.FromArray( - /** @type {import('@src/3d/managers').Move} */ ( + /** @type {import('@tabulous/types').Move} */ ( actionRecorded.mock.calls[2][0] ).pos ), diff --git a/apps/web/tests/3d/managers/replay.test.js b/apps/web/tests/3d/managers/replay.test.js index 53b59d30..73d9a387 100644 --- a/apps/web/tests/3d/managers/replay.test.js +++ b/apps/web/tests/3d/managers/replay.test.js @@ -6,7 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { configures3dTestEngine } from '../../test-utils' -describe('managers.Control', () => { +describe('ReplayManager', () => { /** @type {import('@babylonjs/core').Engine} */ let engine /** @type {import('@babylonjs/core').Scene} */ @@ -93,7 +93,7 @@ describe('managers.Control', () => { }) it('records action with no revert', () => { - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const action = { meshId: mesh.id, fn: 'flip', @@ -113,7 +113,7 @@ describe('managers.Control', () => { }) it('can record action with revert', () => { - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const action = { meshId: mesh.id, fn: 'push', @@ -278,7 +278,7 @@ describe('managers.Control', () => { prev: [0, 0, 0], fromHand: false } - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const draw1 = { meshId: mesh.id, args: [], fn: 'draw', fromHand: false } const move1_2 = { meshId: mesh.id, @@ -292,7 +292,7 @@ describe('managers.Control', () => { prev: [0, 0, 0], fromHand: false } - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const draw2 = { meshId: mesh2.id, args: [], @@ -345,7 +345,7 @@ describe('managers.Control', () => { prev: [1, 0.5, 1], fromHand: false } - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const draw1 = { meshId: mesh.id, args: [], fn: 'draw', fromHand: false } const move3 = { meshId: mesh.id, diff --git a/apps/web/tests/3d/managers/rule.test.js b/apps/web/tests/3d/managers/rule.test.js new file mode 100644 index 00000000..79ed15e5 --- /dev/null +++ b/apps/web/tests/3d/managers/rule.test.js @@ -0,0 +1,163 @@ +// @ts-check +import { createBox } from '@src/3d/meshes' +import { serializeMeshes } from '@src/3d/utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { configures3dTestEngine, mockLogger } from '../../test-utils' + +describe('ReplayManager', () => { + /** @type {import('@babylonjs/core').Engine} */ + let engine + /** @type {import('@babylonjs/core').Scene} */ + let scene + /** @type {import('@babylonjs/core').Scene} */ + let handScene + /** @type {?import('@tabulous/types').Scores} */ + let scores = null + /** @type {import('@src/3d/managers').Managers} */ + let managers + /** @type {string} */ + let playerId + /** @type {import('@babylonjs/core').Mesh} */ + let mesh + + const logger = mockLogger('rule') + + configures3dTestEngine( + created => { + ;({ engine, scene, handScene, playerId, managers } = created) + engine.serialize = () => ({ + meshes: serializeMeshes(scene), + handMeshes: serializeMeshes(handScene), + history: managers.replay.history + }) + managers.rule.onScoreUpdateObservable.add(data => (scores = data)) + }, + { isSimulation: globalThis.use3dSimulation } + ) + + beforeEach(() => { + vi.clearAllMocks() + scores = null + mesh = createBox( + { id: 'box', texture: '', rotable: {}, flippable: {} }, + managers, + scene + ) + }) + + it('has initial state', () => { + expect(managers.rule.engine).toBe(engine) + expect(managers.rule.players).toEqual([]) + expect(managers.rule.preferences).toEqual([]) + expect(managers.rule.ruleEngine).toBeNull() + }) + + describe('init()', () => { + it('handles the lack of rule engine', async () => { + const players = [{ id: playerId, username: 'John', currentGameId: null }] + const preferences = [{ playerId, color: 'red' }] + managers.rule.init({ managers, players, preferences }) + expect(managers.rule.ruleEngine).toBeNull() + expect(managers.rule.players).toEqual(players) + expect(managers.rule.preferences).toEqual(preferences) + await engine.onLoadingObservable.notifyObservers(false) + expect(scores).toBeNull() + }) + + it('initializes engine script and compute score on load', async () => { + const engineScript = `var engine={computeScore:()=>({"${playerId}":{total:1}})}` + const players = [{ id: playerId, username: 'John', currentGameId: null }] + managers.rule.init({ managers, engineScript, players }) + expect(managers.rule.ruleEngine).toEqual({ + computeScore: expect.any(Function) + }) + expect(managers.rule.players).toEqual(players) + await engine.onLoadingObservable.notifyObservers(false) + expect(scores).toEqual({ [playerId]: { total: 1 } }) + }) + }) + + describe('update()', () => { + const players = [{ id: playerId, username: 'Grace', currentGameId: null }] + const preferences = [{ playerId, color: 'blue' }] + + beforeEach(() => { + managers.rule.init({ managers, players, preferences }) + }) + + it('handles the lack of players', async () => { + const updated = [{ playerId, color: 'green' }] + managers.rule.update({ preferences: updated }) + expect(managers.rule.players).toEqual(players) + expect(managers.rule.preferences).toEqual(updated) + }) + + it('handles the lack of preferences', async () => { + const updated = [{ id: playerId, username: 'Kelly', currentGameId: null }] + managers.rule.update({ players: updated }) + expect(managers.rule.preferences).toEqual(preferences) + expect(managers.rule.players).toEqual(updated) + }) + }) + + it('computes scores on every action', async () => { + const engineScript = `let count=0;let engine={computeScore:(action,state,players,preferences)=>action?.fn==='flip'?{"${playerId}":{total:count+=preferences[0].amount}}:undefined}` + const preferences = [{ playerId, amount: 2 }] + managers.rule.init({ managers, engineScript, preferences }) + await managers.control.onActionObservable.notifyObservers({ + meshId: mesh.id, + fromHand: false, + fn: 'flip', + args: [] + }) + expect(scores).toEqual({ [playerId]: { total: 2 } }) + await mesh.metadata.flip?.() + expect(scores).toEqual({ [playerId]: { total: 4 } }) + }) + + it.skip('computes score after action is finished', async () => { + const engineScript = `let engine={computeScore:({meshId},{meshes})=>({"${playerId}":{total:meshes.find(({id})=>id===meshId)?.rotable.angle}})}` + engine.serialize + managers.rule.init({ managers, engineScript, preferences: [] }) + await mesh.metadata.rotate?.() + expect(scores).toEqual({ [playerId]: { total: Math.PI * 0.5 } }) + }) + + it('does not compute scores on moves', async () => { + const engineScript = `let engine={computeScore:()=>({"${playerId}":{total:1}})}` + managers.rule.init({ managers, engineScript }) + await managers.control.onActionObservable.notifyObservers({ + meshId: mesh.id, + fromHand: false, + pos: [1, 0, 0], + prev: [0, 0, 0] + }) + expect(scores).toBeNull() + await managers.control.onActionObservable.notifyObservers({ + meshId: mesh.id, + fromHand: false, + pos: [2, 0, 0], + prev: [1, 0, 0] + }) + expect(scores).toBeNull() + }) + + it('handles score computation error', async () => { + const engineScript = `let engine={computeScore:()=>{throw new Error('boom')}}` + managers.rule.init({ managers, engineScript }) + const action = { + meshId: mesh.id, + fromHand: false, + fn: /** @type {const} */ ('flip'), + args: [] + } + await managers.control.onActionObservable.notifyObservers(action) + expect(scores).toBeNull() + expect(logger.warn).toHaveBeenCalledWith( + { action, error: new Error('boom') }, + 'failed to evaluate score' + ) + expect(logger.warn).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/web/tests/3d/managers/selection.test.js b/apps/web/tests/3d/managers/selection.test.js index fdd6e92b..400b7fd1 100644 --- a/apps/web/tests/3d/managers/selection.test.js +++ b/apps/web/tests/3d/managers/selection.test.js @@ -10,7 +10,7 @@ import { expectMeshIds } from '../../test-utils' -describe('managers.Selection', () => { +describe('SelectionManager', () => { /** @type {import('@babylonjs/core').Scene} */ let scene /** @type {import('@babylonjs/core').Scene} */ @@ -108,13 +108,14 @@ describe('managers.Selection', () => { const box2 = createBox('box2', {}) const box3 = createBox('box3', {}) const box4 = createBox('box4', {}) + const box5 = createBox('box5', {}) box1.addBehavior( new AnchorBehavior( { anchors: [ - { id: '1', snappedId: box2.id }, - { id: '2', snappedId: null }, - { id: '3', snappedId: box3.id } + { id: '1', snappedIds: [box2.id] }, + { id: '2', snappedIds: [] }, + { id: '3', snappedIds: [box3.id, box5.id], max: 2 } ] }, managers @@ -124,8 +125,8 @@ describe('managers.Selection', () => { new AnchorBehavior( { anchors: [ - { id: '6', snappedId: box4.id }, - { id: '5', snappedId: null } + { id: '6', snappedIds: [box4.id] }, + { id: '5', snappedIds: [] } ] }, managers @@ -136,11 +137,13 @@ describe('managers.Selection', () => { expect(managers.selection.meshes.has(box2)).toBe(true) expect(managers.selection.meshes.has(box3)).toBe(true) expect(managers.selection.meshes.has(box4)).toBe(true) - expect(managers.selection.meshes.size).toBe(4) + expect(managers.selection.meshes.has(box5)).toBe(true) + expect(managers.selection.meshes.size).toBe(5) expectSelected(box1, colorByPlayerId.get(playerId)) expectSelected(box2, colorByPlayerId.get(playerId)) expectSelected(box3, colorByPlayerId.get(playerId)) expectSelected(box4, colorByPlayerId.get(playerId)) + expectSelected(box5, colorByPlayerId.get(playerId)) expect(selectionChanged).toHaveBeenCalledOnce() }) @@ -235,13 +238,14 @@ describe('managers.Selection', () => { const box2 = createBox('box2', {}) const box3 = createBox('box3', {}) const box4 = createBox('box4', {}) + const box5 = createBox('box5', {}) box1.addBehavior( new AnchorBehavior( { anchors: [ - { id: '1', snappedId: box2.id }, - { id: '2', snappedId: null }, - { id: '3', snappedId: box3.id } + { id: '1', snappedIds: [box2.id] }, + { id: '2', snappedIds: [] }, + { id: '3', snappedIds: [box3.id] } ] }, managers @@ -251,15 +255,15 @@ describe('managers.Selection', () => { new AnchorBehavior( { anchors: [ - { id: 'a', snappedId: box4.id }, - { id: 'b', snappedId: null } + { id: 'a', snappedIds: [box4.id, box5.id], max: 2 }, + { id: 'b', snappedIds: [] } ] }, managers ) ) managers.selection.select(box1) - expect(managers.selection.meshes.size).toBe(4) + expect(managers.selection.meshes.size).toBe(5) managers.selection.unselect(box3) expect(managers.selection.meshes.size).toBe(2) @@ -267,10 +271,12 @@ describe('managers.Selection', () => { expect(managers.selection.meshes.has(box2)).toBe(true) expect(managers.selection.meshes.has(box3)).toBe(false) expect(managers.selection.meshes.has(box4)).toBe(false) + expect(managers.selection.meshes.has(box5)).toBe(false) expectSelected(box1, colorByPlayerId.get(playerId)) expectSelected(box2, colorByPlayerId.get(playerId)) expectSelected(box3, null, false) expectSelected(box4, null, false) + expectSelected(box5, null, false) expect(selectionChanged).toHaveBeenCalledTimes(2) expect(selectionChanged.mock.calls[1][0].size).toBe(2) }) diff --git a/apps/web/tests/3d/managers/target.test.js b/apps/web/tests/3d/managers/target.test.js index 4563af40..e13314bc 100644 --- a/apps/web/tests/3d/managers/target.test.js +++ b/apps/web/tests/3d/managers/target.test.js @@ -95,6 +95,7 @@ describe('TargetManager', () => { /** @type {import('@src/3d/managers').SingleDropZone} */ let zone2 const aboveZone1 = new Vector3(5, 1, 5) + const partiallyAboveZone1 = new Vector3(5.5, 2, 5) const aboveZone2 = new Vector3(-5, 1, -5) const aboveZone3 = new Vector3(10, 1, 10) @@ -177,6 +178,15 @@ describe('TargetManager', () => { expect(managers.target.findDropZone(mesh)).toBeNull() }) + it('ignores partially overlapped multiple anchor', () => { + zone1.snappedIds = ['box1'] + zone1.max = 2 + const mesh = createBox('box', {}) + mesh.setAbsolutePosition(partiallyAboveZone1) + + expect(managers.target.findDropZone(mesh)).toBeNull() + }) + it('returns targets with same playerId below mesh with kind', () => { const zone3 = createsTargetZone('target3', { position: new Vector3(10, 0, 10), @@ -196,6 +206,13 @@ describe('TargetManager', () => { expectActiveZone(managers.target.findDropZone(mesh, 'box'), zone1, color) }) + it('returns anchor partially below mesh', () => { + const mesh = createBox('box', {}) + mesh.setAbsolutePosition(partiallyAboveZone1) + + expectActiveZone(managers.target.findDropZone(mesh, 'box'), zone1, color) + }) + it('returns targets below mesh with matching kind', () => { zone1.kinds = ['card', 'box'] const mesh = createBox('box', {}) @@ -211,7 +228,7 @@ describe('TargetManager', () => { }) createsTargetZone('target4', { position: new Vector3(-0.5, 1, 0) }) const zone5 = createsTargetZone('target5', { - position: new Vector3(0, 0, 0) + position: Vector3.Zero() }) const mesh = createBox('box', {}) @@ -222,7 +239,7 @@ describe('TargetManager', () => { it('returns highest target below mesh regardless of priorities', () => { createsTargetZone('target3', { - position: new Vector3(0, 0, 0), + position: Vector3.Zero(), priority: 10 }) const zone4 = createsTargetZone('target4', { @@ -237,13 +254,13 @@ describe('TargetManager', () => { }) it('returns highest priority target below mesh', () => { - createsTargetZone('target3', new Vector3(0, 0, 0)) + createsTargetZone('target3', Vector3.Zero()) const zone4 = createsTargetZone('target4', { - position: new Vector3(0, 0, 0), + position: Vector3.Zero(), priority: 2 }) createsTargetZone('target5', { - position: new Vector3(0, 0, 0), + position: Vector3.Zero(), priority: 1 }) @@ -253,6 +270,15 @@ describe('TargetManager', () => { expectActiveZone(managers.target.findDropZone(mesh, 'box'), zone4, color) }) + it('returns overlapped multiple anchor', () => { + zone1.snappedIds = ['box1'] + zone1.max = 2 + const mesh = createBox('box', {}) + mesh.setAbsolutePosition(aboveZone1) + + expectActiveZone(managers.target.findDropZone(mesh), zone1, color) + }) + describe('given a mesh with parts', () => { /** @type {import('@babylonjs/core').Mesh} */ let mesh @@ -268,7 +294,7 @@ describe('TargetManager', () => { it('returns a multi drop zone', () => { const zone4 = createsTargetZone('target4', { - position: new Vector3(0, 0, 0) + position: Vector3.Zero() }) const zone5 = createsTargetZone('target5', { position: new Vector3(1, 0, 0) @@ -282,7 +308,7 @@ describe('TargetManager', () => { }) it('gives precedence to a single drop zone that ignore part', () => { - createsTargetZone('target4', { position: new Vector3(0, 0, 0) }) + createsTargetZone('target4', { position: Vector3.Zero() }) createsTargetZone('target5', { position: new Vector3(1, 0, 0) }) const zone6 = createsTargetZone('target6', { position: new Vector3(0.5, 0, 0), @@ -293,14 +319,14 @@ describe('TargetManager', () => { }) it('returns null when at least one part is not covered', () => { - createsTargetZone('target4', { position: new Vector3(0, 0, 0) }) + createsTargetZone('target4', { position: Vector3.Zero() }) createsTargetZone('target5', { position: new Vector3(-1, 0, 0) }) expect(managers.target.findDropZone(mesh, 'box')).toBeNull() }) it('clears an active multi zone', () => { - createsTargetZone('target4', { position: new Vector3(0, 0, 0) }) + createsTargetZone('target4', { position: Vector3.Zero() }) createsTargetZone('target5', { position: new Vector3(1, 0, 0) }) const multiZone = managers.target.findDropZone(mesh, 'box') @@ -549,7 +575,7 @@ describe('TargetManager', () => { function createsTargetZone( /** @type {string} */ id, /** @type {Record & Partial<{ position: Vector3, scene: import('@babylonjs/core').Scene }>} */ - { position = new Vector3(0, 0, 0), scene: usedScene, ...properties } + { position = Vector3.Zero(), scene: usedScene, ...properties } ) { const targetable = createBox(`targetable-${id}`, {}, usedScene ?? scene) targetable.isPickable = false diff --git a/apps/web/tests/3d/meshes/rounded-tiles.test.js b/apps/web/tests/3d/meshes/rounded-tiles.test.js index b79f7507..57fc011e 100644 --- a/apps/web/tests/3d/meshes/rounded-tiles.test.js +++ b/apps/web/tests/3d/meshes/rounded-tiles.test.js @@ -93,7 +93,8 @@ describe('createRoundedTile()', () => { id: '', width: width * 0.5, height: height * 0.5, - kinds: [faker.lorem.word()] + kinds: [faker.lorem.word()], + snappedIds: [] } ] }, diff --git a/apps/web/tests/3d/utils/behaviors.test.js b/apps/web/tests/3d/utils/behaviors.test.js index a1ded902..aed8d367 100644 --- a/apps/web/tests/3d/utils/behaviors.test.js +++ b/apps/web/tests/3d/utils/behaviors.test.js @@ -343,7 +343,7 @@ describe('registerBehaviors() 3D utility', () => { height: 3.5, depth: 1.5, kinds: ['card'], - snappedId: 'a426f1' + snappedIds: ['a426f1'] } ], duration: 415 @@ -496,7 +496,7 @@ describe('restoreBehaviors() 3D utility', () => { height: 3.5, depth: 1.5, kinds: ['card'], - snappedId: 'a426f1' + snappedIds: ['a426f1'] } ], duration: 415 @@ -545,7 +545,7 @@ describe('restoreBehaviors() 3D utility', () => { height: 3.5, depth: 1.5, kinds: ['card'], - snappedId: 'a426f1' + snappedIds: ['a426f1'] } ], duration: 415 @@ -679,7 +679,7 @@ describe('serializeBehaviors() 3D utility', () => { height: 3.5, depth: 1.5, kinds: ['card'], - snappedId: 'a426f1' + snappedIds: ['a426f1'] } ], duration: 415 @@ -723,7 +723,7 @@ describe('serializeBehaviors() 3D utility', () => { height: 3.5, depth: 1.5, kinds: ['card'], - snappedId: 'a426f1' + snappedIds: ['a426f1'] } ], duration: 415 diff --git a/apps/web/tests/3d/utils/gravity.test.js b/apps/web/tests/3d/utils/gravity.test.js index c6584491..3e8d9722 100644 --- a/apps/web/tests/3d/utils/gravity.test.js +++ b/apps/web/tests/3d/utils/gravity.test.js @@ -4,6 +4,7 @@ import { faker } from '@faker-js/faker' import { altitudeGap, applyGravity, + getAltitudeAfterGravity, getCenterAltitudeAbove, getDimensions, isAbove, @@ -110,6 +111,60 @@ describe('applyGravity() 3D utility', () => { }) }) +describe('getAltitudeAfterGravity() 3D utility', () => { + const x = faker.number.int(999) + const z = faker.number.int(999) + + it('returns mesh position on the ground', () => { + const box = createBox('box', {}) + expect(getDimensions(box).height).toEqual(1) + box.setAbsolutePosition(new Vector3(x, 10, z)) + const box2 = createBox('box2', {}) + box2.setAbsolutePosition(new Vector3(x + 2, 3, z)) + expectCloseVector(getAltitudeAfterGravity(box), [x, 0.5, z]) + expect(box.absolutePosition).toEqual(new Vector3(x, 10, z)) + }) + + it('handles mesh with negative position', () => { + const box = createBox('box', {}) + expect(getDimensions(box).height).toEqual(1) + box.setAbsolutePosition(new Vector3(x, -10, z)) + const box2 = createBox('box2', {}) + box2.setAbsolutePosition(new Vector3(x, 3, z - 2)) + expectCloseVector(getAltitudeAfterGravity(box), [x, 0.5, z]) + expect(box.absolutePosition).toEqual(new Vector3(x, -10, z)) + }) + + it('returns position just above another one', () => { + const box = createBox('box', {}) + box.setAbsolutePosition(new Vector3(x, 15, z)) + const box2 = createBox('box2', {}) + box2.setAbsolutePosition(new Vector3(x, 3, z)) + expectCloseVector(getAltitudeAfterGravity(box), [x, 4 + altitudeGap, z]) + expect(box.absolutePosition).toEqual(new Vector3(x, 15, z)) + }) + + it('returns position above another one with partial overlap', () => { + const box = createBox('box', {}) + box.setAbsolutePosition(new Vector3(x, 10, z)) + const box2 = createBox('box2', {}) + box2.setAbsolutePosition(new Vector3(x - 0.5, 2, z - 0.5)) + expectCloseVector(getAltitudeAfterGravity(box), [x, 3 + altitudeGap, z]) + expect(box.absolutePosition).toEqual(new Vector3(x, 10, z)) + }) + + it('returns position just above several ones', () => { + const box = createBox('box', {}) + box.setAbsolutePosition(new Vector3(x, 20, z)) + const box2 = createBox('box2', {}) + box2.setAbsolutePosition(new Vector3(x - 0.5, 4, z)) + const box3 = createBox('box3', {}) + box3.setAbsolutePosition(new Vector3(x + 0.5, 3, z)) + expectCloseVector(getAltitudeAfterGravity(box), [x, 5 + altitudeGap, z]) + expect(box.absolutePosition).toEqual(new Vector3(x, 20, z)) + }) +}) + describe('isAbove() 3D utility', () => { const x = faker.number.int({ min: -10, max: 10 }) const z = faker.number.int({ min: -10, max: 10 }) diff --git a/apps/web/tests/3d/utils/mesh.test.js b/apps/web/tests/3d/utils/mesh.test.js index d6244873..7ff73aa0 100644 --- a/apps/web/tests/3d/utils/mesh.test.js +++ b/apps/web/tests/3d/utils/mesh.test.js @@ -79,12 +79,12 @@ describe('isContaining() 3D utility', () => { }) it('returns false when testing boxes that do not interesects', () => { - smallBox.setAbsolutePosition(new Vector3(0, 20, 0)) + smallBox.setAbsolutePosition(new Vector3(20, 0, 0)) expect(isContaining(bigBox, smallBox)).toBe(false) }) it('returns false when testing boxes that interesects', () => { - smallBox.setAbsolutePosition(new Vector3(0, 4, 0)) + smallBox.setAbsolutePosition(new Vector3(0, 0, 4)) expect(isContaining(bigBox, smallBox)).toBe(false) }) }) diff --git a/apps/web/tests/3d/utils/scene-loader.test.js b/apps/web/tests/3d/utils/scene-loader.test.js index 30c48870..198d6682 100644 --- a/apps/web/tests/3d/utils/scene-loader.test.js +++ b/apps/web/tests/3d/utils/scene-loader.test.js @@ -799,8 +799,8 @@ describe('loadMeshes() 3D utility', () => { anchorable: { duration: 100, anchors: [ - { snappedId: 'card2', x: shift }, - { snappedId: 'card4', x: -shift } + { snappedIds: ['card2'], x: shift }, + { snappedIds: ['card4'], x: -shift } ] }, movable: {} @@ -810,7 +810,7 @@ describe('loadMeshes() 3D utility', () => { id: 'card4', anchorable: { duration: 100, - anchors: [{ snappedId: 'card3' }] + anchors: [{ snappedIds: ['card3'] }] }, x: -5, y: 2.001, diff --git a/apps/web/tests/integration/utils/coverage.js b/apps/web/tests/integration/utils/coverage.js index decb3858..d1449c6a 100644 --- a/apps/web/tests/integration/utils/coverage.js +++ b/apps/web/tests/integration/utils/coverage.js @@ -15,7 +15,7 @@ const reporters = [ { name: 'lcov' } ] const previewURL = 'https://localhost:3000' -const distFolder = join('dist', 'unused/unused') +const distFolder = join('.vercel', 'output/static') const srcFolder = resolve(__filename, '../../../../src') export async function initializeCoverage() { diff --git a/apps/web/tests/routes/[[lang=lang]]/(auth)/game/[gameid]/Scores.tools.svelte b/apps/web/tests/routes/[[lang=lang]]/(auth)/game/[gameid]/Scores.tools.svelte new file mode 100644 index 00000000..15bad4f7 --- /dev/null +++ b/apps/web/tests/routes/[[lang=lang]]/(auth)/game/[gameid]/Scores.tools.svelte @@ -0,0 +1,26 @@ + + + + + + diff --git a/apps/web/tests/routes/[[lang=lang]]/(auth)/game/[gameid]/__snapshots__/Scores.tools.shot b/apps/web/tests/routes/[[lang=lang]]/(auth)/game/[gameid]/__snapshots__/Scores.tools.shot new file mode 100644 index 00000000..a18669aa --- /dev/null +++ b/apps/web/tests/routes/[[lang=lang]]/(auth)/game/[gameid]/__snapshots__/Scores.tools.shot @@ -0,0 +1,112 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`No score (not visible) 1`] = ` + + + + + + + + +`; + +exports[`With score 1`] = ` +HTMLCollection [ +
+ + +
+
+ 10 +
+ +
+
+
+ 12 +
+ +
+
+
+ 8 +
+ +
+
, + + + + + + + + , +] +`; diff --git a/apps/web/tests/stores/game-engine.test.js b/apps/web/tests/stores/game-engine.test.js index bab3a430..87541b62 100644 --- a/apps/web/tests/stores/game-engine.test.js +++ b/apps/web/tests/stores/game-engine.test.js @@ -74,6 +74,7 @@ describe('initEngine()', () => { const receiveRemoteSelection = vi.fn() const receiveHistory = vi.fn() const receiveReplayRank = vi.fn() + const receiveScores = vi.fn() const sendToPeer = /** @type {import('vitest').FunctionMock} */ (send) const lastMessageReceived = /** @type {import('rxjs').Subject} */ ( @@ -113,7 +114,8 @@ describe('initEngine()', () => { gameEngine.selectedMeshes.subscribe({ next: receiveSelection }), gameEngine.remoteSelection.subscribe({ next: receiveRemoteSelection }), gameEngine.history.subscribe({ next: receiveHistory }), - gameEngine.replayRank.subscribe({ next: receiveReplayRank }) + gameEngine.replayRank.subscribe({ next: receiveReplayRank }), + gameEngine.scores.subscribe({ next: receiveScores }) ] }) @@ -184,7 +186,7 @@ describe('initEngine()', () => { }) it('sends scene actions to peers', () => { - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const data = { fn: 'pop', args: [], @@ -200,7 +202,7 @@ describe('initEngine()', () => { }) it('does not send local actions to peers', () => { - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const data = { fn: 'pop', args: [], @@ -216,7 +218,7 @@ describe('initEngine()', () => { }) it('does not send hand actions to peers', () => { - /** @type {import('@src/3d/managers').Action} */ + /** @type {import('@tabulous/types').Action} */ const data = { fn: 'pop', args: [], @@ -396,6 +398,15 @@ describe('initEngine()', () => { expect(receiveHighlightHand).toHaveBeenCalledOnce() }) + it('proxies scores events', () => { + /** @type {import('@tabulous/types').Scores} */ + const scores = { playerId1: { total: 10 } } + managers.rule.onScoreUpdateObservable.notifyObservers(scores) + expect(receiveScores).toHaveBeenNthCalledWith(1, null) + expect(receiveScores).toHaveBeenNthCalledWith(2, scores) + expect(receiveScores).toHaveBeenCalledTimes(2) + }) + it('prunes peer pointers on connection', () => { expect(pruneUnusedPointers).not.toHaveBeenCalled() const id1 = '1' diff --git a/apps/web/tests/stores/game-manager.test.js b/apps/web/tests/stores/game-manager.test.js index b8281dcc..437dba23 100644 --- a/apps/web/tests/stores/game-manager.test.js +++ b/apps/web/tests/stores/game-manager.test.js @@ -44,11 +44,8 @@ import { send as actualSend } from '@src/stores/peer-channels' import { lastToast } from '@src/stores/toaster' -import { - buildPlayerColors, - findPlayerColor, - findPlayerPreferences -} from '@src/utils' +import { buildPlayerColors, findPlayerColor } from '@src/utils' +import { findPlayerPreferences } from '@tabulous/game-utils' import { translate } from '@tests/test-utils' import { Subject } from 'rxjs' import { get } from 'svelte/store' @@ -116,7 +113,7 @@ const lastMessageSent = actualLastMessageSent ) const action = - /** @type {import('rxjs').BehaviorSubject} */ ( + /** @type {import('rxjs').BehaviorSubject} */ ( actualAction ) const engine$ = @@ -526,7 +523,7 @@ describe('given a mocked game engine', () => { { playerId: player.id, colorByPlayerId, - preferences: { color: '#00ff00' } + preference: { color: '#00ff00' } }, true ) @@ -556,7 +553,7 @@ describe('given a mocked game engine', () => { const game = { id: gameId, players: [player], - schemaString: '{}' + schema: {} } runMutation.mockResolvedValueOnce(game) expect(await joinGame({ gameId, player, turnCredentials })).toEqual(game) @@ -625,7 +622,7 @@ describe('given a mocked game engine', () => { { playerId: player.id, colorByPlayerId: buildPlayerColors(game), - preferences: {} + preference: {} }, true ) @@ -686,7 +683,7 @@ describe('given a mocked game engine', () => { { playerId: player.id, colorByPlayerId: buildPlayerColors(game), - preferences: {} + preference: {} }, true ) @@ -728,7 +725,7 @@ describe('given a mocked game engine', () => { { playerId: player.id, colorByPlayerId: buildPlayerColors(game), - preferences: {} + preference: {} }, true ) @@ -744,7 +741,7 @@ describe('given a mocked game engine', () => { id: gameId, kind: 'belote', players: [{ ...player, isGuest: true }], - schemaString: '{}' + schema: {} } runMutation.mockResolvedValueOnce(game) expect(await joinGame({ gameId, player, turnCredentials })).toEqual(game) @@ -768,7 +765,7 @@ describe('given a mocked game engine', () => { id: gameId, kind: 'belote', players: [{ ...player, isGuest: true }], - schemaString: '{}' + schema: {} } const parameters = { side: 'white' } runMutation.mockResolvedValueOnce(game) @@ -858,7 +855,7 @@ describe('given a mocked game engine', () => { player.id, { ...player, - ...findPlayerPreferences(game, player.id), + ...findPlayerPreferences(game.preferences, player.id), isHost: true, playing: true } @@ -867,7 +864,7 @@ describe('given a mocked game engine', () => { partner1.id, { ...partner1, - ...findPlayerPreferences(game, partner1.id), + ...findPlayerPreferences(game.preferences, partner1.id), isHost: false, playing: true } @@ -902,7 +899,7 @@ describe('given a mocked game engine', () => { player.id, { ...player, - ...findPlayerPreferences(game, player.id), + ...findPlayerPreferences(game.preferences, player.id), isHost: true, playing: true } @@ -911,7 +908,7 @@ describe('given a mocked game engine', () => { partner1.id, { ...partner1, - ...findPlayerPreferences(game, partner1.id), + ...findPlayerPreferences(game.preferences, partner1.id), isHost: false, playing: false } @@ -920,7 +917,7 @@ describe('given a mocked game engine', () => { partner2.id, { ...partner2, - ...findPlayerPreferences(game, partner2.id), + ...findPlayerPreferences(game.preferences, partner2.id), isHost: false, playing: true } @@ -1252,7 +1249,7 @@ describe('given a mocked game engine', () => { { playerId: player.id, colorByPlayerId: buildPlayerColors(game), - preferences: { color: '#00ff00' } + preference: { color: '#00ff00' } }, true ) @@ -1382,7 +1379,7 @@ describe('given a mocked game engine', () => { { ...partner2, isGuest: true }, { ...partner3, currentGameId: faker.string.uuid() } ], - schemaString: '{}' + schema: {} } runMutation.mockReset().mockResolvedValueOnce(gameParameters) expect( @@ -1447,7 +1444,7 @@ describe('given a mocked game engine', () => { { playerId: partner2.id, colorByPlayerId: buildPlayerColors(game), - preferences: { color: '#ffffff' } + preference: { color: '#ffffff' } }, true ) @@ -1464,7 +1461,7 @@ describe('given a mocked game engine', () => { gamer.id, { ...gamer, - ...findPlayerPreferences(game, gamer.id), + ...findPlayerPreferences(game.preferences, gamer.id), isHost: gamer.id === partner1.id, playing: gamer.id === partner2.id } @@ -1489,7 +1486,7 @@ describe('given a mocked game engine', () => { { playerId: partner2.id, colorByPlayerId: buildPlayerColors(game), - preferences: { color: '#ffffff' } + preference: { color: '#ffffff' } }, false ) @@ -1503,7 +1500,7 @@ describe('given a mocked game engine', () => { gamer.id, { ...gamer, - ...findPlayerPreferences(game, gamer.id), + ...findPlayerPreferences(game.preferences, gamer.id), isHost: gamer.id === partner1.id, playing: gamer.id === partner2.id } @@ -1611,7 +1608,7 @@ describe('given a mocked game engine', () => { partner3.id, { ...(game.players ?? [])[0], - ...findPlayerPreferences(game, partner3.id), + ...findPlayerPreferences(game.preferences, partner3.id), isHost: false, playing: false } @@ -1620,7 +1617,7 @@ describe('given a mocked game engine', () => { player.id, { ...player, - ...findPlayerPreferences(game, player.id), + ...findPlayerPreferences(game.preferences, player.id), currentGameId: game.id, isHost: false, playing: false @@ -1630,7 +1627,7 @@ describe('given a mocked game engine', () => { partner1.id, { ...partner1, - ...findPlayerPreferences(game, partner1.id), + ...findPlayerPreferences(game.preferences, partner1.id), currentGameId: game.id, isHost: false, playing: false @@ -1640,7 +1637,7 @@ describe('given a mocked game engine', () => { partner2.id, { ...partner2, - ...findPlayerPreferences(game, partner2.id), + ...findPlayerPreferences(game.preferences, partner2.id), isHost: true, playing: true @@ -1694,7 +1691,7 @@ describe('given a mocked game engine', () => { partner3.id, { ...(game.players ?? [])[0], - ...findPlayerPreferences(game, partner3.id), + ...findPlayerPreferences(game.preferences, partner3.id), isHost: false, playing: false } @@ -1703,7 +1700,7 @@ describe('given a mocked game engine', () => { player.id, { ...player, - ...findPlayerPreferences(game, player.id), + ...findPlayerPreferences(game.preferences, player.id), currentGameId: game.id, // this player will not be host when the new host will send their first update isHost: true, @@ -1714,7 +1711,7 @@ describe('given a mocked game engine', () => { partner1.id, { ...partner1, - ...findPlayerPreferences(game, partner1.id), + ...findPlayerPreferences(game.preferences, partner1.id), currentGameId: game.id, // this player will become host when they'll send their first update isHost: false, @@ -1725,7 +1722,7 @@ describe('given a mocked game engine', () => { partner2.id, { ...partner2, - ...findPlayerPreferences(game, partner2.id), + ...findPlayerPreferences(game.preferences, partner2.id), isHost: false, playing: true } diff --git a/apps/web/tests/stores/indicators.test.js b/apps/web/tests/stores/indicators.test.js index 92ac2f89..83486122 100644 --- a/apps/web/tests/stores/indicators.test.js +++ b/apps/web/tests/stores/indicators.test.js @@ -525,7 +525,7 @@ describe('Indicators store', () => { it('has no player data on drop zones', async () => { const [card] = cards card.getBehaviorByName(AnchorBehaviorName)?.fromState({ - anchors: [{ id: 'anchor-0', playerId: players[0].id }] + anchors: [{ id: 'anchor-0', playerId: players[0].id, snappedIds: [] }] }) expectIndicators([ { @@ -575,12 +575,12 @@ describe('Indicators store', () => { it('has player indicators for drop zones', async () => { const [card1, card2] = cards - card1 - .getBehaviorByName(AnchorBehaviorName) - ?.fromState({ anchors: [{ id: '2', playerId: players[0].id }] }) - card2 - .getBehaviorByName(AnchorBehaviorName) - ?.fromState({ anchors: [{ id: '3', playerId: players[1].id }] }) + card1.getBehaviorByName(AnchorBehaviorName)?.fromState({ + anchors: [{ id: '2', playerId: players[0].id, snappedIds: [] }] + }) + card2.getBehaviorByName(AnchorBehaviorName)?.fromState({ + anchors: [{ id: '3', playerId: players[1].id, snappedIds: [] }] + }) expectIndicators([ { id: `${players[0].id}.drop-zone.2`, diff --git a/apps/web/tests/test-utils.js b/apps/web/tests/test-utils.js index 5b01363c..3a5fd13e 100644 --- a/apps/web/tests/test-utils.js +++ b/apps/web/tests/test-utils.js @@ -10,6 +10,7 @@ import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector' import { CreateBox } from '@babylonjs/core/Meshes/Builders/boxBuilder' import { CreateCylinder } from '@babylonjs/core/Meshes/Builders/cylinderBuilder' import { Logger } from '@babylonjs/core/Misc/logger' +import { Observable } from '@babylonjs/core/Misc/observable' import { faker } from '@faker-js/faker' import cors from '@fastify/cors' import { @@ -28,6 +29,7 @@ import { MaterialManager, MoveManager, ReplayManager, + RuleManager, SelectionManager, TargetManager } from '@src/3d/managers' @@ -140,6 +142,7 @@ export function initialize3dEngine({ handScene.render() }) engine.inputElement = document.body + engine.onLoadingObservable = new Observable() engine.simulation = isSimulation ? null : engine const scene = main.scene @@ -181,7 +184,8 @@ export function initialize3dEngine({ replay: new ReplayManager({ engine, moveDuration: globalThis.use3dSimulation ? 0 : 200 - }) + }), + rule: new RuleManager({ engine }) } const playerId = faker.person.fullName() const color = '#ff0000' @@ -321,23 +325,29 @@ export function expectScreenPosition(actual, { x, y }, message) { /** * @param {import('@babylonjs/core').Mesh} mesh - actual anchorable mesh. - * @param {import('@babylonjs/core').Mesh} snapped - expected snapped mesh. + * @param {import('@babylonjs/core').Mesh[]} snapped - expected snapped mesh. * @param {number} [anchorRank=0] - rank of the snapped anchor, defaults to 0. */ export function expectSnapped(mesh, snapped, anchorRank = 0) { const behavior = mesh.getBehaviorByName(AnchorBehaviorName) const anchor = behavior?.state.anchors[anchorRank] const zone = behavior?.zones[anchorRank] - expect(anchor?.snappedId).toEqual(snapped.id) - expect(mesh.metadata.anchors?.[anchorRank].snappedId).toEqual(snapped.id) + const snappedIds = snapped.map(({ id }) => id) + expect(anchor?.snappedIds).toEqual(snappedIds) + expect(mesh.metadata.anchors?.[anchorRank].snappedIds).toEqual(snappedIds) expectZoneEnabled(mesh, anchorRank, false) - expect(behavior?.snappedZone(snapped.id)?.mesh.id).toEqual(zone?.mesh.id) + for (const id of snappedIds) { + expect(behavior?.snappedZone(id)?.mesh.id).toEqual(zone?.mesh.id) + } zone?.mesh.computeWorldMatrix(true) - expectPosition(snapped, [ - zone?.mesh.absolutePosition.x ?? 0, - getCenterAltitudeAbove(mesh, snapped), - zone?.mesh.absolutePosition.z ?? 0 - ]) + if ((anchor?.max ?? 1) === 1) { + // TODO layout multiple meshes + expectPosition(snapped[0], [ + zone?.mesh.absolutePosition.x ?? 0, + getCenterAltitudeAbove(mesh, snapped[0]), + zone?.mesh.absolutePosition.z ?? 0 + ]) + } } /** @@ -350,8 +360,8 @@ export function expectUnsnapped(mesh, snapped, anchorRank = 0) { const anchor = behavior?.state.anchors[anchorRank] expectZoneEnabled(mesh, anchorRank) expect(behavior?.snappedZone(snapped.id)).toBeNull() - expect(anchor?.snappedId).not.toBeDefined() - expect(mesh.metadata.anchors?.[anchorRank].snappedId).not.toBeDefined() + expect(anchor?.snappedIds).toHaveLength(0) + expect(mesh.metadata.anchors?.[anchorRank].snappedIds).toEqual([]) } /** diff --git a/apps/web/tests/utils/game.test.js b/apps/web/tests/utils/game.test.js index 976351d8..df383008 100644 --- a/apps/web/tests/utils/game.test.js +++ b/apps/web/tests/utils/game.test.js @@ -4,7 +4,6 @@ import { applyGameColors, buildPlayerColors, findPlayerColor, - findPlayerPreferences, isGuest, isLobby } from '@src/utils/game' @@ -21,29 +20,6 @@ describe('Game utils', () => { ] } - describe('findPlayerPreferences()', () => { - it('returns an empty object on games with no preferences', async () => { - expect( - findPlayerPreferences({ id: '', created: Date.now() }, 'whatever') - ).toEqual({}) - }) - - it('returns an empty object for an unknown player', async () => { - expect(findPlayerPreferences(game, 'whatever')).toEqual({}) - }) - - it('returns preferences of a given player, omitting the playerId', async () => { - expect(findPlayerPreferences(game, 'a')).toEqual({ - ...game.preferences[0], - playerId: undefined - }) - expect(findPlayerPreferences(game, 'b')).toEqual({ - ...game.preferences[1], - playerId: undefined - }) - }) - }) - describe('findPlayerColor()', () => { const defaultColor = '#ff4500' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fffa5b9..1f0b3738 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + patchedDependencies: '@vitest/utils@0.34.4': hash: dlurkgklbhntz7rn3vvtvpwdu4 @@ -173,6 +177,9 @@ importers: dotenv: specifier: ^16.3.1 version: 16.3.1 + esbuild: + specifier: ^0.19.4 + version: 0.19.4 fast-jwt: specifier: ^3.2.0 version: 3.2.0 @@ -285,6 +292,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^2.4.6 version: 2.4.6(svelte@4.2.0)(vite@4.4.9) + '@tabulous/game-utils': + specifier: workspace:* + version: link:../game-utils '@tabulous/types': specifier: workspace:* version: link:../types @@ -958,6 +968,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.19.4: + resolution: {integrity: sha512-mRsi2vJsk4Bx/AFsNBqOH2fqedxn5L/moT58xgg51DjX1la64Z3Npicut2VbhvDFO26qjWtPMsVxCd80YTFVeg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm@0.18.20: resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -967,6 +986,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.19.4: + resolution: {integrity: sha512-uBIbiYMeSsy2U0XQoOGVVcpIktjLMEKa7ryz2RLr7L/vTnANNEsPVAh4xOv7ondGz6ac1zVb0F8Jx20rQikffQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-x64@0.18.20: resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -976,6 +1004,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.19.4: + resolution: {integrity: sha512-4iPufZ1TMOD3oBlGFqHXBpa3KFT46aLl6Vy7gwed0ZSYgHaZ/mihbYb4t7Z9etjkC9Al3ZYIoOaHrU60gcMy7g==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/darwin-arm64@0.18.20: resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -985,6 +1022,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.19.4: + resolution: {integrity: sha512-Lviw8EzxsVQKpbS+rSt6/6zjn9ashUZ7Tbuvc2YENgRl0yZTktGlachZ9KMJUsVjZEGFVu336kl5lBgDN6PmpA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@esbuild/darwin-x64@0.18.20: resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -994,6 +1040,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.19.4: + resolution: {integrity: sha512-YHbSFlLgDwglFn0lAO3Zsdrife9jcQXQhgRp77YiTDja23FrC2uwnhXMNkAucthsf+Psr7sTwYEryxz6FPAVqw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@esbuild/freebsd-arm64@0.18.20: resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -1003,6 +1058,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.19.4: + resolution: {integrity: sha512-vz59ijyrTG22Hshaj620e5yhs2dU1WJy723ofc+KUgxVCM6zxQESmWdMuVmUzxtGqtj5heHyB44PjV/HKsEmuQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/freebsd-x64@0.18.20: resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -1012,6 +1076,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.19.4: + resolution: {integrity: sha512-3sRbQ6W5kAiVQRBWREGJNd1YE7OgzS0AmOGjDmX/qZZecq8NFlQsQH0IfXjjmD0XtUYqr64e0EKNFjMUlPL3Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-arm64@0.18.20: resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -1021,6 +1094,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.19.4: + resolution: {integrity: sha512-ZWmWORaPbsPwmyu7eIEATFlaqm0QGt+joRE9sKcnVUG3oBbr/KYdNE2TnkzdQwX6EDRdg/x8Q4EZQTXoClUqqA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-arm@0.18.20: resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -1030,6 +1112,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.19.4: + resolution: {integrity: sha512-z/4ArqOo9EImzTi4b6Vq+pthLnepFzJ92BnofU1jgNlcVb+UqynVFdoXMCFreTK7FdhqAzH0vmdwW5373Hm9pg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-ia32@0.18.20: resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -1039,6 +1130,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.19.4: + resolution: {integrity: sha512-EGc4vYM7i1GRUIMqRZNCTzJh25MHePYsnQfKDexD8uPTCm9mK56NIL04LUfX2aaJ+C9vyEp2fJ7jbqFEYgO9lQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-loong64@0.18.20: resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -1048,6 +1148,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.19.4: + resolution: {integrity: sha512-WVhIKO26kmm8lPmNrUikxSpXcgd6HDog0cx12BUfA2PkmURHSgx9G6vA19lrlQOMw+UjMZ+l3PpbtzffCxFDRg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-mips64el@0.18.20: resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -1057,6 +1166,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.19.4: + resolution: {integrity: sha512-keYY+Hlj5w86hNp5JJPuZNbvW4jql7c1eXdBUHIJGTeN/+0QFutU3GrS+c27L+NTmzi73yhtojHk+lr2+502Mw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-ppc64@0.18.20: resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -1066,6 +1184,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.19.4: + resolution: {integrity: sha512-tQ92n0WMXyEsCH4m32S21fND8VxNiVazUbU4IUGVXQpWiaAxOBvtOtbEt3cXIV3GEBydYsY8pyeRMJx9kn3rvw==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-riscv64@0.18.20: resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -1075,6 +1202,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.19.4: + resolution: {integrity: sha512-tRRBey6fG9tqGH6V75xH3lFPpj9E8BH+N+zjSUCnFOX93kEzqS0WdyJHkta/mmJHn7MBaa++9P4ARiU4ykjhig==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-s390x@0.18.20: resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -1084,6 +1220,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.19.4: + resolution: {integrity: sha512-152aLpQqKZYhThiJ+uAM4PcuLCAOxDsCekIbnGzPKVBRUDlgaaAfaUl5NYkB1hgY6WN4sPkejxKlANgVcGl9Qg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-x64@0.18.20: resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -1093,6 +1238,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.19.4: + resolution: {integrity: sha512-Mi4aNA3rz1BNFtB7aGadMD0MavmzuuXNTaYL6/uiYIs08U7YMPETpgNn5oue3ICr+inKwItOwSsJDYkrE9ekVg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/netbsd-x64@0.18.20: resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -1102,6 +1256,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.19.4: + resolution: {integrity: sha512-9+Wxx1i5N/CYo505CTT7T+ix4lVzEdz0uCoYGxM5JDVlP2YdDC1Bdz+Khv6IbqmisT0Si928eAxbmGkcbiuM/A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/openbsd-x64@0.18.20: resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -1111,6 +1274,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.19.4: + resolution: {integrity: sha512-MFsHleM5/rWRW9EivFssop+OulYVUoVcqkyOkjiynKBCGBj9Lihl7kh9IzrreDyXa4sNkquei5/DTP4uCk25xw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/sunos-x64@0.18.20: resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -1120,6 +1292,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.19.4: + resolution: {integrity: sha512-6Xq8SpK46yLvrGxjp6HftkDwPP49puU4OF0hEL4dTxqCbfx09LyrbUj/D7tmIRMj5D5FCUPksBbxyQhp8tmHzw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-arm64@0.18.20: resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -1129,6 +1310,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.19.4: + resolution: {integrity: sha512-PkIl7Jq4mP6ke7QKwyg4fD4Xvn8PXisagV/+HntWoDEdmerB2LTukRZg728Yd1Fj+LuEX75t/hKXE2Ppk8Hh1w==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-ia32@0.18.20: resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -1138,6 +1328,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.19.4: + resolution: {integrity: sha512-ga676Hnvw7/ycdKB53qPusvsKdwrWzEyJ+AtItHGoARszIqvjffTwaaW3b2L6l90i7MO9i+dlAW415INuRhSGg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-x64@0.18.20: resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -1147,6 +1346,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.19.4: + resolution: {integrity: sha512-HP0GDNla1T3ZL8Ko/SHAS2GgtjOg+VmWnnYLhuTksr++EnduYB0f3Y2LzHsUwb2iQ13JGoY6G3R8h6Du/WG6uA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.49.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3145,6 +3353,36 @@ packages: '@esbuild/win32-x64': 0.18.20 dev: true + /esbuild@0.19.4: + resolution: {integrity: sha512-x7jL0tbRRpv4QUyuDMjONtWFciygUxWaUM1kMX2zWxI0X2YWOt7MSA0g4UdeSiHM8fcYVzpQhKYOycZwxTdZkA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.19.4 + '@esbuild/android-arm64': 0.19.4 + '@esbuild/android-x64': 0.19.4 + '@esbuild/darwin-arm64': 0.19.4 + '@esbuild/darwin-x64': 0.19.4 + '@esbuild/freebsd-arm64': 0.19.4 + '@esbuild/freebsd-x64': 0.19.4 + '@esbuild/linux-arm': 0.19.4 + '@esbuild/linux-arm64': 0.19.4 + '@esbuild/linux-ia32': 0.19.4 + '@esbuild/linux-loong64': 0.19.4 + '@esbuild/linux-mips64el': 0.19.4 + '@esbuild/linux-ppc64': 0.19.4 + '@esbuild/linux-riscv64': 0.19.4 + '@esbuild/linux-s390x': 0.19.4 + '@esbuild/linux-x64': 0.19.4 + '@esbuild/netbsd-x64': 0.19.4 + '@esbuild/openbsd-x64': 0.19.4 + '@esbuild/sunos-x64': 0.19.4 + '@esbuild/win32-arm64': 0.19.4 + '@esbuild/win32-ia32': 0.19.4 + '@esbuild/win32-x64': 0.19.4 + dev: false + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'}