Repository: cuttle-cards/cuttle (js)

Issue:
[Feature]: Create `emitGameState()` and `checkGameStateForWIn()`helpers
### Feature Summary

We need to create the `emitGameState()` helper to input a `GameState` object and to emit a socket event matching the same data structure that is currently sent in production

### Detailed Description

The new socket event should match the shape of the ones already emitted by the move making apis:

```
     Game.publish([fullGame.id], {
       change: 'points',
       game: PopulatedGame,
       victory,
     });

```

This function should also check if the game is over and if so, update the `Game`'s `status` and `winner` columns accordingly, then blast a `gameFinished` event like: `sails.sockets.blast('gameFinished', { gameId: game.id })`.

Here is an example `PopulatedGame`:
```
{
  players: [
    {
      hand: [
        {
          createdAt: "2024-06-13T23:23:58.331Z",
          updatedAt: "2024-06-13T23:23:58.458Z",
          id: 8,
          suit: 0,
          rank: 8,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 2,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
        {
          createdAt: "2024-06-13T23:23:58.337Z",
          updatedAt: "2024-06-13T23:23:58.458Z",
          id: 12,
          suit: 0,
          rank: 12,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 2,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
        {
          createdAt: "2024-06-13T23:23:58.366Z",
          updatedAt: "2024-06-13T23:23:58.458Z",
          id: 33,
          suit: 2,
          rank: 7,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 2,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
        {
          createdAt: "2024-06-13T23:23:58.377Z",
          updatedAt: "2024-06-13T23:24:00.813Z",
          id: 45,
          suit: 3,
          rank: 6,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 2,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
        {
          createdAt: "2024-06-13T23:23:58.380Z",
          updatedAt: "2024-06-13T23:23:58.458Z",
          id: 48,
          suit: 3,
          rank: 9,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 2,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
        {
          createdAt: "2024-06-13T23:23:58.383Z",
          updatedAt: "2024-06-13T23:23:58.458Z",
          id: 52,
          suit: 3,
          rank: 13,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 2,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
      ],
      points: [
      ],
      faceCards: [
      ],
      createdAt: "2024-06-13T23:23:57.440Z",
      updatedAt: "2024-06-13T23:24:00.816Z",
      id: 2,
      username: "definitelyNotTheGovernment6969",
      pNum: 0,
      rank: 1000,
      game: 1,
      frozenId: null,
    },
    {
      hand: [
        {
          createdAt: "2024-06-13T23:23:58.327Z",
          updatedAt: "2024-06-13T23:23:58.460Z",
          id: 5,
          suit: 0,
          rank: 5,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 1,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
        {
          createdAt: "2024-06-13T23:23:58.348Z",
          updatedAt: "2024-06-13T23:23:58.460Z",
          id: 19,
          suit: 1,
          rank: 6,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 1,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
        {
          createdAt: "2024-06-13T23:23:58.368Z",
          updatedAt: "2024-06-13T23:23:58.460Z",
          id: 36,
          suit: 2,
          rank: 10,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 1,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
        {
          createdAt: "2024-06-13T23:23:58.373Z",
          updatedAt: "2024-06-13T23:23:58.460Z",
          id: 41,
          suit: 3,
          rank: 2,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 1,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
        {
          createdAt: "2024-06-13T23:23:58.375Z",
          updatedAt: "2024-06-13T23:23:58.460Z",
          id: 43,
          suit: 3,
          rank: 4,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 1,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
        {
          createdAt: "2024-06-13T23:23:58.381Z",
          updatedAt: "2024-06-13T23:23:58.460Z",
          id: 50,
          suit: 3,
          rank: 11,
          index: 0,
          deck: null,
          scrap: null,
          twos: null,
          hand: 1,
          points: null,
          faceCards: null,
          attachedTo: null,
          targeted: null,
        },
      ],
      points: [
      ],
      faceCards: [
      ],
      createdAt: "2024-06-13T23:23:55.601Z",
      updatedAt: "2024-06-13T23:23:57.851Z",
      id: 1,
      username: "myUsername",
      pNum: 1,
      rank: 1000,
      game: 1,
      frozenId: null,
    },
  ],
  deck: [
    {
      createdAt: "2024-06-13T23:23:58.318Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 1,
      suit: 0,
      rank: 1,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.320Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 2,
      suit: 0,
      rank: 2,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.321Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 3,
      suit: 0,
      rank: 3,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.326Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 4,
      suit: 0,
      rank: 4,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.328Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 6,
      suit: 0,
      rank: 6,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.330Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 7,
      suit: 0,
      rank: 7,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.332Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 9,
      suit: 0,
      rank: 9,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.334Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 10,
      suit: 0,
      rank: 10,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.335Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 11,
      suit: 0,
      rank: 11,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.338Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 13,
      suit: 0,
      rank: 13,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.340Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 14,
      suit: 1,
      rank: 1,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.341Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 15,
      suit: 1,
      rank: 2,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.343Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 16,
      suit: 1,
      rank: 3,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.344Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 17,
      suit: 1,
      rank: 4,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.346Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 18,
      suit: 1,
      rank: 5,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.350Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 20,
      suit: 1,
      rank: 7,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.351Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 21,
      suit: 1,
      rank: 8,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.353Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 22,
      suit: 1,
      rank: 9,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.354Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 23,
      suit: 1,
      rank: 10,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.356Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 24,
      suit: 1,
      rank: 11,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.357Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 25,
      suit: 1,
      rank: 12,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.358Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 26,
      suit: 1,
      rank: 13,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.360Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 27,
      suit: 2,
      rank: 1,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.361Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 28,
      suit: 2,
      rank: 2,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.362Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 29,
      suit: 2,
      rank: 3,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.363Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 30,
      suit: 2,
      rank: 4,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.364Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 31,
      suit: 2,
      rank: 5,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.365Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 32,
      suit: 2,
      rank: 6,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.368Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 35,
      suit: 2,
      rank: 9,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.369Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 37,
      suit: 2,
      rank: 11,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.370Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 38,
      suit: 2,
      rank: 12,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.371Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 39,
      suit: 2,
      rank: 13,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.372Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 40,
      suit: 3,
      rank: 1,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.374Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 42,
      suit: 3,
      rank: 3,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.378Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 46,
      suit: 3,
      rank: 7,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.379Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 47,
      suit: 3,
      rank: 8,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.380Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 49,
      suit: 3,
      rank: 10,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
    {
      createdAt: "2024-06-13T23:23:58.382Z",
      updatedAt: "2024-06-13T23:23:58.462Z",
      id: 51,
      suit: 3,
      rank: 12,
      index: 0,
      deck: 1,
      scrap: null,
      twos: null,
      hand: null,
      points: null,
      faceCards: null,
      attachedTo: null,
      targeted: null,
    },
  ],
  scrap: [
  ],
  twos: [
  ],
  spectatingUsers: [
  ],
  createdAt: "2024-06-13T23:23:56.663Z",
  updatedAt: "2024-06-13T23:24:00.815Z",
  id: 1,
  name: "Test Game",
  status: 2,
  p0Ready: true,
  p1Ready: true,
  p0Rematch: null,
  p1Rematch: null,
  passes: 0,
  oneOffTargetType: "",
  turn: 1,
  log: [
    "definitelyNotTheGovernment6969 drew a card",
  ],
  turnStalemateWasRequestedByP0: -1,
  turnStalemateWasRequestedByP1: -1,
  chat: [
  ],
  isRanked: false,
  lastEvent: {
    change: "draw",
  },
  lock: null,
  lockedAt: null,
  rematchGame: null,
  topCard: {
    createdAt: "2024-06-13T23:23:58.377Z",
    updatedAt: "2024-06-13T23:23:58.455Z",
    id: 44,
    suit: 3,
    rank: 5,
    index: 0,
    deck: null,
    scrap: null,
    twos: null,
    hand: null,
    points: null,
    faceCards: null,
    attachedTo: null,
    targeted: null,
  },
  secondCard: {
    createdAt: "2024-06-13T23:23:58.367Z",
    updatedAt: "2024-06-13T23:24:00.814Z",
    id: 34,
    suit: 2,
    rank: 8,
    index: 0,
    deck: null,
    scrap: null,
    twos: null,
    hand: null,
    points: null,
    faceCards: null,
    attachedTo: null,
    targeted: null,
  },
  oneOff: null,
  resolving: null,
  oneOffTarget: null,
  attachedToTarget: null,
  p0: 2,
  p1: 1,
  match: null,
  winner: null,
}
```

