bensaufley/code-words-api

View on GitHub
models/game.js

Summary

Maintainability
A
1 hr
Test Coverage
'use strict';

const Sequelize = require('sequelize'),
      config = require('../config'),
      sequelizeInstance = config.sequelize,
      GameBoard = require('../lib/game-board');

class Game extends Sequelize.Model {
  completed() {
    let lastTurn = this.turns[this.turns.length - 1];
    if (!lastTurn) return false;
    return lastTurn.event === 'end';
  }

  decode(i) {
    if (i < 0 || i > 24) return Promise.reject(new Error('No such tile'));
    let turn,
        board = this.getDataValue('board');
    if (board[i].revealed) return Promise.reject(new Error('Tile already revealed'));

    if (!this.activePlayerId) return Promise.reject(new Error('Game has not begun'));

    return this.getActivePlayer()
      .then((activePlayer) => {
        if (activePlayer.role !== 'decoder') throw new Error('Active Player cannot make guesses');

        this.activePlayer = activePlayer;

        turn = {
          timestamp: new Date().getTime(),
          event: 'decoding',
          playerId: activePlayer.id,
          tile: i,
          correct: board[i].type === activePlayer.team
        };

        let turns = [...this.turns, turn];

        board[i].revealed = true;

        return this.update({ board, turns });
      }).then(() => {
        if (board[i].type === 'x' || this.board.won()) return this.end(turn, this.activePlayer);
        else if (!turn.correct) return this.nextTurn();
        return this;
      });
  }

  delete() {
    return sequelizeInstance.transaction((transaction) => {
      let deletedAt = new Date();
      return this.getPlayers()
        .then((players) => {
          return Promise.all([
            players.map((player) => player.update({ deletedAt }, { transaction })),
            this.update({ deletedAt }, { transaction })
          ]);
        });
    });
  }

  end(turn, activePlayer) {
    let finalTurn = {
      timestamp: new Date().getTime(),
      event: 'end'
    };
    if (turn.correct) {
      finalTurn.winner = activePlayer.team;
    } else {
      finalTurn.winner = activePlayer.team === 'a' ? 'b' : 'a';
    }
    return this.update({ turns: [...this.turns, finalTurn] });
  }

  nextTurn() {
    if (!this.activePlayerId) return Promise.reject(new Error('Game is not started'));
    if (this.completed()) return Promise.reject(new Error('Game is over'));

    const activePlayer = this.players.find((p) => p.id === this.activePlayerId);

    const otherTeam = activePlayer.team === 'a' ? 'b' : 'a',
          otherRole = activePlayer.role === 'transmitter' ? 'decoder' : 'transmitter',
          newTeam = otherRole === 'transmitter' ? otherTeam : activePlayer.team,
          nextPlayer = this.players.find((p) => p.team === newTeam && p.role === otherRole);

    return this.update({ activePlayerId: nextPlayer.id });
  }

  serializeFor(player) {
    return {
      id: this.id,
      updatedAt: this.updated_at,
      activePlayerId: this.activePlayerId,
      board: this.board.serialize(!this.activePlayerId || player.role !== 'transmitter'),
      completed: this.completed(),
      started: !!this.activePlayerId,
      turns: this.turns
    };
  }

  rematch() {
    if (!this.completed()) return Promise.reject(new Error('Cannot rematch incomplete game'));

    return Game.create({
      players: this.players.map((p) => ({
        userId: p.userId,
        team: p.team === 'a' ? 'b' : 'a',
        role: p.role === 'transmitter' ? 'decoder' : 'transmitter'
      }))
    }, {
      include: [{
        association: Game.Players,
        include: [Game.Players.User]
      }]
    });
  }

  start() {
    if (this.activePlayerId) return Promise.reject(new Error('Game has already been started'));
    return this.getPlayers()
      .then((players) => {
        if (players.length < 4) return Promise.reject(new Error('Not enough players'));
        if (players.some((p) => !p.role || !p.team)) return Promise.reject(new Error('All players must have roles and teams'));

        let activePlayer = players.find((player) => {
          return player.team === this.board.startingTeam() &&
            player.role === 'transmitter';
        });
        return this.update({ activePlayerId: activePlayer.id });
      });
  }

  transmit(word, number) {
    return new Promise((resolve, reject) => {
      if (!this.activePlayerId) reject(new Error('Game has not begun'));
      else if (!word || /\s+/.test(word)) reject(new Error('Transmission must be one single word'));
      else if (isNaN(number) || Math.round(number) !== number || number < 1 || number > 8) reject(new Error('Number must be whole number greater than zero'));
      else resolve();
    })
      .then(() => this.getActivePlayer())
      .then((activePlayer) => {
        this.activePlayer = activePlayer;
        if (number > this.board.teamTiles()[activePlayer.team].filter((t) => !t.revealed).length) throw new Error('Number exceeds remaining tiles for team.');
        const turn = {
          timestamp: new Date().getTime(),
          event: 'transmission',
          number: number,
          playerId: activePlayer.id,
          word: word
        };
        return this.update({ turns: [...this.turns, turn] });
      })
      .then(() => this.nextTurn());
  }

  static createForUser(user) {
    return this.create({
      players: [{ userId: user.id }]
    }, {
      include: [{
        association: Game.Players,
        include: [Game.Players.User]
      }]
    });
  }
}

Game.init({
  id: {
    type: Sequelize.UUID,
    defaultValue: Sequelize.UUIDV4,
    primaryKey: true
  },
  board: {
    type: Sequelize.JSONB,
    defaultValue: () => new GameBoard().tiles,
    get() {
      return new GameBoard(this.getDataValue('board'));
    }
  },
  turns: {
    type: Sequelize.JSONB,
    defaultValue: []
  },
  activePlayerId: {
    field: 'active_player_id',
    type: Sequelize.UUID,
    notNull: false
  },
  deletedAt: {
    field: 'deleted_at',
    type: Sequelize.DATE,
    notNull: false
  }
}, {
  defaultScope: {
    where: { deletedAt: null }
  },
  sequelize: sequelizeInstance,
  scopes: {
    deleted: {
      where: {
        $not: { deletedAt: null }
      }
    }
  },
  tableName: 'games',
  underscored: true
});

module.exports = Game;