We need to implement playing faceCards via a seven in the GameState API in order to support the full game logic in this API. See the spec for a general overview of the API, its underlying motivations, and the development approach.
Playing a face card from a seven (sevenFaceCard) means removing the playedCard from the deck and putting it into the player's faceCards array. It should also remove the current oneOff (the seven being resolved) and put it into the scrap, then increment the turn and set the phase to GamePhase.MAIN. See api/helpers/gamestate/moves/seven-points/execute.js for an example of a seven move, and api/helpers/gamestate/moves/face-card/execute.js` for an example of playing a faceCard.
Making this move, like all others, will be triggered via the endpoint:
POST /api/game/:gameId/move
Which move gets made is determined by the request body, which should specify a moveType field with a value from the MoveType enum. Playing a jack will be triggered by sending a request body like:
When a moveType: MoveType.SEVEN_FACE_CARD is sent in the req body, the endpoint will attempt to utilize the sevenFaceCard helpers: sails.helpers.gamestate.moves.sevenFaceCard.validate() and sails.helpers.gamestate.moves.sevenFaceCard.execute(). Each of these should follow the paradigm outlined in the docs:
There are four pieces to implement:
Update the api/policies/hasValidMoveBody policy to allow and validate requests with a moveType of MoveType.SEVEN_FACE_CARD
Implement the api/helpers/gamestate/moves/seven-face-card/validate helper to verify if the move is legal
Implement the api/helpers/gamestate/moves/seven-face-card/execute helper to create a new GameState that plays the chosen faceCard from the top of the deck.
Unskip the relevant tests by removing the cy.skipOnOnGameStateApi() commands from tests which can now pass in the GameState API
Updating the api/policies/hasValidMoveBody policy
We must also update the policy api/policies/hasValidMoveBody.js to accommodate the MoveType.SEVEN_FACE_CARD moveType. This policy is already applied to the endpoint for making a move and it uses a switch() statement on req.body.moveType to validate that a given req body has all the data necessary for the given moveType. We should add a MoveType.SEVEN_FACE_CARD case to this policy.
A moveType of MoveType.SEVEN_FACE_CARD requires a cardId which is a valid card id. If cardId is present in req.body and can be found in utils/DeckIds.json, then we can return res.next() in this switch case. Otherwise we should return res.badRequest() with an error that explains whether req.body.cardId attribute is missing or invalid.
Implementing validate()
Create a new api/helpers/gamestate/moves/seven-face-card/validate.js helper file that matches the general setup of the existing api/helpers/gamestate/moves/face-card/validate.js helper. It should throw errors if the requested move is illegal. It should have three inputs:
currentState - the unpacked GameState from before the move is made
requestedMove -req body describing the move being made3.
playedBy - 0 or 1 for whether p0 or p1 is making the move.
The general idea is that the move is legal if it's your turn, we're resolving a seven, the card you picked can be played as a face card, and the card you picked is in the top two cards of the deck.
validate() should throw if:
It is not the requesting player's turn (throw new Error('game.snackbar.global.notYourTurn');)
The GameState's phase !== GamePhase.RESOLVING_SEVEN (throw new Error('game.snackbar.seven.pickAndPlay');)
The playedCard is not in the top two cards of the deck (throw new Error('game.snackbar.seven.pickAndPlay');)
The playedCard is not a queen, king, or 8 (throw new Error('game.snackbar.faceCard.withoutTarget');)
Implementing execute()
Create a new api/helpers/gamestate/moves/seven-face-card/execute.js helper file that matches the general approach of the existing execute helper api/helpers/gamestate/moves/face-card/execute.js. It should create and return a new GameState that is the result of making the sevenFaceCard move for the requested player i.e. it should remove the playedCard from the deck and put it into the requesting player's faceCards. The playedBy and playedCard attributes should be included in the returned GameState. The GameState's phase should be set to GamePhase.MAIN, the turn should be incremented, the oneOff should be put into the scrap. The seven which was the oneOff should also be set as the resolved attribute of the GameState.
execute() should have 3 inputs:
currentState - the unpacked GameState from before the move is made
requestedMove -req body describing the move being made
playedBy - 0 or 1 for whether p0 or p1 is making the move.
Unskipping Tests
Because we are approaching the GameState API project with a feature-flag approach, all changes are merged into themainbranch and the application and tests are set to dynamically run in either the GameState API mode, or the legacy mode, based on theVITE_USE_GAMESTATE_API` env variable. In CI, we run the full suite of tests against the legacy version of the application and then run a subset of the tests against the GameState API version of the application.
Which tests are run in the GameState API version of CI is controlled via the test command cy.skipOnGameStateApi() which programmatically skips a test if the env var VITE_USE_GAMESTATE_API is enabled. As we develop new features in the GameState API, we should remove the cy.skipOnGameStateApi() command from more and more of the tests.
Essential tests
For this PR, at minimum, we should unskip the following tests, which should be able to pass once it is working correctly. All the following tests are in the tests/e2e/specs/in-game/one-offs/7_sevens folder:
playerSevens.spec.js - it('Plays king from a seven')
playerSevens.spec.js - it('Plays queen from a seven')
opponentSevens.spec.js - it('Opponent plays king from seven (Top Card)')
opponentSevens.spec.js - it('Opponent plays queen from seven (Second Card)')
opponentSevens.spec.js - it('Opponent plays eight as glasses from seven (top card)')
You can run the tests against the GameState API locally by:
booting the server in gamestate mode with
npm run start:server:gamestate
Then booting the client in gamestate mode with
npm run start:client:gamestate
running the tests in gamestate mode with
npm run e2e:client:gamestate.
Feature Summary
We need to implement playing faceCards via a seven in the GameState API in order to support the full game logic in this API. See the spec for a general overview of the API, its underlying motivations, and the development approach.
Playing a face card from a seven (
sevenFaceCard
) means removing theplayedCard
from the deck and putting it into the player'sfaceCards
array. It should also remove the currentoneOff
(the seven being resolved) and put it into thescrap
, then increment the turn and set thephase
toGamePhase.MAIN.
Seeapi/helpers/gamestate/moves/seven-points/execute.js
for an example of a seven move, and api/helpers/gamestate/moves/face-card/execute.js` for an example of playing a faceCard.Detailed Description
Implementation should take the general approach shown in the Backend Control Flow section of the GameState API Spec and follow the example of #1025
Making this move, like all others, will be triggered via the endpoint:
Which move gets made is determined by the request body, which should specify a moveType field with a value from the MoveType enum. Playing a jack will be triggered by sending a request body like:
When a
moveType: MoveType.SEVEN_FACE_CARD
is sent in the req body, the endpoint will attempt to utilize thesevenFaceCard
helpers:sails.helpers.gamestate.moves.sevenFaceCard.validate()
andsails.helpers.gamestate.moves.sevenFaceCard.execute()
. Each of these should follow the paradigm outlined in the docs:There are four pieces to implement:
api/policies/hasValidMoveBody
policy to allow and validate requests with a moveType ofMoveType.SEVEN_FACE_CARD
api/helpers/gamestate/moves/seven-face-card/validate
helper to verify if the move is legalapi/helpers/gamestate/moves/seven-face-card/execute
helper to create a new GameState that plays the chosen faceCard from the top of the deck.cy.skipOnOnGameStateApi()
commands from tests which can now pass in the GameState APIUpdating the
api/policies/hasValidMoveBody
policyWe must also update the policy
api/policies/hasValidMoveBody.js
to accommodate theMoveType.SEVEN_FACE_CARD
moveType. This policy is already applied to the endpoint for making a move and it uses aswitch()
statement onreq.body.moveType
to validate that a given req body has all the data necessary for the givenmoveType
. We should add aMoveType.SEVEN_FACE_CARD
case to this policy.A
moveType
ofMoveType.SEVEN_FACE_CARD
requires acardId
which is a valid card id. IfcardId
is present in req.body and can be found inutils/DeckIds.json
, then we canreturn res.next()
in this switch case. Otherwise we should returnres.badRequest()
with an error that explains whetherreq.body.cardId
attribute is missing or invalid.Implementing
validate()
Create a new
api/helpers/gamestate/moves/seven-face-card/validate.js
helper file that matches the general setup of the existingapi/helpers/gamestate/moves/face-card/validate.js
helper. It should throw errors if the requested move is illegal. It should have three inputs:currentState
- the unpackedGameState
from before the move is maderequestedMove
-req body describing the move being made3.playedBy
- 0 or 1 for whether p0 or p1 is making the move.The general idea is that the move is legal if it's your turn, we're resolving a seven, the card you picked can be played as a face card, and the card you picked is in the top two cards of the deck.
validate()
should throw if:throw new Error('game.snackbar.global.notYourTurn');
)phase !== GamePhase.RESOLVING_SEVEN
(throw new Error('game.snackbar.seven.pickAndPlay');
)playedCard
is not in the top two cards of the deck (throw new Error('game.snackbar.seven.pickAndPlay');
)playedCard
is not a queen, king, or 8 (throw new Error('game.snackbar.faceCard.withoutTarget');
)Implementing
execute()
Create a new
api/helpers/gamestate/moves/seven-face-card/execute.js
helper file that matches the general approach of the existing execute helperapi/helpers/gamestate/moves/face-card/execute.js
. It should create and return a newGameState
that is the result of making thesevenFaceCard
move for the requested player i.e. it should remove theplayedCard
from the deck and put it into the requesting player'sfaceCards
. TheplayedBy
andplayedCard
attributes should be included in the returnedGameState
. The GameState'sphase
should be set toGamePhase.MAIN
, the turn should be incremented, the oneOff should be put into the scrap. The seven which was theoneOff
should also be set as theresolved
attribute of theGameState
.execute()
should have 3 inputs:currentState
- the unpackedGameState
from before the move is maderequestedMove
-req body describing the move being madeplayedBy
- 0 or 1 for whether p0 or p1 is making the move.Unskipping Tests
Because we are approaching the
GameState API project with a feature-flag approach, all changes are merged into the
mainbranch and the application and tests are set to dynamically run in either the GameState API mode, or the legacy mode, based on the
VITE_USE_GAMESTATE_API` env variable. In CI, we run the full suite of tests against the legacy version of the application and then run a subset of the tests against the GameState API version of the application.Which tests are run in the GameState API version of CI is controlled via the test command
cy.skipOnGameStateApi()
which programmatically skips a test if the env varVITE_USE_GAMESTATE_API
is enabled. As we develop new features in the GameState API, we should remove thecy.skipOnGameStateApi()
command from more and more of the tests.Essential tests For this PR, at minimum, we should unskip the following tests, which should be able to pass once it is working correctly. All the following tests are in the
tests/e2e/specs/in-game/one-offs/7_sevens
folder:playerSevens.spec.js
-it('Plays king from a seven')
playerSevens.spec.js
-it('Plays queen from a seven')
opponentSevens.spec.js
-it('Opponent plays king from seven (Top Card)')
opponentSevens.spec.js
-it('Opponent plays queen from seven (Second Card)')
opponentSevens.spec.js
-it('Opponent plays eight as glasses from seven (top card)')
You can run the tests against the GameState API locally by: booting the server in gamestate mode with
npm run start:server:gamestate
Then booting the client in gamestate mode with
npm run start:client:gamestate
running the tests in gamestate mode with
npm run e2e:client:gamestate
.