And here is an example `victory`:
```
    const res = {
      gameOver: true,
      winner: 137, // user id of the winner
      conceded: false,
      currentMatch: {}, // Match object
    };
```

Pull request description:
Feature #943 create emit game state
<!-- Thanks for contributing to Cuttle! 🎉 -->

## Issue number

Relevant [issue number](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
- Resolves #943 

## Please check the following

- [x] Do the tests still pass? (see [Run the Tests](https://github.com/cuttle-cards/cuttle/blob/main/docs/setup-development.md#run-the-tests))
- [x] Is the code formatted properly? (see [Linting (Formatting)](https://github.com/cuttle-cards/cuttle/blob/main/docs/setup-development.md#linting-formatting))
- For New Features:
  - [ ] Have tests been added to cover any new features or fixes?
  - [ ] Has the documentation been updated accordingly?

## Please describe additional details for testing this change


Patch:
diff --git a/api/helpers/gamestate/check-game-state-for-win.js b/api/helpers/gamestate/check-game-state-for-win.js
new file mode 100644
index 00000000..1a012be7
--- /dev/null
+++ b/api/helpers/gamestate/check-game-state-for-win.js
@@ -0,0 +1,76 @@
+const GameStatus = require('../../../utils/GameStatus.json');
+
+module.exports = {
+  friendlyName: 'Check Game State for win',
+
+  description: 'Determines if the game has winner and updates game status and winner if so',
+
+  inputs: {
+    game: {
+      type: 'ref',
+      description: 'Game object',
+      required: true,
+    },
+    gameState: {
+      type: 'ref',
+      description: 'GameState object',
+      required: true,
+    },
+  },
+
+  fn: async function ({ game, gameState}, exits) {
+    
+    const checkWin = (pNum) => {
+      const player = pNum ? gameState.p1 : gameState.p0;
+      const points = player.points?.reduce((sum, { rank }) => sum + rank, 0);
+      const kings = player.faceCards?.filter((faceCard) => faceCard.rank === 13).length;
+      switch (kings) {
+        case 0:
+          return points >= 21;
+        case 1:
+          return points >= 14;
+        case 2:
+          return points >= 10;
+        case 3:
+          return points >= 5;
+        case 4:
+          return points >= 0;
+      }
+      return false;
+    };
+
+    const res = {
+      gameOver: false,
+      winner: null,
+      conceded: false,
+      currentMatch: null,
+    };
+
+    const p0Wins = checkWin(0);
+    const p1Wins = checkWin(1);
+
+    if (p0Wins || p1Wins) {
+      res.gameOver = true;
+      const gameUpdates = {};
+      gameUpdates.status = GameStatus.FINISHED;
+
+      if (p0Wins) {
+        res.winner = 0;
+        gameUpdates.winner = game.p0.id;
+      } else if (p1Wins) {
+        res.winner = 1;
+        gameUpdates.winner = game.p1.id;
+      }
+
+      await Game.updateOne({ id: game.id }).set(gameUpdates);
+
+      game = {
+        ...game,
+        ...gameUpdates,
+      };
+
+      res.currentMatch = await sails.helpers.addGameToMatch(game);
+    }
+    return exits.success(res);
+  },
+};
diff --git a/api/helpers/gamestate/convert-card-to-str.js b/api/helpers/gamestate/convert-card-to-str.js
index 71d186dc..2cce02d2 100644
--- a/api/helpers/gamestate/convert-card-to-str.js
+++ b/api/helpers/gamestate/convert-card-to-str.js
@@ -1,44 +1,43 @@
- /**
+/**
  * Convert the attachments attribute of a card object to its string representation
  *
  * @param { Card } card - the card object to convert
  * @param { 'p0' | 'p1' } cardOwner - which player owns the card
  * @returns { String } string representation of the card atatchements attributs
  */
- function concatAttachtToId(card, cardOwner) {
-    if (!card.attachments || !card.attachments.length) {
-        return '';
-    }
+function concatAttachtToId(card, cardOwner) {
+  if (!card.attachments || !card.attachments.length) {
+    return '';
+  }
 
-    //GameStateRow Format => (JH-p0,JC-p1,JD-p0)
+  //GameStateRow Format => (JH-p0,JC-p1,JD-p0)
 
-    let opponent = cardOwner === 'p0' ? 'p1' : 'p0';
-    // last player on the string owns the main card
-    // => if card.attachments length is an odd number, start string construction with the opponent
-    let playerNum = card.attachments.length % 2 == 0 ? opponent : cardOwner;
+  let opponent = cardOwner === 'p0' ? 'p1' : 'p0';
+  // First player on the string owns the main card
+  // => if card.attachments length is an odd number, start string construction with the opponent
+  let playerNum = card.attachments.length % 2 === 0 ? cardOwner : opponent;
 
-    const convertedCards = card.attachments.map((card) => {
-      const cardInString = validateCardId(card.id) + '-' + playerNum;
-      playerNum = playerNum === 'p0' ? 'p1' : 'p0';
-      return cardInString;
-    });
+  const convertedCards = card.attachments.map((card) => {
+    const cardInString = validateCardId(card.id) + '-' + playerNum;
+    playerNum = playerNum === 'p0' ? 'p1' : 'p0';
+    return cardInString;
+  });
 
-    return '(' + convertedCards.join(',') + ')';
+  return '(' + convertedCards.join(',') + ')';
 }
 
- /**
+/**
  * Validate card id
  *
  * @param {string  } card,
  */
 function validateCardId(id) {
   if (!id || id.length !== 2) {
-      throw new Error('Invalid card ID');
+    throw new Error('Invalid card ID');
   }
   return id;
 }
 
-
 /**
  * Convert a card object to its string representation
  *
@@ -66,16 +65,13 @@ module.exports = {
   sync: true,
 
   fn: ({ card, playedBy }, exits) => {
+    try {
+      const cardId = validateCardId(card.id);
+      const attachments = concatAttachtToId(card, playedBy);
 
-      try {
-          const cardId = validateCardId(card.id);
-          const attachments = concatAttachtToId(card, playedBy);
-
-          return exits.success(cardId + attachments);
-
-      } catch (err) {
-        return exits.error('Error at unpacking cards: ' + err.message);
-      }
+      return exits.success(cardId + attachments);
+    } catch (err) {
+      return exits.error('Error at unpacking cards: ' + err.message);
     }
+  },
 };
-
diff --git a/api/helpers/gamestate/get-log.js b/api/helpers/gamestate/get-log.js
new file mode 100644
index 00000000..101430e9
--- /dev/null
+++ b/api/helpers/gamestate/get-log.js
@@ -0,0 +1,167 @@
+const MoveType = require('../../../utils/MoveType.json');
+const { getCardName } = require('../../../utils/game-utils');
+const { game: gameText } = require('../../../src/translations/en.json');
+
+module.exports = {
+  friendlyName: 'Get Game Log Messages',
+
+  description: 'Return a game log based on game moves played',
+
+  inputs: {
+    game: {
+      type: 'ref',
+      description: 'Game object, with populated .gameStates',
+      required: true,
+    },
+  },
+  sync: true,
+
+  fn: function ({ game }, exits) {
+    const getMessage = (row, i) => {
+      const { moveType, playedCard, targetCard, resolved, deck, twos, discardedCards } = row;
+      const { convertStrToCard } = sails.helpers.gamestate;
+
+      const getFullCardName = (card) => {
+        const cardObject = convertStrToCard(card);
+        return getCardName(cardObject);
+      };
+
+      const player = game[`p${row.playedBy}`]?.username;
+      const opponent = game[`p${row.playedBy % 2}`]?.username;
+
+      const playedCardName = playedCard ? getFullCardName(playedCard) : null;
+      const targetCardName = targetCard ? getFullCardName(targetCard) : null;
+      const resolvedCardName = resolved ? getFullCardName(resolved) : null;
+
+      const getResolveFiveMessage = () => {
+        const previousRow = game.gameStates[i - 1] ?? null;
+        const amountOfCardsDrawn =
+          row[`p${row.playedBy}Hand`]?.length - previousRow[`p${row.playedBy}Hand`?.length];
+
+        return amountOfCardsDrawn === 1 ? `draws 1 card` : `draws ${amountOfCardsDrawn} cards`;
+      };
+
+      switch (moveType) {
+        case MoveType.DRAW:
+          return `${player} drew a card.`;
+
+        case MoveType.POINTS:
+          return `${player} played the ${playedCardName} for points.`;
+
+        case MoveType.SCUTTLE:
+          return `${player} scuttled ${opponent}'s ${targetCardName} 
+        with the ${playedCardName}.`;
+
+        case MoveType.FACECARD:
+          return `${player} played the ${playedCardName}
+        ${playedCard.rank === 8 ? ' as a glasses eight.' : '.'}`;
+
+        case MoveType.JACK:
+          return `${player} stole ${opponent}'s ${targetCardName} with the ${playedCardName}.`;
+
+        case MoveType.UNTARGETED_ONE_OFF:
+          return `${player} played the ${playedCardName} as a one-off to 
+        ${gameText.moves.effects[playedCard.rank]}.`;
+
+        case MoveType.TARGETED_ONE_OFF:
+          return `${player} played the ${playedCardName} as a one-off to: ${
+            gameText.moves.effects[playedCard.rank]
+          }, targeting the ${targetCardName}.`;
+
+        case MoveType.COUNTER:
+          if (twos.length > 0) {
+            return `${player} played the ${playedCardName} to counter ${opponent}'s 
+            ${getFullCardName(twos[twos.length - 1])}.`;
+          }
+          return `${player} played the ${playedCardName} to counter 
+        ${opponent}'s ${resolvedCardName}.`;
+
+        case MoveType.FIZZLE:
+          return `The ${getFullCardName(
+            game.oneOff,
+          )} is countered, and all cards played this turn are scrapped.`;
+
+        case MoveType.RESOLVE:
+          switch (convertStrToCard(resolved).rank) {
+            case 1:
+              return `The ${resolvedCardName} one-off resolves; all point cards are scrapped.`;
+            case 2:
+              return `The ${resolvedCardName} resolves; the ${targetCardName} is scrapped.`;
+            case 3:
+              return `The ${resolvedCardName} one-off resolves; ${player} will draw one card of their choice from the Scrap pile.`;
+            case 4:
+              return `The ${resolvedCardName} one-off resolves; ${opponent} must discard two cards.`;
+            case 5:
+              return `The ${resolvedCardName} one-off resolves; ${player} must discard 1 card, and will draw up to 3.`;
+            case 6:
+              return `The ${resolvedCardName} one-off resolves; all Royals and Glasses are scrapped.`;
+
+            case 7:
+              if (deck.length < 2) {
+                return `The ${resolvedCardName} one-off resolves. They will play the ${getFullCardName(
+                  deck[0],
+                )} as it is the last card in the deck.`;
+              }
+              return `The ${resolvedCardName} one-off resolves; they will play one card from the top two in the deck. Top two cards are the ${getFullCardName(
+                deck[0],
+              )} and ${getFullCardName(deck[1])}.`;
+
+            case 9:
+              return `The ${resolvedCardName} one-off resolves, returning the ${targetCardName} to ${opponent}'s hand. It cannot be played next turn.`;
+          }
+          break;
+
+        case MoveType.RESOLVE_THREE:
+          return `${player} took the ${targetCardName} from the Scrap pile to their hand.`;
+
+        case MoveType.RESOLVE_FOUR:
+          return `${player} discarded the ${getFullCardName(discardedCards[0])} ${
+            discardedCards.length > 1 ? `and the ${getFullCardName(discardedCards[1])}` : '.'
+          }`;
+
+        case MoveType.RESOLVE_FIVE:
+          if (discardedCards.length) {
+            return `${player} discards the ${getFullCardName(
+              discardedCards[0],
+            )} and ${getResolveFiveMessage()}`;
+          }
+          return `${player} ${getResolveFiveMessage()}`;
+
+        case MoveType.SEVEN_POINTS:
+          return `${player} played the ${playedCardName} from the top of the deck for points.`;
+
+        case MoveType.SEVEN_SCUTTLE:
+          return `${player} scuttled ${opponent}'s ${targetCardName} with the ${playedCardName} from the top of the deck.`;
+
+        case MoveType.SEVEN_FACECARD:
+          return `${player} played the ${playedCardName} from the top of the deck ${
+            playedCard.rank === 8 ? ' as a Glasses eight.' : '.'
+          }`;
+
+        case MoveType.SEVEN_JACK:
+          if (!targetCard) {
+            return `${player} scrapped ${playedCardName}, since there are no point cards to steal on ${opponent}'s field.`;
+          }
+          return `${player} stole ${opponent}'s ${targetCardName} with the ${playedCardName} from the top of the deck.`;
+
+        case MoveType.SEVEN_UNTARGETED_ONE_OFF:
+          return `${player} played the ${playedCardName} from the top of the deck as a one-off to ${
+            gameText.moves.effects[playedCard.rank]
+          }.`;
+
+        case MoveType.SEVEN_TARGETED_ONE_OFF:
+          return `${player} played the ${playedCardName} from the top of the deck as a one-off to ${
+            gameText.moves.effects[playedCard.rank]
+          }, targeting the ${targetCardName}.`;
+
+        case MoveType.PASS:
+          return `${player} passes.`;
+      }
+    };
+
+    const fullLog = game.gameStates.map((row, i) => {
+      return getMessage(row, i);
+    });
+    return exits.success(fullLog);
+  },
+};
diff --git a/api/helpers/gamestate/pack-gamestate.js b/api/helpers/gamestate/pack-gamestate.js
index 2d740f38..db0a597b 100644
--- a/api/helpers/gamestate/pack-gamestate.js
+++ b/api/helpers/gamestate/pack-gamestate.js
@@ -1,7 +1,8 @@
 module.exports = {
   friendlyName: 'pack GameState',
 
-  description: 'Transforms a GameState into a GameStateRow. Converts the object representations of cards to the strings, and separates the p0 and p1 Player objects into the p0Hand, p0Points, and p0FaceCards attributes (doing the same for p1).',
+  description:
+    'Transforms a GameState into a GameStateRow. Converts the object representations of cards to the strings, and separates the p0 and p1 Player objects into the p0Hand, p0Points, and p0FaceCards attributes (doing the same for p1).',
 
   inputs: {
     gameState: {
@@ -13,64 +14,69 @@ module.exports = {
   sync: true,
 
   fn: ({ gameState }, exits) => {
+    try {
+      const convertedData = {};
 
-      try {
-        const convertedData = {};
+      const attributesToConvert = [
+        'deck',
+        'scrap',
+        'playedCard',
+        'targetCard',
+        'oneOff',
+        'oneOffTarget',
+        'twos',
+        'resolved',
+        'discardedCards',
+      ];
 
-        const attributesToConvert = [
-          'deck', 'scrap', 'playedCard', 'targetCard', 'oneOff', 'oneOffTarget', 'twos', 'resolving',
-          'discardedCards'
-        ];
+      attributesToConvert.forEach((attribute) => {
+        const value = gameState[attribute];
+        if (value) {
+          const { convertCardToStr } = sails.helpers.gamestate;
+          convertedData[attribute] = Array.isArray(value)
+            ? value.map((card) => convertCardToStr(card))
+            : convertCardToStr(value);
+        } else {
+          convertedData[attribute] = null; // Handle null or undefined attributes
+        }
+      });
 
-        attributesToConvert.forEach(attribute => {
-            const value = gameState[attribute];
-            if (value) {
-                const { convertCardToStr } = sails.helpers.gamestate;
-                convertedData[attribute] = Array.isArray(value) ?
-                                              value.map(card => convertCardToStr(card))
-                                              : convertCardToStr(value);
-            }
-            else {
-              convertedData[attribute] = null;  // Handle null or undefined attributes
-            }
-        });
+      // Correspondance of attribute name in GameStateRow to attribute name in GameState + player
+      const playerAttToConvert = [
+        { rowName: 'p0Hand', gamestateName: 'hand', player: 'p0' },
+        { rowName: 'p1Hand', gamestateName: 'hand', player: 'p1' },
+        { rowName: 'p0Points', gamestateName: 'points', player: 'p0' },
+        { rowName: 'p1Points', gamestateName: 'points', player: 'p1' },
+        { rowName: 'p0FaceCards', gamestateName: 'faceCards', player: 'p0' },
+        { rowName: 'p1FaceCards', gamestateName: 'faceCards', player: 'p1' },
+      ];
 
-        // Correspondance of attribute name in GameStateRow to attribute name in GameState + player
-        const playerAttToConvert = [
-                                    { rowName: 'p0Hand', gamestateName: 'hand', player: 'p0'},
-                                    { rowName: 'p1Hand', gamestateName: 'hand', player: 'p1'},
-                                    { rowName: 'p0Points', gamestateName: 'points', player: 'p0'},
-                                    { rowName: 'p1Points', gamestateName: 'points', player: 'p1'},
-                                    { rowName: 'p0FaceCards', gamestateName: 'faceCards', player: 'p0'},
-                                    { rowName: 'p1FaceCards', gamestateName: 'faceCards', player: 'p1'}
-                                  ];
+      playerAttToConvert.forEach((attribute) => {
+        // ex GameState format for p0Hand: gamestate.p0.hand
+        const value = gameState[attribute.player][attribute.gamestateName];
 
-        playerAttToConvert.forEach(attribute => {
-           // ex GameState format for p0Hand: gamestate.p0.hand
-          const value = gameState[attribute.player][attribute.gamestateName];
-
-          if (value) {
-              // For the points attribute, the cards owner is needed to convert attachement induced by Jacks
-              // => 8D(JH-p0,JC-p1,JD-p0)
-              if (attribute.gamestateName === 'points') {
-                  convertedData[attribute.rowName] = value.map(card =>
-                    sails.helpers.gamestate.convertCardToStr(card, attribute.player));
-              }
-              else {
-                convertedData[attribute.rowName] = value.map(card => sails.helpers.gamestate.convertCardToStr(card));
-              }
-          }
-          else {
-            convertedData[attribute.rowName] = [];
+        if (value) {
+          // For the points attribute, the cards owner is needed to convert attachement induced by Jacks
+          // => 8D(JH-p0,JC-p1,JD-p0)
+          if (attribute.gamestateName === 'points') {
+            convertedData[attribute.rowName] = value.map((card) =>
+              sails.helpers.gamestate.convertCardToStr(card, attribute.player),
+            );
+          } else {
+            convertedData[attribute.rowName] = value.map((card) =>
+              sails.helpers.gamestate.convertCardToStr(card),
+            );
           }
-        });
+        } else {
+          convertedData[attribute.rowName] = [];
+        }
+      });
 
-        const combinedData = {...gameState, ...convertedData };
+      const combinedData = { ...gameState, ...convertedData };
 
-        return exits.success(combinedData);
+      return exits.success(combinedData);
     } catch (err) {
-        return exits.error(err.message);
+      return exits.error(err.message);
     }
-  }
+  },
 };
-
diff --git a/api/helpers/gamestate/publish-game-state.js b/api/helpers/gamestate/publish-game-state.js
new file mode 100644
index 00000000..533a439f
--- /dev/null
+++ b/api/helpers/gamestate/publish-game-state.js
@@ -0,0 +1,130 @@
+const MoveType = require('../../../utils/MoveType.json');
+
+module.exports = {
+  friendlyName: 'Publish Game State',
+
+  description: 'Determines if the game has ended and sends a socket event based on the game status.',
+
+  inputs: {
+    game: {
+      type: 'ref',
+      description: 'Game overview object',
+      required: true,
+    },
+    gameState: {
+      type: 'ref',
+      description: 'Game state object',
+      required: true,
+    },
+  },
+
+  fn: async function ({ game, gameState }, exits) {
+    try {
+      //Combine game and gamestate users and delete passwords
+      const p0 = { ...game.p0, ...gameState.p0 };
+      delete p0.encryptedPassword;
+      const p1 = { ...game.p1, ...gameState.p1 };
+      delete p1.encryptedPassword;
+      const players = [p0, p1];
+
+      const lastEventTargetType = () => {
+        if (!['targetedOneOff', 'sevenTargetedOneOff'].includes(gameState.moveType)) {
+          return '';
+        }
+        if (gameState.targetCard?.rank === 11) {
+          return 'jack';
+        }
+        if (gameState.targetCard?.rank > 11) {
+          return 'faceCard';
+        }
+        return 'point';
+      };
+
+      const victory = await sails.helpers.gamestate.checkGameStateForWin(game, gameState);
+
+      const countPasses = () => {
+        let numPasses = 0;
+        for (const gameState of game.gameStates.slice(-3)) {
+          if (gameState.moveType !== MoveType.PASS) {
+            return numPasses;
+          }
+          numPasses++;
+        }
+        return numPasses;
+      };
+
+      const fullLog = sails.helpers.gamestate.getLog(game);
+
+      //Last Event, and Extra Socket Variables
+      const happened = gameState.moveType !== MoveType.FIZZLE;
+      const { playedBy } = gameState;
+      const discardedCards = gameState.discardedCards.length ? gameState.discardedCards : null;
+      const chosenCard = gameState.moveType === MoveType.RESOLVE_THREE ? gameState.targetCard : null;
+      const pNum = playedBy;
+
+      const socketGame = {
+        players,
+        id: game.id,
+        createdAt: game.createdAt,
+        updatedAt: gameState.createdAt,
+        name: game.name,
+        chat: game.chat,
+        status: game.status,
+        p0Ready: game.p0Ready,
+        p1Ready: game.p1Ready,
+        p0Rematch: game.p0Rematch,
+        p1Rematch: game.p1Rematch,
+        turnStalemateWasRequestedByP0: game.turnStalemateWasRequestedByP0,
+        turnStalemateWasRequestedByP1: game.turnStalemateWasRequestedByP1,
+        lock: game.lock,
+        lockedAt: game.lockedAt,
+        rematchGame: game.rematchGame,
+        spectatingUsers: game.spectatingUsers,
+        isRanked: game.isRanked,
+        winner: victory.winner,
+        match: victory.currentMatch,
+        log: fullLog,
+        passes: countPasses(),
+        turn: gameState.turn,
+        deck: gameState.deck.slice(2),
+        scrap: gameState.scrap,
+        topCard: gameState.deck[0],
+        secondCard: gameState.deck[1],
+        twos: gameState.twos,
+        resolved: gameState.resolved,
+        oneOffTargetType: lastEventTargetType(),
+        lastEvent: {
+          change: gameState.moveType,
+          oneOffTargetType: lastEventTargetType(),
+          chosenCard,
+          pNum,
+          happened,
+          discardedCards,
+          oneOff: gameState.resolved,
+        },
+      };
+
+      const fullSocketEvent = {
+        change: gameState.moveType,
+        game: socketGame,
+        victory,
+        happened,
+        discardedCards,
+        chosenCard,
+        playedBy,
+        pNum,
+        oneOff: gameState.resolved,
+      };
+
+      Game.publish([game.id], fullSocketEvent);
+
+      if (victory.gameOver) {
+        sails.sockets.blast('gameFinished', { gameId: game.id });
+      }
+
+      return exits.success(fullSocketEvent);
+    } catch (err) {
+      return exits.error(`Error emitting socket: ${err.message}`);
+    }
+  },
+};
diff --git a/api/helpers/gamestate/unpack-gamestate.js b/api/helpers/gamestate/unpack-gamestate.js
index 43ac3133..f08fd419 100644
--- a/api/helpers/gamestate/unpack-gamestate.js
+++ b/api/helpers/gamestate/unpack-gamestate.js
@@ -3,7 +3,8 @@ const MoveType = require('../../../utils/MoveType.json');
 module.exports = {
   friendlyName: 'unpack GameState',
 
-  description: 'Transforms a GameStateRow into a GameState. Converts the String representations of cards to Card objects, and aggregates the p0Hand, p0Points, and p0FaceCards attributes into the p0 and p1 Player objects.',
+  description:
+    'Transforms a GameStateRow into a GameState. Converts the String representations of cards to Card objects, and aggregates the p0Hand, p0Points, and p0FaceCards attributes into the p0 and p1 Player objects.',
 
   inputs: {
     gameStateRow: {
@@ -14,70 +15,81 @@ module.exports = {
   },
   sync: true,
 
-  fn: ({ gameStateRow  }, exits) => {
+  fn: ({ gameStateRow }, exits) => {
     try {
-
       const attributesToConvert = [
-        'deck', 'scrap', 'playedCard', 'targetCard', 'oneOff', 'oneOffTarget', 'twos', 'resolving',
-        'discardedCards', 'p0Hand', 'p1Hand', 'p0Points', 'p1Points', 'p0FaceCards', 'p1FaceCards',
+        'deck',
+        'scrap',
+        'playedCard',
+        'targetCard',
+        'oneOff',
+        'oneOffTarget',
+        'twos',
+        'resolved',
+        'discardedCards',
+        'p0Hand',
+        'p1Hand',
+        'p0Points',
+        'p1Points',
+        'p0FaceCards',
+        'p1FaceCards',
       ];
 
       const convertedData = {};
-      attributesToConvert.forEach(attribute => {
+      attributesToConvert.forEach((attribute) => {
         const value = gameStateRow[attribute];
         if (value) {
           const { convertStrToCard } = sails.helpers.gamestate;
-          convertedData[attribute] = Array.isArray(value) ?
-                                        value.map(cardStr => {
-                                          // Is the card frozen
-                                          const freezeCard = gameStateRow.moveType === MoveType.RESOLVE
-                                                              && gameStateRow.oneOff[0] === 9
-                                                              && gameStateRow.oneOffTarget === cardStr.substring(0,2);
-                                          return convertStrToCard(cardStr, freezeCard);
-                                        })
-                                        : convertStrToCard(value);
+          convertedData[attribute] = Array.isArray(value)
+            ? value.map((cardStr) => {
+                // Is the card frozen
+                const freezeCard =
+                  gameStateRow.moveType === MoveType.RESOLVE &&
+                  gameStateRow.resolved[0] === '9' &&
+                  gameStateRow.targetCard === cardStr.substring(0, 3);
+                return convertStrToCard(cardStr, freezeCard);
+              })
+            : convertStrToCard(value);
         }
       });
 
       const p0 = {
-                    hand: convertedData.p0Hand,
-                    faceCards: convertedData.p0FaceCards,
-                    points: convertedData.p0Points
-                  };
+        hand: convertedData.p0Hand,
+        faceCards: convertedData.p0FaceCards,
+        points: convertedData.p0Points,
+      };
 
       const p1 = {
-                    hand: convertedData.p1Hand,
-                    faceCards: convertedData.p1FaceCards,
-                    points: convertedData.p1Points
-                  };
-
+        hand: convertedData.p1Hand,
+        faceCards: convertedData.p1FaceCards,
+        points: convertedData.p1Points,
+      };
 
       const data = {
-                      gameId: gameStateRow.gameId,
-                      playedBy: gameStateRow.playedBy,
-                      moveType: gameStateRow.moveType,
-                      turn: gameStateRow.turn,
-                      phase: gameStateRow.phase,
+        gameId: gameStateRow.gameId,
+        playedBy: gameStateRow.playedBy,
+        moveType: gameStateRow.moveType,
+        turn: gameStateRow.turn,
+        phase: gameStateRow.phase,
 
-                      p0,
-                      p1,
-                      deck: convertedData.deck,
-                      scrap: convertedData.scrap,
-                      twos: convertedData.twos,
-                      playedCard: convertedData.playedCard ?? null,
-                      targetCard: convertedData.targetCard ?? null,
-                      oneOff: convertedData.oneOff ?? null,
-                      oneOffTarget: convertedData.oneOffTarget ?? null,
-                      resolving: convertedData.resolving ?? null,
-                      discardedCards: convertedData.discardedCards ?? null,
-                    };
+        p0,
+        p1,
+        deck: convertedData.deck,
+        scrap: convertedData.scrap,
+        twos: convertedData.twos,
+        playedCard: convertedData.playedCard ?? null,
+        targetCard: convertedData.targetCard ?? null,
+        oneOff: convertedData.oneOff ?? null,
+        oneOffTarget: convertedData.oneOffTarget ?? null,
+        resolved: convertedData.resolved ?? null,
+        discardedCards: convertedData.discardedCards ?? null,
+      };
 
-      const convertedGameState = sails.helpers.gamestate.validateGamestate( data );
+      const convertedGameState = sails.helpers.gamestate.validateGamestate(data);
 
       return exits.success(convertedGameState);
-  } catch (err) {
-        return exits.error(err.message);
-  }
-
-}
+    } catch (err) {
+      return exits.error(err.message);
+    }
+  },
 };
diff --git a/api/helpers/gamestate/validate-gamestate.js b/api/helpers/gamestate/validate-gamestate.js
index 6489500b..23b1c8ed 100644
--- a/api/helpers/gamestate/validate-gamestate.js
+++ b/api/helpers/gamestate/validate-gamestate.js
@@ -84,7 +84,7 @@ module.exports = {
         targetCard: gameState.targetCard ?? null,
         oneOff: gameState.oneOff ?? null,
         oneOffTarget: gameState.oneOffTarget ?? null,
-        resolving: gameState.resolving ?? null,
+        resolved: gameState.resolved ?? null,
       };
 
       return exits.success(gameStateUpdated);
diff --git a/api/models/GameStateRow.js b/api/models/GameStateRow.js
index f3030bf3..14c7f98e 100644
--- a/api/models/GameStateRow.js
+++ b/api/models/GameStateRow.js
@@ -138,8 +138,8 @@ module.exports = {
       columnType: 'text[]',
       required: true,
     },
-    // Resolving card
-    resolving: {
+    // Resolved card
+    resolved: {
       type: 'string',
       allowNull: true,
     },
diff --git a/src/routes/game/GameView.vue b/src/routes/game/GameView.vue
index 97d9a65e..bc916877 100644
--- a/src/routes/game/GameView.vue
+++ b/src/routes/game/GameView.vue
@@ -357,7 +357,7 @@
                       :suit="card.suit"
                       :rank="card.rank"
                       :is-selected="selectedCard && card.id === selectedCard.id"
-                      :is-frozen="gameStore.player.frozenId === card.id"
+                      :is-frozen="gameStore.player.frozenId === card.id || card.isFrozen"
                       class="mt-2 transition-all"
                       :is-hand-card="true"
                       :data-player-hand-card="`${card.rank}-${card.suit}`"
@@ -373,7 +373,7 @@
                   :suit="card.suit"
                   :rank="card.rank"
                   :is-selected="selectedCard && card.id === selectedCard.id"
-                  :is-frozen="gameStore.player.frozenId === card.id"
+                  :is-frozen="gameStore.player.frozenId === card.id || card.isFrozen"
                   class="mt-2 transition-all"
                   :is-hand-card="true"
                   :data-player-hand-card="`${card.rank}-${card.suit}`"
diff --git a/src/routes/game/components/MoveChoiceOverlay.vue b/src/routes/game/components/MoveChoiceOverlay.vue
index fc0ff4ab..86e35e37 100644
--- a/src/routes/game/components/MoveChoiceOverlay.vue
+++ b/src/routes/game/components/MoveChoiceOverlay.vue
@@ -28,7 +28,7 @@
         :suit="selectedCard.suit"
         :rank="selectedCard.rank"
         :data-player-overlay-card="`${selectedCard.rank}-${selectedCard.suit}`"
-        :is-frozen="frozenId === selectedCard.id"
+        :is-frozen="frozenId === selectedCard.id || selectedCard.isFrozen"
       />
     </div>
     <!-- Move choices -->


Test patch:
diff --git a/tests/unit/fixtures/Game.js b/tests/unit/fixtures/Game.js
new file mode 100644
index 00000000..56a35451
--- /dev/null
+++ b/tests/unit/fixtures/Game.js
@@ -0,0 +1,46 @@
+import GameStatus from '../../../utils/GameStatus.json';
+
+export const gameFixture = {
+  id: 1,
+  name: 'Test Game',
+  status: GameStatus.STARTED,
+  createdAt: '2024-07-14T12:59:46.870Z',
+  updatedAt: '2024-07-14T12:59:52.829Z',
+  chat: [],
+  p0: {
+    createdAt: '2024-07-14T12:59:45.813Z',
+    updatedAt: '2024-07-14T12:59:51.528Z',
+    id: 17,
+    username: 'myUsername',
+    encryptedPassword: 'password',
+    pNum: 0,
+    rank: 1000,
+    game: 1,
+    frozenId: null,
+  },
+  p1: {
+    createdAt: '2024-07-14T12:59:47.709Z',
+    updatedAt: '2024-07-14T12:59:51.769Z',
+    id: 18,
+    username: 'definitelyNotTheGovernment6969',
+    encryptedPassword: 'password',
+    pNum: 1,
+    rank: 1000,
+    game: 1,
+    frozenId: null,
+  },
+  turnStalemateWasRequestedByP0: -1,
+  turnStalemateWasRequestedByP1: -1,
+  spectatingUsers: [],
+  p0Ready: true,
+  p1Ready: true,
+  p0Rematch: null,
+  p1Rematch: null,
+  isRanked: false,
+  lock: null,
+  lockedAt: null,
+  rematchGame: null,
+  gameStates: [],
+  winner: null,
+  match: null,
+};
diff --git a/tests/unit/fixtures/gameState.js b/tests/unit/fixtures/gameState.js
deleted file mode 100644
index 044b62b3..00000000
--- a/tests/unit/fixtures/gameState.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Card } from '../fixtures/Card';
-const fixture = {
-  gameState: {
-    p0: {
-      hand: [Card.ACE_OF_SPADES],
-      points: [
-        Card.TEN_OF_SPADES,
-        {
-          ...Card.EIGHT_OF_DIAMONDS,
-          attachments: [Card.JACK_OF_CLUBS, Card.JACK_OF_SPADES],
-        },
-      ],
-      faceCards: [Card.KING_OF_SPADES],
-    },
-    p1: {
-      hand: [Card.ACE_OF_HEARTS, Card.ACE_OF_DIAMONDS],
-      points: [
-        Card.TEN_OF_HEARTS,
-        {
-          ...Card.ACE_OF_CLUBS,
-          attachments: [Card.JACK_OF_SPADES],
-        },
-      ],
-      faceCards: [],
-    },
-    deck: [Card.ACE_OF_SPADES, Card.ACE_OF_CLUBS, Card.KING_OF_HEARTS],
-    scrap: [],
-    twos: [],
-    discardedCards: [],
-    oneOff: null,
-    oneOffTarget: null,
-    resolving: null,
-    playedCard: Card.TEN_OF_CLUBS,
-    gameId: 1,
-    playedBy: 1,
-    moveType: 'scuttle',
-    turn: 3,
-    phase: 2,
-    targetCard: null,
-  },
-  gameStateRow: {
-    p0Hand: ['AS'],
-    p0Points: ['TS', '8D(JC-p1,JS-p0)'],
-    p0FaceCards: ['KS'],
-    p1Hand: ['AH', 'AD'],
-    p1Points: ['TH', 'AC(JS-p1)'],
-    p1FaceCards: [],
-    deck: ['AS', 'AC', 'KH'],
-    scrap: [],
-    twos: [],
-    discardedCards: [],
-    playedCard: 'TC',
-    oneOff: null,
-    oneOffTarget: null,
-    resolving: null,
-    gameId: 1,
-    playedBy: 1,
-    moveType: 'scuttle',
-    turn: 3,
-    phase: 2,
-    targetCard: null,
-  },
-};
-
-export { fixture };
diff --git a/tests/unit/fixtures/gameStates/points.js b/tests/unit/fixtures/gameStates/points.js
new file mode 100644
index 00000000..406ff0c9
--- /dev/null
+++ b/tests/unit/fixtures/gameStates/points.js
@@ -0,0 +1,214 @@
+import { Card } from '../Card';
+import GamePhase from '../../../../utils/GamePhase.json';
+import MoveType from '../../../../utils/MoveType.json';
+import GameStatus from '../../../../utils/GameStatus.json';
+import { gameFixture } from '../Game';
+
+const gameStateRow = {
+  p0Hand: ['AS'],
+  p0Points: ['TS', '8D(JS-p0,JC-p1)'],
+  p0FaceCards: ['KS'],
+  p1Hand: ['AH', 'AD'],
+  p1Points: ['TH', 'AC'],
+  p1FaceCards: [],
+  deck: ['7S', '7C', 'KH'],
+  scrap: ['6C'],
+  twos: [],
+  discardedCards: [],
+  playedCard: 'TS',
+  oneOff: null,
+  oneOffTarget: null,
+  resolved: null,
+  gameId: 1,
+  playedBy: 0,
+  moveType: MoveType.POINTS,
+  turn: 3,
+  phase: GamePhase.MAIN,
+  targetCard: null,
+};
+
+export const points = {
+  gameState: {
+    p0: {
+      hand: [Card.ACE_OF_SPADES],
+      points: [
+        Card.TEN_OF_SPADES,
+        {
+          ...Card.EIGHT_OF_DIAMONDS,
+          attachments: [Card.JACK_OF_SPADES, Card.JACK_OF_CLUBS],
+        },
+      ],
+      faceCards: [Card.KING_OF_SPADES],
+    },
+    p1: {
+      hand: [Card.ACE_OF_HEARTS, Card.ACE_OF_DIAMONDS],
+      points: [Card.TEN_OF_HEARTS, Card.ACE_OF_CLUBS],
+      faceCards: [],
+    },
+    deck: [Card.SEVEN_OF_SPADES, Card.SEVEN_OF_CLUBS, Card.KING_OF_HEARTS],
+    scrap: [Card.SIX_OF_CLUBS],
+    twos: [],
+    discardedCards: [],
+    oneOff: null,
+    oneOffTarget: null,
+    resolved: null,
+    playedCard: Card.TEN_OF_SPADES,
+    gameId: 1,
+    playedBy: 0,
+    moveType: MoveType.POINTS,
+    turn: 3,
+    phase: GamePhase.MAIN,
+    targetCard: null,
+  },
+
+  gameStateRow: gameStateRow,
+
+  game: {
+    ...gameFixture,
+    gameStates: [gameStateRow],
+    status: GameStatus.FINISHED,
+  },
+
+  socket: {
+    change: MoveType.POINTS,
+    happened: true,
+    oneOff: null,
+    discardedCards: null,
+    playedBy: 0,
+    pNum: 0,
+    chosenCard: null,
+    game: {
+      players: [
+        {
+          hand: [Card.ACE_OF_SPADES],
+          points: [
+            Card.TEN_OF_SPADES,
+            {
+              ...Card.EIGHT_OF_DIAMONDS,
+              attachments: [Card.JACK_OF_SPADES, Card.JACK_OF_CLUBS],
+            },
+          ],
+          faceCards: [Card.KING_OF_SPADES],
+          createdAt: '2024-07-14T12:59:45.813Z',
+          updatedAt: '2024-07-14T12:59:51.528Z',
+          id: 17,
+          username: 'myUsername',
+          pNum: 0,
+          rank: 1000,
+          game: 1,
+          frozenId: null,
+        },
+        {
+          hand: [Card.ACE_OF_HEARTS, Card.ACE_OF_DIAMONDS],
+          points: [Card.TEN_OF_HEARTS, Card.ACE_OF_CLUBS],
+          faceCards: [],
+          createdAt: '2024-07-14T12:59:47.709Z',
+          updatedAt: '2024-07-14T12:59:51.769Z',
+          id: 18,
+          username: 'definitelyNotTheGovernment6969',
+          pNum: 1,
+          rank: 1000,
+          game: 1,
+          frozenId: null,
+        },
+      ],
+      name: 'Test Game',
+      chat: [],
+      status: 3,
+      p0Ready: true,
+      p1Ready: true,
+      p0Rematch: null,
+      p1Rematch: null,
+      turnStalemateWasRequestedByP0: -1,
+      turnStalemateWasRequestedByP1: -1,
+      lock: null,
+      lockedAt: null,
+      rematchGame: null,
+      spectatingUsers: [],
+      isRanked: false,
+      match: {
+        endTime: null,
+        games: [
+          {
+            chat: [],
+            gameStates: [gameStateRow],
+            isRanked: false,
+            lock: null,
+            lockedAt: null,
+            match: null,
+            name: 'Test Game',
+            p0: gameFixture.p0,
+            p0Ready: true,
+            p0Rematch: null,
+            p1: gameFixture.p1,
+            p1Ready: true,
+            p1Rematch: null,
+            rematchGame: null,
+            spectatingUsers: [],
+            status: 3,
+            turnStalemateWasRequestedByP0: -1,
+            turnStalemateWasRequestedByP1: -1,
+            winner: 17,
+          },
+        ],
+        player1: gameFixture.p0,
+        player2: gameFixture.p1,
+        winner: null,
+      },
+      winner: 0,
+      log: ['myUsername played the 10♠️ for points.'],
+      passes: 0,
+      turn: 3,
+      deck: [Card.KING_OF_HEARTS],
+      scrap: [Card.SIX_OF_CLUBS],
+      topCard: Card.SEVEN_OF_SPADES,
+      secondCard: Card.SEVEN_OF_CLUBS,
+      twos: [],
+      resolved: null,
+      oneOffTargetType: '',
+      lastEvent: {
+        change: MoveType.POINTS,
+        oneOff: null,
+        oneOffTargetType: '',
+        chosenCard: null,
+        happened: true,
+        pNum: 0,
+        discardedCards: null,
+      },
+    },
+    victory: {
+      gameOver: true,
+      winner: 0,
+      conceded: false,
+      currentMatch: {
+        endTime: null,
+        games: [
+          {
+            chat: [],
+            gameStates: [gameStateRow],
+            isRanked: false,
+            lock: null,
+            lockedAt: null,
+            match: null,
+            name: 'Test Game',
+            p0: gameFixture.p0,
+            p0Ready: true,
+            p0Rematch: null,
+            p1: gameFixture.p1,
+            p1Ready: true,
+            p1Rematch: null,
+            rematchGame: null,
+            spectatingUsers: [],
+            status: 3,
+            turnStalemateWasRequestedByP0: -1,
+            turnStalemateWasRequestedByP1: -1,
+            winner: 17,
+          },
+        ],
+        player1: gameFixture.p0,
+        player2: gameFixture.p1,
+        winner: null,
+      },
+    },
+  },
+};
diff --git a/tests/unit/fixtures/gameStates/resolveNine.js b/tests/unit/fixtures/gameStates/resolveNine.js
new file mode 100644
index 00000000..d50047c5
--- /dev/null
+++ b/tests/unit/fixtures/gameStates/resolveNine.js
@@ -0,0 +1,143 @@
+import { gameFixture } from '../Game';
+import { Card } from '../../fixtures/Card';
+import MoveType from '../../../../utils/MoveType.json';
+import GamePhase from '../../../../utils/GamePhase.json';
+
+const gameStateRow = {
+  playedBy: 0,
+  moveType: MoveType.RESOLVE,
+  playedCard: null,
+  targetCard: 'JC',
+  discardedCards: [],
+  turn: 3,
+  phase: GamePhase.MAIN,
+  p0Hand: [],
+  p0Points: ['AH', 'TH'],
+  p0FaceCards: [],
+  p1Hand: ['JC'],
+  p1Points: [],
+  p1FaceCards: [],
+  deck: ['TC', '7D'],
+  scrap: ['9C'],
+  oneOff: null,
+  oneOffTarget: null,
+  twos: [],
+  resolved: '9C',
+  gameId: 1,
+};
+
+export const resolveNine = {
+  game: { ...gameFixture, gameStates: [gameStateRow] },
+
+  gameStateRow,
+
+  gameState: {
+    playedBy: 0,
+    moveType: MoveType.RESOLVE,
+    playedCard: null,
+    targetCard: Card.JACK_OF_CLUBS,
+    discardedCards: [],
+    turn: 3,
+    phase: GamePhase.MAIN,
+    p0: {
+      hand: [],
+      points: [Card.ACE_OF_HEARTS, Card.TEN_OF_HEARTS],
+      faceCards: [],
+    },
+    p1: {
+      hand: [{ ...Card.JACK_OF_CLUBS, isFrozen: true }],
+      points: [],
+      faceCards: [],
+    },
+    deck: [Card.TEN_OF_CLUBS, Card.SEVEN_OF_DIAMONDS],
+    scrap: [Card.NINE_OF_CLUBS],
+    oneOff: null,
+    oneOffTarget: null,
+    twos: [],
+    resolved: Card.NINE_OF_CLUBS,
+    gameId: 1,
+  },
+
+  socket: {
+    change: MoveType.RESOLVE,
+    happened: true,
+    oneOff: Card.NINE_OF_CLUBS,
+    discardedCards: null,
+    pNum: 0,
+    playedBy: 0,
+    chosenCard: null,
+    game: {
+      chat: [],
+      deck: [],
+      isRanked: false,
+      lastEvent: {
+        change: MoveType.RESOLVE,
+        happened: true,
+        chosenCard: null,
+        oneOff: Card.NINE_OF_CLUBS,
+        oneOffTargetType: '',
+        discardedCards: null,
+        pNum: 0,
+      },
+      lock: null,
+      lockedAt: null,
+      log: [
+        "The 9♣️ one-off resolves, returning the J♣️ to myUsername's hand. It cannot be played next turn.",
+      ],
+      match: null,
+      name: 'Test Game',
+      oneOffTargetType: '',
+      p0Ready: true,
+      p0Rematch: null,
+      p1Ready: true,
+      p1Rematch: null,
+      passes: 0,
+      players: [
+        {
+          hand: [],
+          points: [Card.ACE_OF_HEARTS, Card.TEN_OF_HEARTS],
+          faceCards: [],
+          createdAt: '2024-07-14T12:59:45.813Z',
+          updatedAt: '2024-07-14T12:59:51.528Z',
+          id: 17,
+          username: 'myUsername',
+          pNum: 0,
+          rank: 1000,
+          game: 1,
+          frozenId: null,
+        },
+        {
+          hand: [{ ...Card.JACK_OF_CLUBS, isFrozen: true }],
+          points: [],
+          faceCards: [],
+          createdAt: '2024-07-14T12:59:47.709Z',
+          updatedAt: '2024-07-14T12:59:51.769Z',
+          id: 18,
+          username: 'definitelyNotTheGovernment6969',
+          pNum: 1,
+          rank: 1000,
+          game: 1,
+          frozenId: null,
+        },
+      ],
+      rematchGame: null,
+      resolved: Card.NINE_OF_CLUBS,
+      scrap: [Card.NINE_OF_CLUBS],
+      secondCard: Card.SEVEN_OF_DIAMONDS,
+      spectatingUsers: [],
+      status: 2,
+      topCard: Card.TEN_OF_CLUBS,
+      turn: 3,
+      turnStalemateWasRequestedByP0: -1,
+      turnStalemateWasRequestedByP1: -1,
+      twos: [],
+      winner: null,
+    },
+    victory: {
+      conceded: false,
+      currentMatch: null,
+      gameOver: false,
+      winner: null,
+    },
+  },
+};
diff --git a/tests/unit/fixtures/gameStates/resolveThree.js b/tests/unit/fixtures/gameStates/resolveThree.js
new file mode 100644
index 00000000..a6feba1a
--- /dev/null
+++ b/tests/unit/fixtures/gameStates/resolveThree.js
@@ -0,0 +1,145 @@
+import MoveType from '../../../../utils/MoveType.json';
+import GamePhase from '../../../../utils/GamePhase.json';
+import GameStatus from '../../../../utils/GameStatus.json';
+import { gameFixture } from '../Game';
+import { Card } from '../Card';
+
+const gameStateRow = {
+  playedBy: 0,
+  moveType: MoveType.RESOLVE_THREE,
+  playedCard: null,
+  targetCard: 'TH',
+  discardedCards: [],
+  turn: 1,
+  phase: GamePhase.MAIN,
+  p0Hand: ['TH'],
+  p0Points: [],
+  p0FaceCards: [],
+  p1Hand: ['TD'],
+  p1Points: ['AH'],
+  p1FaceCards: [],
+  deck: ['6S', '2H', 'AC'],
+  scrap: [],
+  oneOff: null,
+  oneOffTarget: null,
+  twos: [],
+  resolved: '3S',
+  gameId: 1,
+};
+
+export const resolveThree = {
+  gameState: {
+    playedBy: 0,
+    moveType: MoveType.RESOLVE_THREE,
+    playedCard: null,
+    targetCard: Card.TEN_OF_HEARTS,
+    discardedCards: [],
+    turn: 1,
+    phase: GamePhase.MAIN,
+    p0: {
+      hand: [Card.TEN_OF_HEARTS],
+      points: [],
+      faceCards: [],
+    },
+    p1: {
+      hand: [Card.TEN_OF_DIAMONDS],
+      points: [Card.ACE_OF_HEARTS],
+      faceCards: [],
+    },
+    deck: [Card.SIX_OF_SPADES, Card.TWO_OF_HEARTS, Card.ACE_OF_CLUBS],
+    scrap: [],
+    oneOff: null,
+    oneOffTarget: null,
+    twos: [],
+    resolved: Card.THREE_OF_SPADES,
+    gameId: 1,
+  },
+
+  gameStateRow,
+
+  game: {
+    ...gameFixture,
+    gameStates: [gameStateRow],
+  },
+
+  socket: {
+    change: MoveType.RESOLVE_THREE,
+    happened: true,
+    oneOff: Card.THREE_OF_SPADES,
+    discardedCards: null,
+    chosenCard: Card.TEN_OF_HEARTS,
+    playedBy: 0,
+    pNum: 0,
+    game: {
+      players: [
+        {
+          hand: [Card.TEN_OF_HEARTS],
+          points: [],
+          faceCards: [],
+          createdAt: '2024-07-14T12:59:45.813Z',
+          updatedAt: '2024-07-14T12:59:51.528Z',
+          id: 17,
+          username: 'myUsername',
+          pNum: 0,
+          rank: 1000,
+          game: 1,
+          frozenId: null,
+        },
+        {
+          hand: [Card.TEN_OF_DIAMONDS],
+          points: [Card.ACE_OF_HEARTS],
+          faceCards: [],
+          createdAt: '2024-07-14T12:59:47.709Z',
+          updatedAt: '2024-07-14T12:59:51.769Z',
+          id: 18,
+          username: 'definitelyNotTheGovernment6969',
+          pNum: 1,
+          rank: 1000,
+          game: 1,
+          frozenId: null,
+        },
+      ],
+      deck: [Card.ACE_OF_CLUBS],
+      scrap: [],
+      twos: [],
+      spectatingUsers: [],
+      name: 'Test Game',
+      status: GameStatus.STARTED,
+      p0Ready: true,
+      p1Ready: true,
+      p0Rematch: null,
+      p1Rematch: null,
+      rematchGame: null,
+      resolved: Card.THREE_OF_SPADES,
+      passes: 0,
+      oneOffTargetType: '',
+      turn: 1,
+      log: ['myUsername took the 10♥️ from the Scrap pile to their hand.'],
+      turnStalemateWasRequestedByP0: -1,
+      turnStalemateWasRequestedByP1: -1,
+      chat: [],
+      isRanked: false,
+      lastEvent: {
+        change: MoveType.RESOLVE_THREE,
+        pNum: 0,
+        chosenCard: Card.TEN_OF_HEARTS,
+        oneOff: Card.THREE_OF_SPADES,
+        oneOffTargetType: '',
+        discardedCards: null,
+        happened: true,
+      },
+      lock: null,
+      lockedAt: null,
+      match: null,
+      winner: null,
+      topCard: Card.SIX_OF_SPADES,
+      secondCard: Card.TWO_OF_HEARTS,
+    },
+    victory: {
+      conceded: false,
+      currentMatch: null,
+      gameOver: false,
+      winner: null,
+    },
+  },
+};
diff --git a/tests/unit/specs/game-state.spec.js b/tests/unit/specs/game-state.spec.js
index 4a0b65eb..c381c3e5 100644
--- a/tests/unit/specs/game-state.spec.js
+++ b/tests/unit/specs/game-state.spec.js
@@ -1,34 +1,113 @@
-import { describe, it , expect, beforeEach} from 'vitest';
-import { fixture } from '../fixtures/gameState';
+import { describe, it, expect, beforeEach } from 'vitest';
+import { points } from '../fixtures/gameStates/points';
+import { resolveThree } from '../fixtures/gameStates/resolveThree';
+import { resolveNine } from '../fixtures/gameStates/resolveNine';
 
-describe('Coverting GameState\'s and GameStateRow\'s', () => {
+//remove attributes added while creating the entry in the database
+function stripDbAttributes(obj) {
+  const attributesToRemove = ['createdAt', 'id', 'updatedAt'];
+  attributesToRemove.forEach((attr) => delete obj[attr]);
+}
 
+describe("Converting GameState's and GameStateRow's and emitting sockets for Points", () => {
   beforeEach(async () => {
     await sails.helpers.wipeDatabase();
   });
 
   it('Converts GameState to GameStateRow (packing)', async () => {
-    const gameStateRow = await sails.helpers.gamestate.saveGamestate(fixture.gameState);
-
-    //remove attributes added while creating the entry in the database
-    const stripDbAttributes = (obj) => {
-      const attributesToRemove = ['createdAt', 'id', 'updatedAt'];
-      attributesToRemove.forEach(attr => delete obj[attr]);
-    };
+    const gameStateRow = await sails.helpers.gamestate.saveGamestate(points.gameState);
 
     stripDbAttributes(gameStateRow);
-    expect(gameStateRow).toEqual(fixture.gameStateRow);
+    expect(gameStateRow).toEqual(points.gameStateRow);
 
-    const dbGameStateRows = await GameStateRow.find({ gameId : gameStateRow.gameId });
+    const dbGameStateRows = await GameStateRow.find({ gameId: gameStateRow.gameId });
     expect(dbGameStateRows.length).toEqual(1);
 
-    const [ savedRow ] = dbGameStateRows;
+    const [savedRow] = dbGameStateRows;
     stripDbAttributes(savedRow);
-    expect(savedRow).toEqual(fixture.gameStateRow);
+    expect(savedRow).toEqual(points.gameStateRow);
   });
 
   it('Converts GameStateRow to GameState (unpacking)', () => {
-    const gameState =  sails.helpers.gamestate.unpackGamestate(fixture.gameStateRow);
-    expect(fixture.gameState).toEqual(gameState);
+    const gameState = sails.helpers.gamestate.unpackGamestate(points.gameStateRow);
+    expect(points.gameState).toEqual(gameState);
+  });
+
+  it('Creates and Publishes socket event', async () => {
+    const socketEvent = await sails.helpers.gamestate.publishGameState(points.game, points.gameState);
+    stripDbAttributes(socketEvent.game);
+    delete socketEvent.game.match.startTime;
+    delete socketEvent.victory.currentMatch.startTime;
+    socketEvent.victory.currentMatch.games.forEach((game) => stripDbAttributes(game));
+    expect(socketEvent).toEqual(points.socket);
+  });
+});
+
+describe('Converting GameState, and GameStateRow, and Publishing Socket for Resolve Three', () => {
+  beforeEach(async () => {
+    await sails.helpers.wipeDatabase();
+  });
+
+  it('Converts GameState to GameStateRow (packing)', async () => {
+    const gameStateRow = await sails.helpers.gamestate.saveGamestate(resolveThree.gameState);
+    stripDbAttributes(gameStateRow);
+    expect(gameStateRow).toEqual(resolveThree.gameStateRow);
+
+    const dbGameStateRows = await GameStateRow.find({ gameId: gameStateRow.gameId });
+    expect(dbGameStateRows.length).toEqual(1);
+
+    const [savedRow] = dbGameStateRows;
+    stripDbAttributes(savedRow);
+    expect(savedRow).toEqual(resolveThree.gameStateRow);
+  });
+
+  it('Converts GameStateRow to GameState (unpacking)', async () => {
+    const gameState = await sails.helpers.gamestate.unpackGamestate(resolveThree.gameStateRow);
+    stripDbAttributes(gameState);
+    expect(gameState).toEqual(resolveThree.gameState);
+  });
+
+  it('Creates and Publishes socket event', async () => {
+    const socketEvent = await sails.helpers.gamestate.publishGameState(
+      resolveThree.game,
+      resolveThree.gameState,
+    );
+    stripDbAttributes(socketEvent.game);
+    expect(socketEvent).toEqual(resolveThree.socket);
+  });
+});
+
+describe('Converting GameState, and GameStateRow, and Publishing Socket for Resolve Nine on Jack', () => {
+  beforeEach(async () => {
+    await sails.helpers.wipeDatabase();
+  });
+
+  it('Converts GameState to GameStateRow (packing)', async () => {
+    const gameStateRow = await sails.helpers.gamestate.saveGamestate(resolveNine.gameState);
+    stripDbAttributes(gameStateRow);
+    expect(gameStateRow).toEqual(resolveNine.gameStateRow);
+
+    const dbGameStateRows = await GameStateRow.find({ gameId: gameStateRow.gameId });
+    expect(dbGameStateRows.length).toEqual(1);
+
+    const [savedRow] = dbGameStateRows;
+    stripDbAttributes(savedRow);
+    expect(savedRow).toEqual(resolveNine.gameStateRow);
+  });
+
+  it('Converts GameStateRow to GameState (unpacking)', async () => {
+    const gameState = await sails.helpers.gamestate.unpackGamestate(resolveNine.gameStateRow);
+    stripDbAttributes(gameState);
+    expect(gameState).toEqual(resolveNine.gameState);
+  });
+
+  it('Creates and Publishes socket event', async () => {
+    const socketEvent = await sails.helpers.gamestate.publishGameState(
+      resolveNine.game,
+      resolveNine.gameState,
+    );
+
+    stripDbAttributes(socketEvent.game);
+    expect(socketEvent).toEqual(resolveNine.socket);
   });
 });