diff --git a/core/src/game/controller/GameController.ts b/core/src/game/controller/GameController.ts index d19466c..f862d6c 100644 --- a/core/src/game/controller/GameController.ts +++ b/core/src/game/controller/GameController.ts @@ -137,10 +137,10 @@ export class GameController { if (result) { tickResults.push([this.game.frame, result]); if (result.type === GameTickResultType.Win) { - this.fsm.go(GameControllerMode.Won); + this.fsm.go(GameControllerMode.Ended); break; } else if (result.type === GameTickResultType.Lose) { - this.fsm.go(GameControllerMode.Lost); + this.fsm.go(GameControllerMode.Ended); break; } } @@ -271,9 +271,9 @@ export class GameController { // Resume fsm.from(GameControllerMode.Paused).to(GameControllerMode.Playing); // Win - fsm.from(GameControllerMode.Playing).to(GameControllerMode.Won); + fsm.from(GameControllerMode.Playing).to(GameControllerMode.Ended); // Lose - fsm.from(GameControllerMode.Playing).to(GameControllerMode.Lost); + // fsm.from(GameControllerMode.Playing).to(GameControllerMode.Lost); // Reset fsm.fromAny(GameControllerMode).to(GameControllerMode.Ready); // End diff --git a/core/src/game/controller/GameController2.ts b/core/src/game/controller/GameController2.ts index 137638b..2a9e524 100644 --- a/core/src/game/controller/GameController2.ts +++ b/core/src/game/controller/GameController2.ts @@ -28,7 +28,7 @@ import { GameControllerSettingsAction, GameControllerStartAction, GameInput, - GameInputMove, + GameInputMove, GameMode, GameOptions, GameState, GameTickResult, @@ -45,12 +45,15 @@ import { isMoveAction, isMoveInput } from "../utils"; import { GameAction, GameActionMove } from "../types/gameAction"; import { assert } from "../../utils/assert"; import produce from "immer"; +import { clearDriftless, setDriftlessInterval } from "driftless"; export interface GameControllerOptions { // number of players (ie. number of games) players: number; seed?: string; gameOptions: Partial[]; + hasTimer: boolean; + useRAF: boolean; hasHistory: boolean; getTime: () => number; // each game may provide its own inputmanagers @@ -71,6 +74,8 @@ export const DEFAULT_GAME_CONTROLLER_OPTIONS: GameControllerOptions = { baseSpeed: 15 } ], + hasTimer: true, + useRAF: false, hasHistory: true, getTime: getGetTime(), // list of input managers, eg. of keyboard, touch events @@ -90,23 +95,22 @@ export enum GameControllerMode { // Countdown = "Countdown", Playing = "Playing", Paused = "Paused", - Ended = "Ended" + Ended = "Ended", + // game ended early before it started + Cancelled = "Cancelled", + + //todo remove + Ready = "Ready", + Countdown = "Countdown" } -export interface BaseGameControllerState { +export type BaseGameControllerState = { mode: GameControllerMode; gameOptions: Partial[]; -} - -export interface GameControllerSetupState extends BaseGameControllerState { - mode: GameControllerMode.Setup; - playersReady: boolean[]; -} +}; -export interface GameControllerInitializedState extends BaseGameControllerState { - mode: // | GameControllerMode.Ready - GameControllerMode.Playing | GameControllerMode.Paused | GameControllerMode.Ended; - playersReady: true[]; +export type BaseGameControllerGameState = { + // the portion of game controller state that keeps track of active game(s) games: Game[]; frame: number; refFrame: number; @@ -116,16 +120,58 @@ export interface GameControllerInitializedState extends BaseGameControllerState actionHistory: TimedGameActions[][]; stateHistory: GameState[][]; initialGameStates: GameState[]; - + // todo do we need resultHistory? resultHistory: TimedGameTickResult[][]; -} +}; + +export type GameControllerSetupState = BaseGameControllerState & { + mode: GameControllerMode.Setup; + playersReady: boolean[]; +}; +// extra state to handle case of game ending early before it starts +export type GameControllerCanceledState = BaseGameControllerState & { + mode: GameControllerMode.Cancelled; + playersReady: boolean[]; +}; + +export type GameControllerInitializedState = BaseGameControllerState & + // definitely has all game state when playing/paused + BaseGameControllerGameState & { + mode: // | GameControllerMode.Ready + // GameControllerMode.Playing | GameControllerMode.Paused | GameControllerMode.Ended; + GameControllerMode.Playing | GameControllerMode.Paused | GameControllerMode.Ended; + playersReady: true[]; + }; + +// export interface GameControllerInitializedState extends BaseGameControllerState { +// mode: // | GameControllerMode.Ready +// GameControllerMode.Playing | GameControllerMode.Paused | GameControllerMode.Ended; +// playersReady: true[]; +// games: Game[]; +// frame: number; +// refFrame: number; +// refTime: number; +// // these are [][]s because they have one array per game +// futureActions: TimedGameActions[][]; +// actionHistory: TimedGameActions[][]; +// stateHistory: GameState[][]; +// initialGameStates: GameState[]; +// // todo do we need resultHistory? +// resultHistory: TimedGameTickResult[][]; +// } -export type GameControllerState = GameControllerSetupState | GameControllerInitializedState; +export type GameControllerState = + | GameControllerSetupState + | GameControllerInitializedState + | GameControllerCanceledState; export type GameControllerPublicInitializedState = Omit & { gameStates: GameState[]; }; -export type GameControllerPublicState = GameControllerSetupState | GameControllerPublicInitializedState; +export type GameControllerPublicState = + | GameControllerSetupState + | GameControllerCanceledState + | GameControllerPublicInitializedState; // import { encodeTimedActions } from "../../encoding/action"; @@ -142,21 +188,9 @@ export class GameController { public getTime: () => number; protected seed: string; - // protected playersReady: boolean[]; - // protected games: Game[]; - // protected frame: number; - // protected refTime: number; - // protected refFrame: number; - // protected fsm: TypeState.FiniteStateMachine; - + protected timer: number | undefined; protected state: GameControllerState; - // these are [][]s because they have one array per game - // protected futureActions: TimedGameActions[][]; - // protected actionHistory: TimedGameActions[][]; - // protected stateHistory: GameState[][]; - // protected initialGameStates: GameState[]; - constructor(passedOptions: Partial = {}) { const options: GameControllerOptions = defaults({}, passedOptions, defaultOptions); this.options = options; @@ -218,24 +252,25 @@ export class GameController { // but not to be used for actions from the network (see handleRemoteAction), // onLocalAction may be used to send actions over the network this.handleAction(action); - if(this.options.onLocalAction) this.options.onLocalAction(action); + if (this.options.onLocalAction) this.options.onLocalAction(action); } public handleRemoteAction(action: GameControllerAction) { // opposite of handleLocalAction above - // used solely to denote (via callbacks) which actions cone from network vs. local user + // used solely to denote (via callbacks) which actions come from network vs. local user this.handleAction(action); - if(this.options.onLocalAction) this.options.onLocalAction(action); + if (this.options.onRemoteAction) this.options.onRemoteAction(action); } public getState(): GameControllerPublicState { - if (this.state.mode === GameControllerMode.Setup) { - return this.state; + const { state } = this; + if (state.mode === GameControllerMode.Setup || state.mode === GameControllerMode.Cancelled) { + return state; } else { // don't return game instances, instead call getState on each game and return gameStates - const { games } = this.state; + const { games } = state; return { - ...omit(this.state, ["games"]), + ...omit(state, ["games"]), gameStates: games.map(game => game.getState()) }; } @@ -249,14 +284,39 @@ export class GameController { manager.removeAllListeners(); } } - // todo stop timer + // make sure game timer is stopped + this.stopTimer(); + } + + protected initGames() { + const { players } = this.options; + const games: Game[] = times(players, (i: number) => { + const gameIOptions: Partial = this.state.gameOptions[i]; + // the game instance, which does the hard work + const game = new Game({ ...gameIOptions }); + assert(game.frame === 0, "Game must have frame = 0 after initialization"); + return game; + }); + return games; + } + + protected stopTimer() { + // stop timer if it exists + if (this.timer !== undefined) { + clearDriftless(this.timer); + delete this.timer; + } } + protected assertValidPlayer(player: number) { + const { players } = this.options; + assert(player < players, `Invalid player index ${player} (${players}-player game)`); + } protected handleAction(action: GameControllerAction) { // main GameControllerAction handler // private method - call via handleLocalAction or handleRemoteAction - console.log(`${action.type} action:`, action); + // console.log(`${action.type} action:`, action); switch (action.type) { case GameControllerActionType.Settings: @@ -303,8 +363,6 @@ export class GameController { console.log(this.state); } - - // protected setState(state: Partial) { // // const nextState = // this.state = {...this.state, ...state}; @@ -326,9 +384,10 @@ export class GameController { this.state = nextState; // fire Start action if all players are ready + // todo for network game, wait for server to send Start action if (every(nextState.playersReady)) { setTimeout(() => { - this.handleAction({ type: GameControllerActionType.Start }); + this.handleLocalAction({ type: GameControllerActionType.Start }); }, 0); } } @@ -360,6 +419,10 @@ export class GameController { this.state = nextState; + this.timer = setDriftlessInterval(() => { + this.tick(); + }, 1000 / 60); + console.log(this.getState()); console.log(action.type); } @@ -371,13 +434,60 @@ export class GameController { } protected handleEnd(action: GameControllerEndAction) { - // todo handle end action - console.log(action.type); + // todo handle end action.. what to do exactly, besides update mode? + console.log(action); + const { state } = this; + if (state.mode === GameControllerMode.Ended || state.mode === GameControllerMode.Cancelled) { + // already ended. throw error? + return; + } + + if (state.mode === GameControllerMode.Setup) { + // set mode to cancelled if gane ends during setup (before starting) + this.state = { + ...state, + mode: GameControllerMode.Cancelled + }; + } else { + this.state = { + ...state, + mode: GameControllerMode.Ended + }; + } + + this.stopTimer(); } - protected assertValidPlayer(player: number) { - const { players } = this.options; - assert(player < players, `Invalid player index ${player} (${players}-player game)`); + protected handleGameTickResult(gameIndex: number, result: GameTickResult, frame: number) { + switch (result.type) { + case GameTickResultType.Win: + this.handleWinResult(gameIndex, result, frame); + break; + case GameTickResultType.Lose: + this.handleLoseResult(gameIndex, result, frame); + break; + case GameTickResultType.Combo: + this.handleComboResult(gameIndex, result, frame); + break; + default: + throw new Error(`Unexpected GameTickResult: ${result}`); + } + + // this.state = produce(this.state, next => { + // next. + // }) + } + protected handleWinResult(gameIndex: number, result: GameTickResultWin, frame: number) { + // todo handle win + console.log("WIN", gameIndex, result, frame); + } + protected handleLoseResult(gameIndex: number, result: GameTickResultLose, frame: number) { + // todo handle lose + console.log("LOSE", gameIndex, result, frame); + } + protected handleComboResult(gameIndex: number, result: GameTickResultCombo, frame: number) { + // todo handle lose + console.log("COMBO", gameIndex, result, frame); } // public play() { @@ -420,8 +530,9 @@ export class GameController { // render with the current game state this.options.render(this.getState()); // this.last = now; - // requestAnimationFrame(this.tick.bind(this)); - + // if(this.options.hasTimer && this.options.useRAF) { + // requestAnimationFrame(this.tick.bind(this)); + // } } public tickToFrame(toFrame: number): void { @@ -445,23 +556,34 @@ export class GameController { const nextFrame = state.frame + 1; let results = []; + let shouldSaveGames = false; for (let i = 0; i < state.games.length; i++) { - const result = this.tickGame(i) || undefined; + const { result, shouldSave } = this.tickGame(i) || undefined; results.push(result); if (result) this.handleGameTickResult(i, result, nextFrame); + if (shouldSave) shouldSaveGames = true; } - // todo add to results history? - // gamesTickResults[j].push([this.frame, result]); + // save state of all games in history, if necessary + if (shouldSaveGames) { + for (let i = 0; i < state.games.length; i++) { + const game = state.games[i]; + // todo still need to cloneDeep? + // todo limit length of stored stateHistory? + state.stateHistory[i].push(cloneDeep(game.getState())); + } + // console.log(state.stateHistory); + } // if any of the game ticks had results, // process them and queue up any actions they created for the next tick + // todo add to results history? const nextTickActions = this.processGameResults(results); for (let i = 0; i < nextTickActions.length; i++) { const nextAction = nextTickActions[i]; - if(nextAction) { - // todo make sure this works - or nextFrame + 1?? - this.handleTimedGameActions(i, [nextFrame, [nextAction]]) + if (nextAction) { + // todo make sure this works - or nextFrame ?? + this.handleTimedGameActions(i, [nextFrame + 1, [nextAction]]); } } @@ -470,8 +592,7 @@ export class GameController { }); } - - protected tickGame(gameIndex: number): void | GameTickResult { + protected tickGame(gameIndex: number): { result: undefined | GameTickResult; shouldSave: boolean } { const { state } = this; assert(state.mode === GameControllerMode.Playing); this.assertValidPlayer(gameIndex); @@ -494,7 +615,7 @@ export class GameController { } } - const tickResult = game.tick(actions); + const tickResult = game.tick(actions) || undefined; // call user-provided callback so they can eg. send moves to server if (timedActions && timedActions.length && this.options.onMoveActions) { @@ -502,60 +623,24 @@ export class GameController { if (moveActions.length) { this.options.onMoveActions(gameIndex, [timedActions[0], moveActions]); } - // todo general action callback + // todo general action callback?? } + let shouldSave = false; // todo two different options for statehistory/actionhistory? if (actions && actions.length && this.options.hasHistory) { const gameActionHistory = state.actionHistory[gameIndex]; - const gameStateHistory = state.stateHistory[gameIndex]; const frameActions: TimedGameActions = [game.frame, actions]; // console.log("history item", frameActions); // console.log(this.actionHistory.map(item => encodeTimedActions(item)).join(";")); - // console.log(this.actionHistory, this.stateHistory); gameActionHistory.push(frameActions); - // todo still need to cloneDeep? - gameStateHistory.push(cloneDeep(game.getState())); - // todo limit length of stored stateHistory - } - - return tickResult; - } - - protected handleGameTickResult(gameIndex: number, result: GameTickResult, frame: number) { - switch (result.type) { - case GameTickResultType.Win: - this.handleWinResult(gameIndex, result, frame); - break; - case GameTickResultType.Lose: - this.handleLoseResult(gameIndex, result, frame); - break; - case GameTickResultType.Combo: - this.handleComboResult(gameIndex, result, frame); - break; - default: - throw new Error(`Unexpected GameTickResult: ${result}`); + // return shouldSave as boolean because we need to save *all* game states + shouldSave = true; } - // this.state = produce(this.state, next => { - // next. - // }) - } - protected handleWinResult(gameIndex: number, result: GameTickResultWin, frame: number) { - // todo handle win - console.log("WIN", gameIndex, result, frame); + return { result: tickResult, shouldSave }; } - protected handleLoseResult(gameIndex: number, result: GameTickResultLose, frame: number) { - // todo handle lose - console.log("LOSE", gameIndex, result, frame); - } - protected handleComboResult(gameIndex: number, result: GameTickResultCombo, frame: number) { - // todo handle lose - console.log("COMBO", gameIndex, result, frame); - } - - // public run() { // // called when gameplay starts, to initialize the game loop @@ -568,45 +653,6 @@ export class GameController { // requestAnimationFrame(this.tick.bind(this)); // } - protected initGames() { - const { players } = this.options; - const games: Game[] = times(players, (i: number) => { - const gameIOptions: Partial = this.state.gameOptions[i]; - // the game instance, which does the hard work - const game = new Game({ ...gameIOptions }); - assert(game.frame === 0, "Game must have frame = 0 after initialization"); - return game; - }); - return games; - } - - // protected initStateMachine( - - // // Countdown - // // fsm.on(GameControllerMode.Countdown, this.onCountdown); - // // Play - // fsm.on(GameControllerMode.Playing, () => { - // this.run(); - // }); - // // todo Reset? - // // fsm.on(GameControllerMode.Ready, () => { - // // this.game = this.initGame(); - // // }); - // fsm.on(GameControllerMode.Playing, from => { - // if (from === GameControllerMode.Paused) { - // // Resume - // // tick to get the game started again after being paused - // this.tick(); - // } - // }); - // - // fsm.onTransition = (from: GameControllerMode, to: GameControllerMode) => { - // this.onChangeMode(from, to); - // }; - // - // return fsm; - // } - // protected onCountdown = (): void => { // console.log("countdown!"); // const startTime = this.refTime; @@ -629,6 +675,7 @@ export class GameController { for (let i = 0; i < inputManagers.length; i++) { const gameInputManagers = inputManagers[i]; for (const manager of gameInputManagers) { + console.log("binding", "to", i); manager.on("input", this.handleInput.bind(this, i)); } } @@ -652,13 +699,10 @@ export class GameController { const action: GameControllerMovesAction = { type: GameControllerActionType.Moves, player: gameIndex, - moves: [ - state.games[gameIndex].frame + 1, - [{ type: GameActionType.Move, input, eventType }] - ] + moves: [state.games[gameIndex].frame + 1, [{ type: GameActionType.Move, input, eventType }]] }; this.handleLocalAction(action); - console.log('handled moves', action); + // console.log("handled moves", action); }; protected onChangeMode = (fromMode: GameControllerMode, toMode: GameControllerMode): void => { @@ -691,8 +735,7 @@ export class GameController { let resultIndices = [] as number[]; for (let i = 0; i < results.length; i++) { const maybeResult = results[i]; - if(!!maybeResult && maybeResult.type === resultType) - resultIndices.push(i); + if (!!maybeResult && maybeResult.type === resultType) resultIndices.push(i); } return resultIndices; } @@ -712,22 +755,22 @@ export class GameController { if (loseIndex > -1) { // non-losing players get ForfeitWin action return times(gameCount, gameIndex => { - return gameIndex === loseIndex ? undefined : { type: GameActionType.Defeat }; + return gameIndex === loseIndex ? undefined : { type: GameActionType.ForfeitWin }; }); } // default - no results, no actions - let nextActions = times(gameCount, () => undefined); + let nextActions = times(gameCount, () => undefined); // if any player(s) get combo, another player gets garbage const comboIndices = findAllResultIndices(GameTickResultType.Combo); - for(let comboIndex of comboIndices) { + for (let comboIndex of comboIndices) { const comboResult = results[comboIndex]; assert(comboResult && comboResult.type === GameTickResultType.Combo); // give garbage to next player, use % to wraparound const garbageIndex = (comboIndex + 1) % gameCount; // garbage should have same colors as combo colors - nextActions[garbageIndex] = {type: GameActionType.Garbage, colors:comboResult.colors} + nextActions[garbageIndex] = { type: GameActionType.Garbage, colors: comboResult.colors }; } return nextActions; @@ -760,7 +803,7 @@ export class GameController { protected rewriteHistoryWithActions(gameIndex: number, timedActions: TimedGameActions) { const { state } = this; - assert(state.mode !== GameControllerMode.Setup); + assert(state.mode === GameControllerMode.Playing || state.mode === GameControllerMode.Paused); assert(this.options.hasHistory, `Cannot rewrite history, options.hasHistory is false`); // given a gameIndex and a new timedActions which occurred *in the past* @@ -779,11 +822,7 @@ export class GameController { const currentFrame = game.frame; // make dummy games with last state before timedActions - // todo fix this, need to rewind in lockstep - rewindDummyGamesToFrame? - const dummyGames = games.map(g => this.makeDummyGameCopy(g)); - dummyGames.forEach((dummy, i) => { - this.rewindGameToFrame(i, dummy, frame - 1); - }); + const dummyGames = this.getDummyGamesAtFrame(frame - 1); // insert new frameActions at the correct place in affected game's actionHistory addTimedActionsTo(timedActions, actionHistory[gameIndex]); @@ -839,29 +878,31 @@ export class GameController { // } let dummyFrame = dummyGames[0].frame; - while(dummyFrame < currentFrame) { + while (dummyFrame < currentFrame) { // tick each dummy game, taking timed actions from actionHistory - const frameResults = dummyGames.map((dummyGame, i): GameTickResult | undefined => { - // todo! optimize - don't search entire history on every frame! - // eg. find the earliest action(s) in actionHistory which happen after the game's current frame - const frameActions = actionHistory[i].find(([aFrame]) => aFrame === dummyGame.frame); - // tick game, with or without actions, returning result - if(frameActions) { - // save (post-tick) game state to stateHistory if we have actions for this frame - const result = dummyGame.tick(frameActions[1]) || undefined; - // todo refactor to not need cloneDeep? (added this to fix a bug, details of which i dont recall) - stateHistory[i].push(cloneDeep(dummyGame.getState())); - return result; + const frameResults = dummyGames.map( + (dummyGame, i): GameTickResult | undefined => { + // todo! optimize - don't search entire history on every frame! + // eg. find the earliest action(s) in actionHistory which happen after the game's current frame + const frameActions = actionHistory[i].find(([aFrame]) => aFrame === dummyGame.frame); + // tick game, with or without actions, returning result + if (frameActions) { + // save (post-tick) game state to stateHistory if we have actions for this frame + const result = dummyGame.tick(frameActions[1]) || undefined; + // todo refactor to not need cloneDeep? (added this to fix a bug, details of which i dont recall) + stateHistory[i].push(cloneDeep(dummyGame.getState())); + return result; + } + return dummyGame.tick() || undefined; } - return dummyGame.tick() || undefined; - }); + ); // process dummy game results // add any resulting actions to relevant games' actionHistory for next tick const nextFrameActions = this.processGameResults(frameResults); - for(let i = 0; i < nextFrameActions.length; i++) { + for (let i = 0; i < nextFrameActions.length; i++) { const nextAction = nextFrameActions[i]; - if(nextAction) addTimedActionsTo([dummyFrame + 1, [nextAction]], actionHistory[i]); + if (nextAction) addTimedActionsTo([dummyFrame + 1, [nextAction]], actionHistory[i]); } // todo add to resultHistory or deprecate resultHistory? // todo!! handle case of LAST frame before current - should action go in futureActions?? @@ -870,7 +911,7 @@ export class GameController { } // done - dummyGames are at same frame as this.game, but have frameActions in their history // set our true state.games' Game states to the dummy game states - for(let i = 0; i < state.games.length; i++) { + for (let i = 0; i < state.games.length; i++) { state.games[i].setState(dummyGames[i].getState()); } } @@ -887,13 +928,46 @@ export class GameController { return dummyGame; } + protected getDummyGamesAtFrame(frame: number): Game[] { + const { state } = this; + assert(state.mode === GameControllerMode.Playing || state.mode === GameControllerMode.Paused); + assert(this.options.hasHistory, `Cannot rewind game, options.hasHistory is false`); + assert(frame <= state.frame, `Can't rewind game to a later frame (${frame} > ${state.frame})`); + + const { games, stateHistory, initialGameStates } = state; + + // find (in stateHistory) latest saved game state before `frame` + const dummyGames: Game[] = games.map((game, gameIndex) => { + let restoreState = findLast(stateHistory[gameIndex], gameState => gameState.frame <= frame); + if (!restoreState) { + restoreState = initialGameStates[gameIndex]; + } + // make a new dummy Game with the saved state + const dummyGame = this.makeDummyGameCopy(game, restoreState); + // dummy game is now at last known state *before or at* `frame`, tick ahead to `frame` + // safe to ignore actions here because state should be saved on every frame with actions, + // so there shouldn't be any actions between restoreState and `frame` + while (dummyGame.frame < frame) { + game.tick(); // todo handle pass actions??!! + if(game.getState().mode === GameMode.Ended) break; + } + }); + // games may actually be at an earlier frame *before* the given `frame` + // this is OK because there should be no actions between dummyGames.frame and `frame` + // just double check all dummyGames are on the same frame as one another + const restoredFrame = dummyGames[0].frame; + assert(dummyGames.every(game => game.frame === restoredFrame)); + + return dummyGames; + } + protected rewindGameToFrame(gameIndex: number, game: Game, frame: number) { // use state history to "rewind" the state of the game to a given frame // may not have saved that exact frame, so find the nearest saved frame less tham or equal to the target, // start there, and tick forward through time until reaching the target frame const { state } = this; - assert(state.mode !== GameControllerMode.Setup); + assert(state.mode === GameControllerMode.Playing || state.mode === GameControllerMode.Paused); assert(this.options.hasHistory, `Cannot rewind game, options.hasHistory is false`); const { stateHistory, initialGameStates } = state; diff --git a/core/src/game/controller/constants.ts b/core/src/game/controller/constants.ts index cc9acb8..2029980 100644 --- a/core/src/game/controller/constants.ts +++ b/core/src/game/controller/constants.ts @@ -43,10 +43,12 @@ export const DEFAULT_KEYS: KeyBindings = { [GameControllerMode.Paused]: { [GameInput.Resume]: ["enter", "space", "escape"] }, - [GameControllerMode.Won]: { [GameInput.Reset]: ["enter", "space", "escape"] }, - [GameControllerMode.Lost]: { - [GameInput.Reset]: ["enter", "space", "escape"] - }, + // [GameControllerMode.Won]: { [GameInput.Reset]: ["enter", "space", "escape"] }, + // [GameControllerMode.Lost]: { + // [GameInput.Reset]: ["enter", "space", "escape"] + // }, [GameControllerMode.Ready]: {}, - [GameControllerMode.Ended]: {} + [GameControllerMode.Ended]: { + [GameInput.Reset]: ["enter", "space", "escape"] + } }; diff --git a/core/src/game/controller/types.ts b/core/src/game/controller/types.ts index 9442a9a..b3ab79e 100644 --- a/core/src/game/controller/types.ts +++ b/core/src/game/controller/types.ts @@ -10,9 +10,10 @@ export enum GameControllerMode { Countdown = "Countdown", Playing = "Playing", Paused = "Paused", - Won = "Won", - Lost = "Lost", - Ended = "Ended" + // Won = "Won", + // Lost = "Lost", + Ended = "Ended", + Cancelled = "Cancelled" } export interface GameControllerState { diff --git a/core/src/game/enums.ts b/core/src/game/enums.ts index ef2ec47..c0a16b1 100644 --- a/core/src/game/enums.ts +++ b/core/src/game/enums.ts @@ -10,7 +10,10 @@ export enum GameMode { // Destruction: lines are being destroyed Destruction = "Destruction", // Ended: game has ended - Ended = "Ended" + Ended = "Ended", + // todo: deprecate Ended mode and replace with Won/Lost + Won = "Won", + Lost = "Lost" } export enum GameInput { diff --git a/core/src/game/input/types.ts b/core/src/game/input/types.ts index 7b3f46a..922990b 100644 --- a/core/src/game/input/types.ts +++ b/core/src/game/input/types.ts @@ -1,3 +1,4 @@ +// import { GameControllerMode } from "../controller"; import { GameControllerMode } from "../controller"; import { GameInput, InputEventType, ModeKeyBindings } from "../types"; diff --git a/terminal-client/src/@types/driftless/index.d.ts b/terminal-client/src/@types/driftless/index.d.ts new file mode 100644 index 0000000..fcaf498 --- /dev/null +++ b/terminal-client/src/@types/driftless/index.d.ts @@ -0,0 +1,20 @@ +declare module "driftless" { + export function setDriftlessTimeout( + callback: (...args: any[]) => void, + delayMs: number, + ...params: any[] + ): number; + + export function setDriftlessInterval( + callback: (...args: any[]) => void, + delayMs: number, + ...params: any[] + ): number; + + export function clearDriftless( + id: number, + options?: { + customClearTimeout?: (...args: any[]) => void; + } + ): void; +} diff --git a/terminal-client/src/TerminalGameUi.ts b/terminal-client/src/TerminalGameUi.ts index 1cc8236..f2f92e1 100644 --- a/terminal-client/src/TerminalGameUi.ts +++ b/terminal-client/src/TerminalGameUi.ts @@ -1,7 +1,8 @@ import * as blessed from "blessed"; import chalk from "chalk"; -import { GameColor, GameControllerState, GameGridRow, GridObject } from "mrdario-core/lib/game/types"; +import { GameColor, GameGridRow, GridObject } from "mrdario-core/lib/game/types"; +import { GameControllerMode, GameControllerPublicState } from "mrdario-core/lib/game/controller/GameController2"; import { hasColor } from "mrdario-core/lib/game/utils/guards"; import { GRID_OBJECT_STRINGS } from "./constants"; @@ -73,20 +74,24 @@ export default class TerminalGameUi { render() { this.screen.render(); } - renderGame(state: GameControllerState) { - const gridRowStrs = state.gameState.grid.map((row: GameGridRow) => { - const objStrs = row.map((obj: GridObject) => { - return renderObject(obj, GRID_OBJECT_STRINGS); + renderGame(state: GameControllerPublicState) { + if(state.mode === GameControllerMode.Playing) { + const gameState = state.gameStates[0]; + const gridRowStrs = gameState.grid.map((row: GameGridRow) => { + const objStrs = row.map((obj: GridObject) => { + return renderObject(obj, GRID_OBJECT_STRINGS); + }); + return objStrs.join(""); }); - return objStrs.join(""); - }); - const gridStr = gridRowStrs.join("\n"); - if (gridStr !== this.lastGridStr) { - this.lastGridStr = gridStr; - this.gameBox.setContent(gridStr); - this.scoreBox.setContent(`Score\n${state.gameState.score}`); - this.screen.render(); + const gridStr = gridRowStrs.join("\n"); + if (gridStr !== this.lastGridStr) { + this.lastGridStr = gridStr; + this.gameBox.setContent(gridStr); + this.scoreBox.setContent(`Score\n${gameState.score}`); + this.screen.render(); + } } + } } diff --git a/terminal-client/src/TerminalKeyManager.ts b/terminal-client/src/TerminalKeyManager.ts index 16e8e93..e27578d 100644 --- a/terminal-client/src/TerminalKeyManager.ts +++ b/terminal-client/src/TerminalKeyManager.ts @@ -2,13 +2,14 @@ import { EventEmitter } from "events"; import * as blessed from "blessed"; import { - GameControllerMode, + // GameControllerMode, GameInput, InputEventType, InputManager, KeyBindings, ModeKeyBindings } from "mrdario-core/lib/game/types"; +import {GameControllerMode} from "mrdario-core/src/game/controller/GameController2"; export default class TerminalKeyManager extends EventEmitter implements InputManager { @@ -38,8 +39,8 @@ export default class TerminalKeyManager extends EventEmitter implements InputMan this.mode = mode; } private handleInput(inputType: GameInput) { - super.emit(inputType, InputEventType.KeyDown); - super.emit(inputType, InputEventType.KeyUp); + super.emit("input", inputType, InputEventType.KeyDown); + super.emit("input", inputType, InputEventType.KeyUp); } private bindModeKeys(mode: GameControllerMode) { if (!this.keyBindings[mode]) return; @@ -48,9 +49,12 @@ export default class TerminalKeyManager extends EventEmitter implements InputMan const inputType = inputTypeStr as GameInput; const keyStr = modeBindings[inputType] as string | string[]; - const listener = this.handleInput.bind(this, inputType); - this.keyListeners[inputTypeStr] = { keyStr, listener }; + // const listener = this.handleInput.bind(this, inputType); + const listener = () => { + this.handleInput.call(this, inputType) + } + this.keyListeners[inputTypeStr] = { keyStr, listener }; this.screen.key(keyStr, listener); } } diff --git a/terminal-client/src/index.ts b/terminal-client/src/index.ts index e1e7d15..e14bffa 100644 --- a/terminal-client/src/index.ts +++ b/terminal-client/src/index.ts @@ -1,19 +1,14 @@ import { create as createSocket, SCClientSocket } from "socketcluster-client"; import { defaults } from "lodash"; - // import Game from "mrdario-core/src/Game"; -import { - GameControllerMode, - GameControllerState, - KeyBindings, -} from "mrdario-core/lib/game/types"; - +import { GameControllerActionType, KeyBindings } from "mrdario-core/lib/game/types"; +import { GameController, GameControllerMode } from "mrdario-core/lib/game/controller/GameController2"; import { GridObjectStringMap } from "./types"; import { GRID_OBJECT_STRINGS, KEY_BINDINGS } from "./constants"; import TerminalGameUi from "./TerminalGameUi"; import TerminalKeyManager from "./TerminalKeyManager"; -import CLIGameController from "./CLIGameController"; +// import CLIGameController from "./CLIGameController"; // import TerminalGameController from "./TerminalGameController"; @@ -30,7 +25,8 @@ export class CLIGameClient { ui: TerminalGameUi; options: CLIGameClientOptions; // gameController: TerminalGameController; - gameController: CLIGameController; + // gameController: CLIGameController; + gameController: GameController; lastGridStr: string; keyManager: TerminalKeyManager; @@ -60,21 +56,35 @@ export class CLIGameClient { // this.gameController.play(); - this.gameController = new CLIGameController({ - screen: this.ui.screen, - render: (state: GameControllerState) => { + // this.gameController = new CLIGameController({ + // screen: this.ui.screen, + // render: (state: GameControllerState) => { + // this.ui.renderGame(state); + // }, + // onWin: () => { + // console.log("YOU WIN :)"); + // process.exit(); + // }, + // onLose: () => { + // console.log("YOU LOSE :("); + // process.exit(); + // }, + // keyManager: this.keyManager + // }); + + this.gameController = new GameController({ + players: 1, + render: (state) => { this.ui.renderGame(state); }, - onWin: () => { - console.log("YOU WIN :)"); - process.exit(); - }, - onLose: () => { - console.log("YOU LOSE :("); - process.exit(); - }, - keyManager: this.keyManager - }); + // onLocalAction: action => {console.log(action)}, + inputManagers: [[this.keyManager]] + }) + this.gameController.handleLocalAction({type: GameControllerActionType.Ready, player: 0, ready: true}); + + + + diff --git a/yarn.lock b/yarn.lock index d9bccfd..75fe755 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2780,11 +2780,6 @@ camelcase@^2.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - camelcase@^4.0.0, camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -2994,15 +2989,6 @@ cli-width@^2.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - cliui@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" @@ -3614,7 +3600,7 @@ debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" -decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -5875,11 +5861,6 @@ invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - invert-kv@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" @@ -7029,13 +7010,6 @@ lazyness@^1.1.1: resolved "https://registry.yarnpkg.com/lazyness/-/lazyness-1.1.1.tgz#d5a7b63a217254bcd559f9b17753c92b70a84d81" integrity sha512-rYHC6l6LeRlJSt5jxpqN8z/49gZ0CqLi89HAGzJjHahCFlqEjFGFN9O15hmzSzUGFl7zN/vOWduv/+0af3r/kQ== -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - lcid@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" @@ -7971,10 +7945,10 @@ node-releases@^1.1.29: dependencies: semver "^6.3.0" -node-sass@^4.11.0: - version "4.12.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017" - integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ== +node-sass@^4.14.1: + version "4.14.1" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.14.1.tgz#99c87ec2efb7047ed638fb4c9db7f3a42e2217b5" + integrity sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -7983,14 +7957,14 @@ node-sass@^4.11.0: get-stdin "^4.0.1" glob "^7.0.3" in-publish "^2.0.0" - lodash "^4.17.11" + lodash "^4.17.15" meow "^3.7.0" mkdirp "^0.5.1" nan "^2.13.2" node-gyp "^3.8.0" npmlog "^4.0.0" request "^2.88.0" - sass-graph "^2.2.4" + sass-graph "2.2.5" stdout-stream "^1.4.0" "true-case-path" "^1.0.2" @@ -8312,13 +8286,6 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - os-locale@^3.0.0, os-locale@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" @@ -9957,15 +9924,15 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sass-graph@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" - integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k= +sass-graph@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8" + integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag== dependencies: glob "^7.0.0" lodash "^4.0.0" scss-tokenizer "^0.2.3" - yargs "^7.0.0" + yargs "^13.3.2" sass-loader@^7.1.0: version "7.3.1" @@ -10717,7 +10684,7 @@ string-width@4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^5.2.0" -string-width@^1.0.1, string-width@^1.0.2: +string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= @@ -11999,11 +11966,6 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= - which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -12136,11 +12098,6 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= - "y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" @@ -12195,12 +12152,13 @@ yargs-parser@^13.1.0, yargs-parser@^13.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" - integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo= +yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== dependencies: - camelcase "^3.0.0" + camelcase "^5.0.0" + decamelize "^1.2.0" yargs@12.0.5: version "12.0.5" @@ -12253,24 +12211,21 @@ yargs@^13.3.0: y18n "^4.0.0" yargs-parser "^13.1.1" -yargs@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" - integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg= +yargs@^13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" require-directory "^2.1.1" - require-main-filename "^1.0.1" + require-main-filename "^2.0.0" set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^5.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" yauzl@^2.4.2: version "2.10.0"