diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..24d0bbb --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,46 @@ +module.exports = { + env: { + commonjs: true, + es6: true, + browser: true, + }, + extends: ['eslint:recommended', 'plugin:prettier/recommended'], + globals: { + DISCORD_APP_ID: true, + DISCORD_PUBLIC_KEY: true, + DISCORD_BOT_TOKEN: true, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + }, + plugins: ['@typescript-eslint'], + rules: { + 'prettier/prettier': 'warn', + 'no-cond-assign': [2, 'except-parens'], + 'no-unused-vars': 0, + '@typescript-eslint/no-unused-vars': 1, + 'no-empty': [ + 'error', + { + allowEmptyCatch: true, + }, + ], + 'prefer-const': [ + 'warn', + { + destructuring: 'all', + }, + ], + 'spaced-comment': 'warn', + }, + overrides: [ + { + files: ['slash-up.config.js', 'webpack.config.js'], + env: { + node: true, + }, + }, + ], +} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 8049cd2..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,46 +0,0 @@ -module.exports = { - env: { - commonjs: true, - es6: true, - browser: true - }, - extends: ['eslint:recommended', 'plugin:prettier/recommended'], - globals: { - DISCORD_APP_ID: true, - DISCORD_PUBLIC_KEY: true, - DISCORD_BOT_TOKEN: true - }, - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 6, - sourceType: 'module' - }, - plugins: ['@typescript-eslint'], - rules: { - 'prettier/prettier': 'warn', - 'no-cond-assign': [2, 'except-parens'], - 'no-unused-vars': 0, - '@typescript-eslint/no-unused-vars': 1, - 'no-empty': [ - 'error', - { - allowEmptyCatch: true - } - ], - 'prefer-const': [ - 'warn', - { - destructuring: 'all' - } - ], - 'spaced-comment': 'warn' - }, - overrides: [ - { - files: ['slash-up.config.js', 'webpack.config.js'], - env: { - node: true - } - } - ] -}; diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..d23208f --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/slash-up.config.js b/slash-up.config.js index 8ca7851..253c5be 100644 --- a/slash-up.config.js +++ b/slash-up.config.js @@ -2,18 +2,18 @@ // Make sure to fill in "token" and "applicationId" before using. // You can also use environment variables from the ".env" file if any. -module.exports = { - // The Token of the Discord bot - token: process.env.DISCORD_BOT_TOKEN, - // The Application ID of the Discord bot - applicationId: process.env.DISCORD_APP_ID, - // This is where the path to command files are, .ts files are supported! - commandPath: './src/commands', - // You can use different environments with --env (-e) - env: { - development: { - // The "globalToGuild" option makes global commands sync to the specified guild instead. - globalToGuild: process.env.DEVELOPMENT_GUILD_ID - } - } -}; +export default { + // The Token of the Discord bot + token: process.env.DISCORD_BOT_TOKEN, + // The Application ID of the Discord bot + applicationId: process.env.DISCORD_APP_ID, + // This is where the path to command files are, .ts files are supported! + commandPath: './src/commands', + // You can use different environments with --env (-e) + env: { + development: { + // The "globalToGuild" option makes global commands sync to the specified guild instead. + globalToGuild: process.env.DEVELOPMENT_GUILD_ID, + }, + }, +} diff --git a/src/game/gameData.spec.ts b/src/game/dieState.spec.ts similarity index 97% rename from src/game/gameData.spec.ts rename to src/game/dieState.spec.ts index c567f9c..4518dd8 100644 --- a/src/game/gameData.spec.ts +++ b/src/game/dieState.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from '@jest/globals' -import { DieState, isHeldState, isSelectedState, setDeselected, setSelected, toggleSelected } from './gameData' +import { DieState, isHeldState, isSelectedState, setDeselected, setSelected, toggleSelected } from './dieState' describe('isHeldState', () => { test.each<[DieState, boolean]>([ diff --git a/src/game/dieState.ts b/src/game/dieState.ts new file mode 100644 index 0000000..31c0dd1 --- /dev/null +++ b/src/game/dieState.ts @@ -0,0 +1,130 @@ +export enum DieFace { + FAIL = 'F', + STOP = 'S', + WILD = '*', + BONUS = 'B', + USELESS = 'X', + SCORE_0 = '0', + SCORE_1 = '1', + SCORE_2 = '2', + SCORE_3 = '3', + SCORE_4 = '4', + SCORE_5 = '5', + SCORE_6 = '6', + SCORE_7 = '7', + SCORE_8 = '8', + SCORE_9 = '9', + SCORE_10 = '10', +} + +export type SpecialDie = DieFace.FAIL | DieFace.STOP | DieFace.WILD | DieFace.BONUS | DieFace.USELESS +export type ScoringDie = + | DieFace.SCORE_0 + | DieFace.SCORE_1 + | DieFace.SCORE_2 + | DieFace.SCORE_3 + | DieFace.SCORE_4 + | DieFace.SCORE_5 + | DieFace.SCORE_6 + | DieFace.SCORE_7 + | DieFace.SCORE_8 + | DieFace.SCORE_9 + | DieFace.SCORE_10 + +type DieFaceCount = { + [v in DieFace]?: number +} + +export enum DieState { + ROLLED = 'rolled', + SELECTED = 'selected', + HELD = 'held', + HELD_SELECTED = 'held_selected', + SCORED = 'scored', +} + +export function isHeldState(d: DieState): d is DieState.HELD | DieState.HELD_SELECTED { + return d === DieState.HELD || d === DieState.HELD_SELECTED +} + +export function isSelectedState(d: DieState): d is DieState.SELECTED | DieState.HELD_SELECTED { + return d === DieState.SELECTED || d === DieState.HELD_SELECTED +} + +export function setSelected(d: DieState): DieState.SELECTED | DieState.HELD_SELECTED | DieState.SCORED { + switch (d) { + case DieState.ROLLED: + return DieState.SELECTED + case DieState.HELD: + return DieState.HELD_SELECTED + case DieState.SELECTED: + case DieState.HELD_SELECTED: + case DieState.SCORED: + return d + } +} + +export function setDeselected(d: DieState): DieState.ROLLED | DieState.HELD | DieState.SCORED { + switch (d) { + case DieState.SELECTED: + return DieState.ROLLED + case DieState.HELD_SELECTED: + return DieState.HELD + case DieState.ROLLED: + case DieState.HELD: + case DieState.SCORED: + return d + } +} + +export function toggleSelected(d: DieState): DieState { + switch (d) { + case DieState.ROLLED: + return DieState.SELECTED + case DieState.SELECTED: + return DieState.ROLLED + case DieState.HELD: + return DieState.HELD_SELECTED + case DieState.HELD_SELECTED: + return DieState.HELD + case DieState.SCORED: + return DieState.SCORED + } +} + +export interface DieResult { + readonly type: DieType + readonly face: DieFace + readonly state: DieState +} + +export interface DieType { + // The name of this die type + readonly name: string + // The faces of this die type; faces may be repeated to increase their odds + readonly faces: readonly DieFace[] +} + +export interface DiceCombo { + // The name of this combo, for the UI + readonly name: string + // How many points this combo is worth + readonly points: number + // Which dice are needed to satisfy this combo + readonly dice: readonly ScoringDie[] + // How many dice can be replaced for the combo + readonly wildMaxDice?: number +} + +export interface DiceComboResult { + // combo used + readonly combo: DiceCombo + // total number of points + readonly points: number + // number of dice used + readonly usesDice: DieFaceCount + // number of wild dice used + readonly wildDice: number + // number of bonus dice used + readonly bonusDice: number +} diff --git a/src/game/gameEvent.ts b/src/game/gameEvent.ts index 483f4ff..effd471 100644 --- a/src/game/gameEvent.ts +++ b/src/game/gameEvent.ts @@ -1,30 +1,40 @@ -import { DieResult, GameData } from './gameData' +import { DiceCombo, DieResult } from './dieState' export enum IncomingEventType { // Chooses the action to be performed for this round but does not lock it in until the player rolls. + // Valid during GameState.ROUND_START SelectAction = 'select_action', // Dice buttons toggle the selection of that die + // Valid during GameState.TURN_ROLLED ToggleSelectDie = 'toggle_select_die', // Button 1 is Select All/Deselect All // Select All appears if no dice are selected // Deselect All displays if at least one die is selected + // Both valid during GameState.TURN_ROLLED SelectAllDice = 'select_all_dice', DeselectAllDice = 'deselect_all_dice', // Button 2 is Hold/Release selected dice // If only held dice are selected, Release appears // For selections with at least one unheld die, Hold appears + // Both valid during GameState.TURN_ROLLED HoldSelectedDice = 'hold_selected_dice', ReleaseSelectedDice = 'release_selected_dice', // Button 3 is End Turn/Score Selected Dice - // End Turn appears if at least one Stop die is selected, but is only enabled if sufficient Stop dice are selected - // and no non-Stop dice are selected - // Score Selected Dice appears otherwise, but is only enabled if at least one scoreable die is selected and no - // non-scoreable dice are selected + // End Turn appears by default, but is only enabled if sufficient Stop dice are in the current roll or if the turn + // has been failed + // Score Selected Dice appears when any dice are selected, but is only enabled if at least one scoreable die is + // selected and no non-scoreable dice are selected + // Both appear during GameState.TURN_ROLLED + // End Turn also appears and must be pressed to move to the next round (or end the game) in GameState.TURN_FAILED EndTurn = 'end_turn', ScoreSelectedDice = 'score_selected_dice', // Button 4 is Roll Dice - // It's always visible, but is disabled if there are no unheld dice + // It's always visible, but is disabled if there are no unheld dice or if the action has not been chosen + // Valid during GameState.ROUND_START, GameState.TURN_START, and GameState.TURN_ROLLED RollDice = 'roll_dice', + // Abort ends the game without a victor + // Valid during any state except GameState.ABORTED or GameState.VICTORY + Abort = 'abort', } export interface IncomingBaseEvent { @@ -46,6 +56,7 @@ export interface IncomingSimpleEvent extends IncomingBaseEvent { | IncomingEventType.EndTurn | IncomingEventType.DeselectAllDice | IncomingEventType.RollDice + | IncomingEventType.Abort } export type IncomingEvent = IncomingIndexedEvent | IncomingSimpleEvent @@ -74,6 +85,7 @@ export enum OutgoingEventType { ScoreDice = 'score_dice', GainFailures = 'gain_failures', + SafeEndTurn = 'safe_end_turn', LoseRound = 'lose_round', LoseGame = 'lose_game', @@ -83,11 +95,6 @@ export enum OutgoingEventType { StartRound = 'new_round', } -export enum EventTarget { - Top = 'top', - Bottom = 'bottom', -} - export enum TextSender { Top = 'top', Bottom = 'bottom', @@ -114,6 +121,7 @@ export interface OutgoingBaseTargetedEvent extends OutgoingBaseEvent { | OutgoingEventType.LoseGame | OutgoingEventType.ScoreDice | OutgoingEventType.GainFailures + | OutgoingEventType.SafeEndTurn | OutgoingEventType.StartRoll | OutgoingEventType.StartTurn | OutgoingEventType.StartRound @@ -121,7 +129,11 @@ export interface OutgoingBaseTargetedEvent extends OutgoingBaseEvent { } export interface OutgoingSimpleTargetedEvent extends OutgoingBaseTargetedEvent { - readonly type: OutgoingEventType.LoseGame | OutgoingEventType.StartTurn | OutgoingEventType.StartRound + readonly type: + | OutgoingEventType.LoseGame + | OutgoingEventType.StartTurn + | OutgoingEventType.StartRound + | OutgoingEventType.SafeEndTurn } export interface OutgoingDamageEvent extends OutgoingBaseTargetedEvent { @@ -137,12 +149,13 @@ export interface OutgoingDamageEvent extends OutgoingBaseTargetedEvent { export interface OutgoingStartRollEvent extends OutgoingBaseTargetedEvent { readonly type: OutgoingEventType.StartRoll - readonly newDice: DieResult[] + readonly newDice: readonly DieResult[] } export interface OutgoingScoreDiceEvent extends OutgoingBaseTargetedEvent { readonly type: OutgoingEventType.ScoreDice - readonly scoredDice: DieResult[] + readonly scoredCombo: DiceCombo + readonly scoredDice: readonly DieResult[] readonly scoreDelta: number } @@ -153,16 +166,16 @@ export type OutgoingEvent = | OutgoingStartRollEvent | OutgoingScoreDiceEvent -export interface BaseEventResult { - readonly newState: GameData +export interface BaseEventResult { + readonly newState: T } -export interface SuccessResult extends BaseEventResult { - readonly events: OutgoingEvent[] +export interface SuccessResult extends BaseEventResult { + readonly events: readonly OutgoingEvent[] } -export interface FailedResult extends BaseEventResult { +export interface FailedResult extends BaseEventResult { readonly error: GameError } -export type EventResult = SuccessResult | FailedResult +export type EventResult = SuccessResult | FailedResult diff --git a/src/game/gameData.ts b/src/game/gameState.ts similarity index 74% rename from src/game/gameData.ts rename to src/game/gameState.ts index cf80250..c6f4969 100644 --- a/src/game/gameData.ts +++ b/src/game/gameState.ts @@ -1,88 +1,5 @@ import { RenderableText } from '../util/renderableText' - -export enum DieFace { - FAIL = 'F', - STOP = 'S', - SCORE_0 = '0', - SCORE_1 = '1', - SCORE_2 = '2', - SCORE_3 = '3', - SCORE_4 = '4', - SCORE_5 = '5', - WILD = '*', - HEART = '<3', - QUESTION = '?', -} - -export enum DieState { - ROLLED = 'rolled', - SELECTED = 'selected', - HELD = 'held', - HELD_SELECTED = 'held_selected', - SCORED = 'scored', -} - -export function isHeldState(d: DieState): d is DieState.HELD | DieState.HELD_SELECTED { - return d === DieState.HELD || d === DieState.HELD_SELECTED -} - -export function isSelectedState(d: DieState): d is DieState.SELECTED | DieState.HELD_SELECTED { - return d === DieState.SELECTED || d === DieState.HELD_SELECTED -} - -export function setSelected(d: DieState): DieState.SELECTED | DieState.HELD_SELECTED | DieState.SCORED { - switch (d) { - case DieState.ROLLED: - return DieState.SELECTED - case DieState.HELD: - return DieState.HELD_SELECTED - case DieState.SELECTED: - case DieState.HELD_SELECTED: - case DieState.SCORED: - return d - } -} - -export function setDeselected(d: DieState): DieState.ROLLED | DieState.HELD | DieState.SCORED { - switch (d) { - case DieState.SELECTED: - return DieState.ROLLED - case DieState.HELD_SELECTED: - return DieState.HELD - case DieState.ROLLED: - case DieState.HELD: - case DieState.SCORED: - return d - } -} - -export function toggleSelected(d: DieState): DieState { - switch (d) { - case DieState.ROLLED: - return DieState.SELECTED - case DieState.SELECTED: - return DieState.ROLLED - case DieState.HELD: - return DieState.HELD_SELECTED - case DieState.HELD_SELECTED: - return DieState.HELD - case DieState.SCORED: - return DieState.SCORED - } -} - -export interface DieResult { - readonly type: DieType - readonly face: DieFace - readonly state: DieState -} - -export interface DieType { - // The name of this die type - readonly name: string - // The faces of this die type; faces may be repeated to increase their odds - readonly faces: readonly DieFace[] -} +import { DiceCombo, DieResult, DieType } from './dieState' export interface AIWeights { // How much this AI likes to choose actions that increase the damage bonus @@ -165,11 +82,13 @@ export interface Difficulty { readonly shortDescription?: string // Description of the difficulty when selected readonly description?: string + // The player who should start in this difficulty; defaults to random + readonly startingPlayer?: PlayerSide | null - // Starting values for the top - readonly topStats: PlayerStartingState - // Starting values for the bottom - readonly bottomStats: PlayerStartingState + // Starting values for each character + readonly stats: { + readonly [x in PlayerSide]: PlayerStartingState + } } export interface PlayerText { @@ -198,6 +117,8 @@ export interface GameTheme { readonly shortDescription: string readonly description: string + readonly diceCombos: readonly DiceCombo[] + readonly actions: readonly GameAction[] readonly difficulties: readonly Difficulty[] @@ -205,8 +126,10 @@ export interface GameTheme { readonly commonText?: { readonly [key: string]: RenderableText | undefined } readonly narratorName: string - readonly topText: PlayerText - readonly bottomText: PlayerText + + readonly text: { + readonly [x in PlayerSide]: PlayerText + } } export interface TriggeredText { @@ -257,10 +180,10 @@ export interface ActionText { } export interface GameAction { - // The text from the top's perspective, and for the top's actions - readonly topText: ActionText - // The text from the bottom's perspective, and for the bottom's actions - readonly bottomText: ActionText + // The text from each player's perspective. + readonly text: { + readonly [x in PlayerSide]: ActionText + } // The dice that are rolled for this action readonly dice: readonly DieType[] @@ -276,6 +199,11 @@ export interface GameAction { readonly canFinishBottom: boolean } +export enum PlayerSide { + TOP = 'top', + BOTTOM = 'bottom', +} + export interface PlayerState { // This player's name readonly name: string @@ -301,49 +229,45 @@ export interface PlayerState { readonly timesRecovered: number } -export enum GameState { - ONGOING = 'ongoing', +export enum GamePhase { + ROUND_START = 'round_start', + TURN_START = 'turn_start', + TURN_ROLLED = 'turn_rolled', + TURN_FAILED = 'turn_failed', ABORTED = 'aborted', - TOP_WINS = 'top_wins', - BOTTOM_WINS = 'bottom_wins', + VICTORY = 'victory', } -export interface GameData { - // The version of the serialization format this data was saved with. - readonly version: number - // Whether the game is still ongoing, or if not, who won (if anyone). - readonly gameState: GameState - - // The state of the top player - readonly top: PlayerState - // The state of the bottom player - readonly bottom: PlayerState - +export interface GameState { // The theme that defines this game for the players. readonly theme: GameTheme // The difficulty selected for this game. readonly difficulty: Difficulty - // If true, the top chose/is choosing the action this round. - readonly isTopRound: boolean - // The action chosen for this round, or null if the current player has not chosen an action yet. - // The action is locked in if lastRoll is not null or lastTurnTotal is not 0. - readonly action: GameAction | null + // The current phase of the game. + readonly gamePhase: GamePhase + // The owner of the current phase, or null if the phase is Aborted + readonly phaseOwner: PlayerSide | null + + // The state of each player in the game. + readonly players: { + readonly [x in PlayerSide]: PlayerState + } - // If true, the top is rolling this turn. - readonly isTopTurn: boolean + // The action chosen for this round. Defaults to the first action in the list. + readonly action: GameAction // The total for the previous turn, or 0 if the current player is taking the first turn // If the current player fails when the turn total is 0, they take the penalty and pass the initiative without taking damage. // This can still end the game if the current action is capable of ending the game for this player and // this player has more damage than their maximum. readonly lastTurnTotal: number - // The dice available for the current turn, or null if the current player has not rolled yet this turn. - readonly lastRoll: readonly DieResult[] | null + // The dice available for the current turn + readonly lastRoll: readonly DieResult[] // The total value of the selected dice, or 0 if the selected dice cannot be scored. readonly selectedDiceValue: number // Whether the selected dice are sufficient to end the turn. readonly selectedDiceEndTurn: boolean - // The number of dice that have come up failures so far in the current turn + // The number of dice that have come up failures so far in the current turn, including the lastRoll. readonly countedFails: number // The total of the dice that have been scored so far this turn readonly currentTurnTotal: number diff --git a/src/game/handleEvent.ts b/src/game/handleEvent.ts new file mode 100644 index 0000000..49b8206 --- /dev/null +++ b/src/game/handleEvent.ts @@ -0,0 +1,198 @@ +import { ErrorType, EventResult, IncomingEvent, IncomingEventType } from './gameEvent' +import { GamePhase, GameState } from './gameState' +import { DieResult, DieState, isSelectedState, setDeselected, setSelected, toggleSelected } from './dieState' +import { isValidIndex } from '../util/precondition' + +export function handleEvent(state: GameState, event: IncomingEvent): EventResult { + try { + switch (event.type) { + case IncomingEventType.SelectAction: + return handleSelectActionEvent(state, event) + case IncomingEventType.ToggleSelectDie: + return handleToggleSelectIndexEvent(state, event) + case IncomingEventType.SelectAllDice: + return handleSelectAllEvent(state, true) + case IncomingEventType.DeselectAllDice: + return handleSelectAllEvent(state, false) + case IncomingEventType.HoldSelectedDice: + return handleHoldDiceEvent(state, true) + case IncomingEventType.ReleaseSelectedDice: + return handleHoldDiceEvent(state, false) + case IncomingEventType.Abort: + return handleAbortEvent(state) + default: + return { + newState: state, + error: { + type: ErrorType.UnexpectedError, + message: 'Event type not yet implemented', + }, + } + } + } catch (e) { + return { + newState: state, + error: { + type: ErrorType.UnexpectedError, + message: `Failed handling event: ${e}`, + }, + } + } +} + +export interface SelectActionState { + readonly gamePhase: GamePhase + readonly theme: { + readonly actions: readonly A[] + } + readonly action: A +} + +export function handleSelectActionEvent>( + state: T, + { index }: { readonly index: number }, +): EventResult { + if (state.gamePhase !== GamePhase.ROUND_START) { + return { + error: { + type: ErrorType.NotValidRightNow, + message: `You can only change actions when the turn has not started yet.`, + }, + newState: state, + } + } + if (index < 0 || index >= state.theme.actions.length || index !== Math.floor(index)) { + return { + error: { + type: ErrorType.InvalidIndex, + message: `Select an integer action index between 0 (inclusive) and ${state.theme.actions.length} (exclusive)`, + }, + newState: state, + } + } + return { + newState: { + ...state, + action: state.theme.actions[index], + }, + events: [], + } +} + +export interface DiceStatesState { + readonly gamePhase: GamePhase + readonly lastRoll: readonly D[] +} + +export function handleToggleSelectIndexEvent>( + state: T, + { index }: { readonly index: number }, +): EventResult { + if (state.gamePhase !== GamePhase.TURN_ROLLED) { + return { + error: { + type: ErrorType.NotValidRightNow, + message: `You can only toggle the selected state of a die when dice have been rolled.`, + }, + newState: state, + } + } + if (!isValidIndex(index, state.lastRoll.length)) { + return { + newState: state, + error: { + type: ErrorType.InvalidIndex, + message: `Select an integer action index between 0 (inclusive) and ${state.lastRoll.length} (exclusive)`, + }, + } + } + return { + newState: { + ...state, + lastRoll: [ + ...state.lastRoll.slice(0, index), + { + ...state.lastRoll[index], + state: toggleSelected(state.lastRoll[index].state), + }, + ...state.lastRoll.slice(index + 1), + ], + }, + events: [], + } +} + +function handleSelectAllEvent>( + state: T, + select: boolean, +): EventResult { + if (state.gamePhase !== GamePhase.TURN_ROLLED) { + return { + error: { + type: ErrorType.NotValidRightNow, + message: `You can only change the selected state of the dice when dice have been rolled.`, + }, + newState: state, + } + } + return { + newState: { + ...state, + lastRoll: state.lastRoll.map((x) => ({ + ...x, + state: select ? setSelected(x.state) : setDeselected(x.state), + })), + }, + events: [], + } +} + +function handleHoldDiceEvent>( + state: T, + hold: boolean, +): EventResult { + if (state.gamePhase !== GamePhase.TURN_ROLLED) { + return { + error: { + type: ErrorType.NotValidRightNow, + message: `You can only toggle the hold state of dice when dice have been rolled.`, + }, + newState: state, + } + } + return { + newState: { + ...state, + lastRoll: state.lastRoll.map((v) => { + if (isSelectedState(v.state)) { + return { + ...v, + state: hold ? DieState.HELD_SELECTED : DieState.SELECTED, + } + } else { + return v + } + }), + }, + events: [], + } +} + +function handleAbortEvent(state: T): EventResult { + if (state.gamePhase === GamePhase.ABORTED || state.gamePhase === GamePhase.VICTORY) { + return { + error: { + type: ErrorType.NotValidRightNow, + message: `The game is already over.`, + }, + newState: state, + } + } + return { + newState: { + ...state, + gamePhase: GamePhase.ABORTED, + }, + events: [], + } +} diff --git a/src/util/precondition.ts b/src/util/precondition.ts new file mode 100644 index 0000000..f5b9803 --- /dev/null +++ b/src/util/precondition.ts @@ -0,0 +1,3 @@ +export function isValidIndex(index: number, length: number): boolean { + return Number.isInteger(index) && index >= 0 && index < length +} diff --git a/webpack.config.js b/webpack.config.js index edd2c1c..9fc6575 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,32 +1,32 @@ -const path = require('path'); +import { join } from 'path' -module.exports = { - output: { - filename: 'worker.js', - path: path.join(__dirname, 'dist') - }, - mode: 'production', - resolve: { - extensions: ['.ts', '.tsx', '.js'], - plugins: [], - fallback: { - zlib: false, - https: false, - fs: false, - fastify: false, - express: false, - path: false - } - }, - module: { - rules: [ - { - test: /\.tsx?$/, - loader: 'ts-loader', - options: { - transpileOnly: true - } - } - ] - } -}; +export default { + output: { + filename: 'worker.js', + path: join(__dirname, 'dist'), + }, + mode: 'production', + resolve: { + extensions: ['.ts', '.tsx', '.js'], + plugins: [], + fallback: { + zlib: false, + https: false, + fs: false, + fastify: false, + express: false, + path: false, + }, + }, + module: { + rules: [ + { + test: /\.tsx?$/, + loader: 'ts-loader', + options: { + transpileOnly: true, + }, + }, + ], + }, +}