Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): introduce score tracking #169

Merged
merged 28 commits into from
Oct 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f5f0792
feat(server): bundles games on server start
feugy Oct 2, 2023
da8dd7d
feat(server): returns bundled rule engine when promoting and joining …
feugy Oct 2, 2023
fd0e9b7
chore(server): configures repositories isProduction option
feugy Oct 2, 2023
4fe7b42
refactor(web): moves some types to @tabulous/types
feugy Oct 2, 2023
a63e3b4
feat(web): initializes rule engine and computes score on action
feugy Oct 2, 2023
375acdd
refactor(game-utils): takes findPlayerPreferences from web
feugy Oct 5, 2023
fc13f65
feat(server)!: GraphQL game queries now return serialized preferences
feugy Oct 5, 2023
2c0b306
feat(web): displays game scores, loads serialized preferences, provid…
feugy Oct 5, 2023
9db1d0a
feat(game-utils): allows testing random game code
feugy Oct 6, 2023
2dcf59f
fix(web): anchorable flip is ignore when loading game.
feugy Oct 6, 2023
763544f
chore(web): adds more logs to rule manager
feugy Oct 6, 2023
a388496
feat(game-utils): supports multiple anchors
feugy Oct 10, 2023
04f56e5
feat(types): supports multiple anchors
feugy Oct 10, 2023
706c209
feat(server): supports multiple anchors and migrate existing games
feugy Oct 10, 2023
4c29bac
feat(web): supports anchors with multiple snapped meshes
feugy Oct 11, 2023
2c1e9e0
feat(draught): supports multiple anchors and implements score
feugy Oct 11, 2023
a666d27
feat(playground): supports multiple anchors
feugy Oct 11, 2023
d5f0a41
feat(klondike): supports multiple anchors
feugy Oct 11, 2023
90ea9d2
feat(chess): supports multiple anchors
feugy Oct 11, 2023
7c7f701
feat(mah-jong): supports multiple anchors and implements score
feugy Oct 14, 2023
6ad9b4c
fix(web): lost meshes when decrementing a quantifiable
feugy Oct 14, 2023
fa993a2
chore(web): resets score when changing games
feugy Oct 14, 2023
f46af13
chore(web): tweaks score rendering
feugy Oct 14, 2023
e70ab32
fix(web): game loading screen hides game aside tabs
feugy Oct 14, 2023
c64523d
chore(web): consider game update with no players nor preferences
feugy Oct 14, 2023
234b7cb
chore(web): deserialize preference string on game update
feugy Oct 14, 2023
0dac733
chore(ci): upgrade web integration tests to pnpm@8
feugy Oct 14, 2023
8aa6ab5
chore: fixes integration tests
feugy Oct 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ test-results
.vercel
.last-db
upload-manually.sh
en
en
engine.min.js
16 changes: 13 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?
Expand Down Expand Up @@ -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.
26 changes: 18 additions & 8 deletions apps/game-utils/src/descriptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { mergeProps } from './utils.js'

/**
* Creates a unique game from a game descriptor.
* @template {Record<string, ?>} Parameters
* @param {string} kind - created game's kind.
* @param {Partial<import('@tabulous/types').GameDescriptor>} descriptor - to create game from.
* @param {Partial<import('@tabulous/types').GameDescriptor<Parameters>>} descriptor - to create game from.
* @returns a list of serialized 3D meshes.
*/
export async function createMeshes(kind, descriptor) {
Expand Down Expand Up @@ -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<string, import('@tabulous/types').Mesh[]>} meshesByBagId - randomized meshes per bags.
* @param {import('@tabulous/types').Mesh[]} allMeshes - all meshes
Expand All @@ -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)
}
}

Expand Down
10 changes: 6 additions & 4 deletions apps/game-utils/src/hand.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,25 @@ 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)
if (drawn === stack) {
break
}
}
if (stack.stackable?.stackIds?.length === 0) {
anchor.snappedId = null
if ((stack.stackable?.stackIds?.length ?? 0) === 0) {
anchor.snappedIds.splice(0, 1)
}
}

Expand Down
15 changes: 7 additions & 8 deletions apps/game-utils/src/mesh.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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)
}

Expand Down
18 changes: 18 additions & 0 deletions apps/game-utils/src/preference.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<import('@tabulous/types').PlayerPreference, 'playerId'>} */
const preference = {
...(preferences?.find(preference => preference.playerId === playerId) ?? {
color: undefined,
angle: undefined
})
}
delete preference.playerId
return preference
}
Loading
Loading