WFTDA/bouttime

View on GitHub
app/scripts/models/game_state.coffee

Summary

Maintainability
Test Coverage
moment = require 'moment'
$ = require 'jquery'
_ = require 'underscore'
Promise = require 'bluebird'
functions = require '../functions'
AppDispatcher = require '../dispatcher/app_dispatcher'
Store = require './store'
{ClockManager} = require '../clock'
Team = require './team'
Pass = require './pass'
Jam = require './jam'
Skater = require './skater'
GameMetadata = require './game_metadata'
{ActionTypes} = require '../constants'
constants = require '../constants'
PERIOD_CLOCK_SETTINGS =
  time: constants.PERIOD_DURATION_IN_MS
  isRunning: true
PREGAME_CLOCK_SETTINGS =
  time: constants.PREGAME_DURATION_IN_MS
HALFTIME_CLOCK_SETTINGS =
  time: constants.HALFTIME_DURATION_IN_MS
JAM_CLOCK_SETTINGS =
  time: constants.JAM_DURATION_IN_MS
  warningTime: constants.JAM_WARNING_IN_MS
  isRunning: true
LINEUP_CLOCK_SETTINGS =
  time: constants.LINEUP_DURATION_IN_MS
  isRunning: true
OVERTIME_CLOCK_SETTINGS =
  time: constants.OVERTIME_DURATION_IN_MS
  isRunning: true
TIMEOUT_CLOCK_SETTINGS =
  time: 0
  tickUp: true
  isRunning: true
