import pDefer = require ( "p-defer" ) ; import { v4 as uuid } from "uuid" ; import { all , takeEvery , takeLatest , put } from "@redux-saga/core/effects" ; import createSagaMiddleware from "redux-saga" ; import { createStore , combineReducers , applyMiddleware , Store , Reducer } from "redux" ; import { BoardState , mergeBoards , rotatePiecesAboutCenter , createBoardSlice , BoardPiecesState , BoardSlice , BoardSelectors } from "@shoki/board" ; import { battleSagaFactory , startBattle , BattleEvents } from "@creature-chess/battle" ; import { GRID_SIZE , PieceModel , GameOptions } from "@creature-chess/models" ; import { call } from "redux-saga/effects" ; import delay = require ( "delay" ) ; import { PlayerEntity } from "../entities" ; import { PlayerStateSelectors } from "../entities/player" ; import { playerFinishMatchEvent } from "../entities/player/events" ; import { Logger } from "winston" ; interface MatchState { board : BoardState < PieceModel > ; turn : number ; } const turnReducer = ( state = 0 , event ) => ( event . type === BattleEvents . BATTLE_TURN_EVENT ? event . payload . turn : state ) ; export class Match { private store : Store < MatchState > ; private finalBoard ! : BoardState < PieceModel > ; private boardId = uuid ( ) ; private board : BoardSlice < PieceModel > = createBoardSlice < PieceModel > ( this . boardId , GRID_SIZE ) ; private serverFinishedMatch = pDefer ( ) ; private clientFinishedMatchHome = pDefer ( ) ; private clientFinishedMatchAway = pDefer ( ) ; public constructor ( public readonly home , public readonly away , private awayIsClone , private logger , gameOptions ) { this . store = this . createStore ( gameOptions ) ; const mergedBoard = mergeBoards ( this . boardId , home . select ( PlayerStateSelectors . getPlayerBoard ) , away . select ( PlayerStateSelectors . getPlayerBoard ) ) ; const board = { ... mergedBoard , pieces : Object . entries ( mergedBoard . pieces ) . reduce < BoardPiecesState < PieceModel > > ( ( acc , [ id , piece ] ) => ( { ... acc , [ id ] : { ... piece , facingAway : ( piece . ownerId === home . id ) } } ) , { } ) } ; this . store . dispatch ( this . board . commands . setBoardPiecesCommand ( board ) ) ; } public onClientFinishMatch ( playerId ) { if ( playerId === this . home . id ) { this . clientFinishedMatchHome . resolve ( ) ; } else if ( playerId === this . away . id ) { this . clientFinishedMatchAway . resolve ( ) ; } } public getBoardForPlayer ( playerId ) { const { board } = this . store . getState ( ) ; if ( playerId === this . away . id ) { const rotatedBoard = rotatePiecesAboutCenter ( board ) ; const newState = { ... rotatedBoard , pieces : Object . entries ( rotatedBoard . pieces ) . reduce < BoardPiecesState < PieceModel > > ( ( acc , [ id , piece ] ) => ( { ... acc , [ id ] : { ... piece , facingAway : ! piece . facingAway } } ) , { } ) } ; return newState ; } return board ; } public getTurn ( ) { return this . store . getState ( ) . turn ; } public getFinalBoard ( ) { return this . finalBoard ; } public async fight ( battleTimeout ) { this . store . dispatch ( startBattle ( ) ) ; await Promise . race ( [ battleTimeout , Promise . all ( [ this . serverFinishedMatch . promise , this . clientFinishedMatchHome . promise , this . clientFinishedMatchAway . promise ] ) ] ) ; await delay ( 500 ) ; this . finalBoard = this . store . getState ( ) . board ; const survivingPieces = BoardSelectors . getAllPieces ( this . finalBoard ) . filter ( p => p . currentHealth > 0 ) ; const surviving = { home : survivingPieces . filter ( p => p . ownerId === this . home . id ) , away : survivingPieces . filter ( p => p . ownerId === this . away . id ) } ; const homeScore = surviving . home . length ; const awayScore = surviving . away . length ; this . home . put ( playerFinishMatchEvent ( { homeScore , awayScore , isHomePlayer : true } ) ) ; if ( ! this . awayIsClone ) { this . away . put ( playerFinishMatchEvent ( { homeScore , awayScore , isHomePlayer : false } ) ) ; } return this . finalBoard ; } private createStore ( gameOptions ) { const _this = this ; const rootSaga = function * ( ) { yield all ( [ call ( battleSagaFactory < MatchState > ( state => state . board ) , gameOptions , _this . board ) , takeEvery < BattleEvents . BattleFinishEvent > ( BattleEvents . BATTLE_FINISH_EVENT , function * ( { payload : { turns } } ) { _this . onServerFinishMatch ( ) ; _this . logger . info ( "Battle finished" , { meta : { home : _this . home . getVariable ( v => v . name ) , away : _this . away . getVariable ( v => v . name ) , turns } } ) ; } ) , takeLatest < BattleEvents . BattleTurnEvent > ( BattleEvents . BATTLE_TURN_EVENT , function * ( { payload : { board } } ) { yield put ( _this . board . commands . setBoardPiecesCommand ( { pieces : board . pieces , piecePositions : board . piecePositions , size : undefined } ) ) ; } ) ] ) ; } ; const sagaMiddleware = createSagaMiddleware ( ) ; const store = createStore ( combineReducers < MatchState > ( { board : this . board . boardReducer , turn : turnReducer } ) , applyMiddleware ( sagaMiddleware ) ) ; sagaMiddleware . run ( rootSaga ) ; return store ; } private onServerFinishMatch ( ) { this . serverFinishedMatch . resolve ( ) ; } }