class GameState extends Store
  @dispatchToken: AppDispatcher.register (action) =>
    switch action.type
      when ActionTypes.START_CLOCK
        @find(action.gameId).then (game) ->
          game.syncClocks(action)
          game.startClock()
          game.save()
      when ActionTypes.STOP_CLOCK
        @find(action.gameId).then (game) ->
          game.syncClocks(action)
          game.stopClock()
          game.save()
      when ActionTypes.START_JAM
        @find(action.gameId).tap (game) ->
          game.syncClocks(action)
          game.startJam()
        .tap (game) ->
          Promise.join game.save(), game.home.save(), game.away.save()
      when ActionTypes.STOP_JAM
        @find(action.gameId).then (game) ->
          game.syncClocks(action)
          game.stopJam()
          game.save()
      when ActionTypes.START_LINEUP
        @find(action.gameId).tap (game) ->
          game.syncClocks(action)
          game.startLineup()
          Promise.join game.save(), game.home.save(), game.away.save()
      when ActionTypes.START_PREGAME
        @find(action.gameId).then (game) ->
          game.syncClocks(action)
          game.startPregame()
          game.save()
      when ActionTypes.START_HALFTIME
        @find(action.gameId).then (game) ->
          game.syncClocks(action)
          game.startHalftime()
          game.save()
      when ActionTypes.START_UNOFFICIAL_FINAL
        @find(action.gameId).then (game) ->
          game.syncClocks(action)
          game.startUnofficialFinal()
          game.save()
      when ActionTypes.START_OFFICIAL_FINAL
        @find(action.gameId).then (game) ->
          game.syncClocks(action)
          game.startOfficialFinal()
          game.save()
      when ActionTypes.START_OVERTIME
        @find(action.gameId).then (game) ->
          game.syncClocks(action)
          game.startOvertime()
          game.save()
      when ActionTypes.START_TIMEOUT
        @find(action.gameId).then (game) ->
          game.syncClocks(action)
          game.startTimeout()
          game.save()
      when ActionTypes.SET_TIMEOUT_AS_OFFICIAL_TIMEOUT
        @find(action.gameId).tap (game) ->
          game.setTimeoutAsOfficialTimeout()
          Promise.join(game.save(), game.home.save(), game.away.save())
      when ActionTypes.SET_TIMEOUT_AS_HOME_TEAM_TIMEOUT
        @find(action.gameId).tap (game) ->
          game.setTimeoutAsHomeTeamTimeout()
          Promise.join(game.save(), game.home.save(), game.away.save())
      when ActionTypes.SET_TIMEOUT_AS_HOME_TEAM_OFFICIAL_REVIEW
        @find(action.gameId).tap (game) ->
          game.setTimeoutAsHomeTeamOfficialReview()
          Promise.join(game.save(), game.home.save(), game.away.save())
      when ActionTypes.SET_TIMEOUT_AS_AWAY_TEAM_TIMEOUT
        @find(action.gameId).tap (game) ->
          game.setTimeoutAsAwayTeamTimeout()
          Promise.join(game.save(), game.home.save(), game.away.save())
      when ActionTypes.SET_TIMEOUT_AS_AWAY_TEAM_OFFICIAL_REVIEW
        @find(action.gameId).tap (game) ->
          game.setTimeoutAsAwayTeamOfficialReview()
          Promise.join(game.save(), game.home.save(), game.away.save())
      when ActionTypes.SET_JAM_ENDED_BY_TIME
        @find(action.gameId).then (game) ->
          game.setJamEndedByTime()
          game.save()
      when ActionTypes.HANDLE_CLOCK_EXPIRATION
        @find(action.gameId).then (game) ->
          game.handleClockExpiration()
          game.save()
      when ActionTypes.SET_JAM_CLOCK
        @find(action.gameId).then (game) ->
          game.setJamClock(action.value)
          game.save()
      when ActionTypes.SET_PERIOD_CLOCK
        @find(action.gameId).then (game) ->
          game.setPeriodClock(action.value)
          game.save()
      when ActionTypes.SET_HOME_TEAM_TIMEOUTS
        @find(action.gameId).tap (game) ->
          game.setHomeTeamTimeouts(action.value)
          Promise.join game.save(), game.home.save()
      when ActionTypes.SET_AWAY_TEAM_TIMEOUTS
        @find(action.gameId).tap (game) ->
          game.setAwayTeamTimeouts(action.value)
          Promise.join game.save(), game.away.save()
      when ActionTypes.SET_PERIOD
        @find(action.gameId).then (game) ->
          game.setPeriod(action.value)
          game.save()
      when ActionTypes.SET_JAM_NUMBER
        @find(action.gameId).then (game) ->
          game.setJamNumber(action.value)
          game.save()
      when ActionTypes.REMOVE_HOME_TEAM_OFFICIAL_REVIEW
        @find(action.gameId).tap (game) ->
          game.removeHomeTeamOfficialReview()
          Promise.join game.save(), game.home.save()
      when ActionTypes.REMOVE_AWAY_TEAM_OFFICIAL_REVIEW
        @find(action.gameId).tap (game) ->
          game.removeAwayTeamOfficialReview()
          Promise.join game.save(), game.away.save()
      when ActionTypes.RESTORE_HOME_TEAM_OFFICIAL_REVIEW
        @find(action.gameId).tap (game) ->
          game.restoreHomeTeamOfficialReview()
          Promise.join game.save(), game.home.save()
      when ActionTypes.RESTORE_AWAY_TEAM_OFFICIAL_REVIEW
        @find(action.gameId).tap (game) ->
          game.restoreAwayTeamOfficialReview()
          Promise.join game.save(), game.away.save()
      when ActionTypes.JAM_TIMER_UNDO
        @find(action.gameId).tap (game) ->
          game.undo()
          game.save()
      when ActionTypes.JAM_TIMER_REDO
        @find(action.gameId).tap (game) ->
          game.redo()
          game.save()
      when ActionTypes.SAVE_GAME
        game = new this(action.gameState)
        game.syncClocks(action.gameState)
        game.save(true)
      when ActionTypes.SET_PENALTY
        dispatch = AppDispatcher.waitFor [Team.dispatchToken, Skater.dispatchToken]
        game = dispatch.spread (team, skater) =>
          @findBy $or: [{'away.id': team.id}, {'home.id': team.id}]
        .get(0)
        Promise.join game, dispatch, (game, dispatch) ->
          [team, skater] = dispatch
          game.pushFeed type: 'penalty', penalty: action.penalty, skater: skater, style: team.colorBarStyle
          game.save()
      when ActionTypes.TOGGLE_LEAD
        dispatch = AppDispatcher.waitFor [Team.dispatchToken, Pass.dispatchToken]
        game = dispatch.spread (team) =>
          @findBy $or: [{'away.id': team.id}, {'home.id': team.id}]
        .get(0)
        Promise.join game, dispatch, (game, dispatch) ->
          [team, pass] = dispatch
          game.pushFeed type: 'lead', skater:pass.jammer, style: team.colorBarStyle
          game.save()
      when ActionTypes.TOGGLE_LOST_LEAD
        dispatch = AppDispatcher.waitFor [Team.dispatchToken, Pass.dispatchToken]
        game = dispatch.spread (team) =>
          @findBy $or: [{'away.id': team.id}, {'home.id': team.id}]
        .get(0)
        Promise.join game, dispatch, (game, dispatch) ->
          [team, pass] = dispatch
          game.pushFeed type: 'lost lead', skater:pass.jammer, style: team.colorBarStyle
          game.save()
      when ActionTypes.TOGGLE_CALLOFF
        dispatch = AppDispatcher.waitFor [Team.dispatchToken, Pass.dispatchToken]
        game = dispatch.spread (team) =>
          @findBy $or: [{'away.id': team.id}, {'home.id': team.id}]
        .get(0)
        Promise.join game, dispatch, (game, dispatch) ->
          [team, pass] = dispatch
          game.pushFeed type: 'calloff', skater: pass.jammer, style: team.colorBarStyle
          game.save()
      when ActionTypes.SET_POINTS
        dispatch = AppDispatcher.waitFor [Team.dispatchToken, Jam.dispatchToken, Pass.dispatchToken]
        game = dispatch.spread (team, pass) =>
          @findBy $or: [{'away.id': team.id}, {'home.id': team.id}]
        .get(0)
        Promise.join game, dispatch, (game, dispatch) ->
          [team, jam, pass] = dispatch
          game.pushFeed type: 'points', jam: jam, pass: pass, style: team.colorBarStyle
          game.save()
  constructor: (options={}) ->
    super options
    @name = options.name
    @venue = options.venue
    @date = options.date
    @time = options.time
    @officials = options.officials ? []
    @debug = options.debug ? false
    @state = options.state ? 'pregame'
    @period = options.period ? 'pregame'
    @jamNumber = options.jamNumber ? 0
    @clockManager = new ClockManager()
    @jamClock = @clockManager.getOrAddClock "jamClock-#{@id}", options.jamClock ? PREGAME_CLOCK_SETTINGS
    @periodClock = @clockManager.getOrAddClock "periodClock-#{@id}", options.periodClock ? PERIOD_CLOCK_SETTINGS
    @timeout = options.timeout ? null
    @home = new Team(options.home)
    @away = new Team(options.away)
    @penalties = [
      {code: "A", name: "High Block"},
      {code: "N", name: "Insubordination"},
      {code: "B", name: "Back Block"},
      {code: "S", name: "Skating Out of Bnds."},
      {code: "E", name: "Elbows"},
      {code: "X", name: "Cutting the Track"},
      {code: "F", name: "Forearms"},
      {code: "Z", name: "Delay of Game"},
      {code: "G", name: "Misconduct"},
      {code: "C", name: "Dir. of Game Play"},
      {code: "H", name: "Blocking with Head"},
      {code: "O", name: "Out of Bounds"},
      {code: "L", name: "Low Block"},
      {code: "P", name: "Out of Play"},
      {code: "M", name: "Multi-Player Block"},
      {code: "I", name: "Illegal Procedure"},
      {code: "G", name: "Gross Misconduct", egregious: true}
    ]
    @feed = options.feed ? []
    @ads = options.ads ? []
    @_undoStack = options._undoStack ? []
    @_redoStack = options._redoStack ? []
  load: (options={}) ->
    home = Team.findOrCreate(@home).then (home) =>
      @home = home
    away = Team.findOrCreate(@away).then (away) =>
      @away = away
    Promise.join(home, away).return(this)
  save: (cascade=false) ->
    promise = super()
    if cascade
      promise = promise.then () =>
        Promise.join(@home.save(true), @away.save(true))
      .return this
    promise
  getDisplayName: () ->
    "#{moment(@date, 'MM/DD/YYYY').format('YYYY-MM-DD')} #{@home.name} vs #{@away.name}"
  getCurrentJam: (team) ->
    (jam for jam in team.jams when jam.jamNumber is @jamNumber)[0]
  syncClocks: (clocks) ->
    @jamClock.sync(clocks.jamClock)
    @periodClock.sync(clocks.periodClock)
    if clocks.sourceDelay? and clocks.destinationDelay?
      delta = (clocks.sourceDelay + clocks.destinationDelay) / 2.0
      @jamClock.tick(delta)
      @periodClock.tick(delta)
  startClock: ()->
    @_pushUndo()
    @periodClock.reset() #Dummy reset
    @jamClock.start()
  stopClock: () ->
    @_pushUndo()
    @periodClock.reset() #Dummy reset
    @jamClock.stop()
  advancePeriod: () ->
    if @period is "pregame"
      @period = "period 1"
      @periodClock.reset(PERIOD_CLOCK_SETTINGS)
    else if @period is "halftime"
      @period = "period 2"
      @periodClock.reset(PERIOD_CLOCK_SETTINGS)
    else
      @periodClock.start()
  startJam: () ->
    @_clearTimeouts()
    @_pushUndo()
    @jamClock.reset(JAM_CLOCK_SETTINGS)
    @advancePeriod()
    isOvertime = @state is 'overtime'
    @state = "jam"
    @jamNumber = @jamNumber + 1
    @pushFeed type: 'jam start', jamNumber: @jamNumber
    home = @home.createJamsThrough(@jamNumber, isOvertime)
    away = @away.createJamsThrough(@jamNumber, isOvertime)
    Promise.join home, away
  stopJam: () =>
    @startLineup()
  startLineup: () ->
    @_clearTimeouts()
    @_pushUndo()
    @periodClock.reset() #Dummy reset
    @jamClock.reset(LINEUP_CLOCK_SETTINGS)
    @state = "lineup"
  startPregame: () ->
    @_pushUndo()
    @periodClock.reset(time: 0)
    @jamClock.reset(PREGAME_CLOCK_SETTINGS)
    @state = "pregame"
    @period = "pregame"
  startHalftime: () ->
    @_pushUndo()
    @periodClock.reset(time: 0)
    @jamClock.reset(HALFTIME_CLOCK_SETTINGS)
    @state = "halftime"
    @period = "halftime"
  startUnofficialFinal: () ->
    @_pushUndo()
    @periodClock.reset() #Dummy reset
    @jamClock.reset() #Dummy reset
    @state = "unofficial final"
    @period = "unofficial final"
  startOfficialFinal: () ->
    @_pushUndo()
    @periodClock.reset() #Dummy reset
    @jamClock.reset() #Dummy reset
    @state = "official final"
    @period = "official final"
  startOvertime: () ->
    @_pushUndo()
    @periodClock.reset()
    @jamClock.reset(OVERTIME_CLOCK_SETTINGS)
    @state = "overtime"
  startTimeout: () ->
    @_pushUndo()
    @periodClock.stop()
    @jamClock.reset(TIMEOUT_CLOCK_SETTINGS)
    @state = "timeout"
    @timeout = "timeout"
    @pushFeed type: 'timeout', body: 'Timeout'
  setTimeoutAsOfficialTimeout: () ->
    if @_inTimeout() == false
      @startTimeout()
    @_clearTimeouts()
    @_clearUndo()
    @timeout = "official timeout"
    @modifyFeed type: 'timeout', body: 'Official Timeout', style: null
  setTimeoutAsHomeTeamTimeout: () ->
    if @_inTimeout() == false
      @startTimeout()
    @_clearTimeouts()
    @_clearUndo()
    @timeout = "timeout"
    @home.startTimeout()
    @modifyFeed type: 'timeout', body: 'Timeout', style: @home.colorBarStyle
  setTimeoutAsHomeTeamOfficialReview: () ->
    if @_inTimeout() == false
      @startTimeout()
    @_clearTimeouts()
    @_clearUndo()
    @home.hasOfficialReview = false
    @home.isTakingOfficialReview = true
    @timeout = "official review"
    @modifyFeed type: 'timeout', body: 'Official Review', style: @home.colorBarStyle
  setTimeoutAsAwayTeamTimeout: () ->
    if @_inTimeout() == false
      @startTimeout()
    @_clearTimeouts()
    @_clearUndo()
    @timeout = "timeout"
    @away.startTimeout()
    @modifyFeed type: 'timeout', body: 'Timeout', style: @away.colorBarStyle
  setTimeoutAsAwayTeamOfficialReview: () ->
    if @_inTimeout() == false
      @startTimeout()
    @_clearTimeouts()
    @_clearUndo()
    @away.hasOfficialReview = false
    @away.isTakingOfficialReview = true
    @state = "timeout"
    @timeout = "official review"
    @modifyFeed type: 'timeout', body: 'Official Review', style: @away.colorBarStyle
  handleClockExpiration: () ->
    if @state == "jam"
      #Mark as jam ended by time
      @stopJam()
  setJamClock: (val) ->
    @_pushUndo()
    @periodClock.reset() #Dummy reset
    @jamClock.reset(_.extend(@jamClock, time: val))
  setPeriodClock: (val) ->
    @_pushUndo()
    @jamClock.reset() #Dummy reset
    @periodClock.reset(_.extend(@periodClock, time: val))
  setHomeTeamTimeouts: (val) ->
    @home.timeouts = parseInt(val)
  setAwayTeamTimeouts: (val) ->
    @away.timeouts = parseInt(val)
  setPeriod: (val) ->
    @_pushUndo()
    @periodClock.reset() #Dummy reset
    @jamClock.reset() #Dummy reset
    @period = val
  setJamNumber: (val) ->
    @_pushUndo()
    @periodClock.reset() #Dummy reset
    @jamClock.reset() #Dummy reset
    @jamNumber = parseInt(val)
  removeHomeTeamOfficialReview: () ->
    @home.removeOfficialReview()
  removeAwayTeamOfficialReview: () ->
    @away.removeOfficialReview()
  restoreHomeTeamOfficialReview: () ->
    @_clearTimeouts()
    @home.restoreOfficialReview()
  restoreAwayTeamOfficialReview: () ->
    @_clearTimeouts()
    @away.restoreOfficialReview()
  getMetadata: () ->
    new GameMetadata
      id: @id
      display: @getDisplayName()
  undo: () ->
    @_popUndo()
    @jamClock.undo()
    @periodClock.undo()
  redo: () ->
    @_popRedo()
    @jamClock.redo()
    @periodClock.redo()
  isUndoable: () ->
    @_undoStack.length > 0 and @jamClock.isUndoable() and @periodClock.isUndoable()
  isRedoable: () ->
    @_redoStack.length > 0 and @jamClock.isRedoable() and @periodClock.isRedoable()
  pushFeed: (opts={}) ->
    entry = id: functions.uniqueId()
    @feed.unshift _.extend entry, opts
  modifyFeed: (opts) ->
    matches = @feed.filter (e) -> e.type is opts.type
    match = matches[0]
    if match?
      _.extend(match, opts)
  _inTimeout: ()->
      @state == "timeout"
  _clearAlerts: () ->
    @_clearTimeouts()
  _clearTimeouts: () ->
    @home.isTakingTimeout = false
    @away.isTakingTimeout = false
    @home.isTakingOfficialReview = false
    @away.isTakingOfficialReview = false
  _historyFrame: () ->
    state: @state
    period: @period
    jamNumber: @jamNumber
  _pushFrame: (stack) ->
    stack.push @_historyFrame()
  _popFrame: (stack) ->
    for key, value of stack.pop()
      @[key] = value
  _pushUndo: () ->
    @_clearRedo()
    @_pushFrame @_undoStack
  _popUndo: () ->
    @_pushRedo()
    @_popFrame @_undoStack
  _clearUndo: () ->
    @_clearRedo()
    @_undoStack = []
    @jamClock.clearUndo()
    @periodClock.clearUndo()
  _pushRedo: () ->
    @_pushFrame @_redoStack
  _popRedo: () ->
    @_pushFrame @_undoStack
    @_popFrame @_redoStack
  _clearRedo: () ->
    @_redoStack = []
    @jamClock.clearRedo()
    @periodClock.clearRedo()
module.exports = GameState