Progress on incoming event handling

main
Mari 1 year ago
parent d312577036
commit 381ca59c82
  1. 46
      .eslintrc.cjs
  2. 46
      .eslintrc.js
  3. 6
      .idea/jsLibraryMappings.xml
  4. 30
      slash-up.config.js
  5. 2
      src/game/dieState.spec.ts
  6. 130
      src/game/dieState.ts
  7. 53
      src/game/gameEvent.ts
  8. 162
      src/game/gameState.ts
  9. 198
      src/game/handleEvent.ts
  10. 3
      src/util/precondition.ts
  11. 62
      webpack.config.js

@ -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,
},
},
],
}

@ -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
}
}
]
};

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

@ -2,18 +2,18 @@
// Make sure to fill in "token" and "applicationId" before using. // Make sure to fill in "token" and "applicationId" before using.
// You can also use environment variables from the ".env" file if any. // You can also use environment variables from the ".env" file if any.
module.exports = { export default {
// The Token of the Discord bot // The Token of the Discord bot
token: process.env.DISCORD_BOT_TOKEN, token: process.env.DISCORD_BOT_TOKEN,
// The Application ID of the Discord bot // The Application ID of the Discord bot
applicationId: process.env.DISCORD_APP_ID, applicationId: process.env.DISCORD_APP_ID,
// This is where the path to command files are, .ts files are supported! // This is where the path to command files are, .ts files are supported!
commandPath: './src/commands', commandPath: './src/commands',
// You can use different environments with --env (-e) // You can use different environments with --env (-e)
env: { env: {
development: { development: {
// The "globalToGuild" option makes global commands sync to the specified guild instead. // The "globalToGuild" option makes global commands sync to the specified guild instead.
globalToGuild: process.env.DEVELOPMENT_GUILD_ID globalToGuild: process.env.DEVELOPMENT_GUILD_ID,
} },
} },
}; }

@ -1,5 +1,5 @@
import { describe, expect, test } from '@jest/globals' 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', () => { describe('isHeldState', () => {
test.each<[DieState, boolean]>([ test.each<[DieState, boolean]>([

@ -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
}

@ -1,30 +1,40 @@
import { DieResult, GameData } from './gameData' import { DiceCombo, DieResult } from './dieState'
export enum IncomingEventType { export enum IncomingEventType {
// Chooses the action to be performed for this round but does not lock it in until the player rolls. // 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', SelectAction = 'select_action',
// Dice buttons toggle the selection of that die // Dice buttons toggle the selection of that die
// Valid during GameState.TURN_ROLLED
ToggleSelectDie = 'toggle_select_die', ToggleSelectDie = 'toggle_select_die',
// Button 1 is Select All/Deselect All // Button 1 is Select All/Deselect All
// Select All appears if no dice are selected // Select All appears if no dice are selected
// Deselect All displays if at least one die is selected // Deselect All displays if at least one die is selected
// Both valid during GameState.TURN_ROLLED
SelectAllDice = 'select_all_dice', SelectAllDice = 'select_all_dice',
DeselectAllDice = 'deselect_all_dice', DeselectAllDice = 'deselect_all_dice',
// Button 2 is Hold/Release selected dice // Button 2 is Hold/Release selected dice
// If only held dice are selected, Release appears // If only held dice are selected, Release appears
// For selections with at least one unheld die, Hold appears // For selections with at least one unheld die, Hold appears
// Both valid during GameState.TURN_ROLLED
HoldSelectedDice = 'hold_selected_dice', HoldSelectedDice = 'hold_selected_dice',
ReleaseSelectedDice = 'release_selected_dice', ReleaseSelectedDice = 'release_selected_dice',
// Button 3 is End Turn/Score 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 // End Turn appears by default, but is only enabled if sufficient Stop dice are in the current roll or if the turn
// and no non-Stop dice are selected // has been failed
// Score Selected Dice appears otherwise, but is only enabled if at least one scoreable die is selected and no // Score Selected Dice appears when any dice are selected, but is only enabled if at least one scoreable die is
// non-scoreable dice are selected // 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', EndTurn = 'end_turn',
ScoreSelectedDice = 'score_selected_dice', ScoreSelectedDice = 'score_selected_dice',
// Button 4 is Roll 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', 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 { export interface IncomingBaseEvent {
@ -46,6 +56,7 @@ export interface IncomingSimpleEvent extends IncomingBaseEvent {
| IncomingEventType.EndTurn | IncomingEventType.EndTurn
| IncomingEventType.DeselectAllDice | IncomingEventType.DeselectAllDice
| IncomingEventType.RollDice | IncomingEventType.RollDice
| IncomingEventType.Abort
} }
export type IncomingEvent = IncomingIndexedEvent | IncomingSimpleEvent export type IncomingEvent = IncomingIndexedEvent | IncomingSimpleEvent
@ -74,6 +85,7 @@ export enum OutgoingEventType {
ScoreDice = 'score_dice', ScoreDice = 'score_dice',
GainFailures = 'gain_failures', GainFailures = 'gain_failures',
SafeEndTurn = 'safe_end_turn',
LoseRound = 'lose_round', LoseRound = 'lose_round',
LoseGame = 'lose_game', LoseGame = 'lose_game',
@ -83,11 +95,6 @@ export enum OutgoingEventType {
StartRound = 'new_round', StartRound = 'new_round',
} }
export enum EventTarget {
Top = 'top',
Bottom = 'bottom',
}
export enum TextSender { export enum TextSender {
Top = 'top', Top = 'top',
Bottom = 'bottom', Bottom = 'bottom',
@ -114,6 +121,7 @@ export interface OutgoingBaseTargetedEvent extends OutgoingBaseEvent {
| OutgoingEventType.LoseGame | OutgoingEventType.LoseGame
| OutgoingEventType.ScoreDice | OutgoingEventType.ScoreDice
| OutgoingEventType.GainFailures | OutgoingEventType.GainFailures
| OutgoingEventType.SafeEndTurn
| OutgoingEventType.StartRoll | OutgoingEventType.StartRoll
| OutgoingEventType.StartTurn | OutgoingEventType.StartTurn
| OutgoingEventType.StartRound | OutgoingEventType.StartRound
@ -121,7 +129,11 @@ export interface OutgoingBaseTargetedEvent extends OutgoingBaseEvent {
} }
export interface OutgoingSimpleTargetedEvent extends OutgoingBaseTargetedEvent { 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 { export interface OutgoingDamageEvent extends OutgoingBaseTargetedEvent {
@ -137,12 +149,13 @@ export interface OutgoingDamageEvent extends OutgoingBaseTargetedEvent {
export interface OutgoingStartRollEvent extends OutgoingBaseTargetedEvent { export interface OutgoingStartRollEvent extends OutgoingBaseTargetedEvent {
readonly type: OutgoingEventType.StartRoll readonly type: OutgoingEventType.StartRoll
readonly newDice: DieResult[] readonly newDice: readonly DieResult[]
} }
export interface OutgoingScoreDiceEvent extends OutgoingBaseTargetedEvent { export interface OutgoingScoreDiceEvent extends OutgoingBaseTargetedEvent {
readonly type: OutgoingEventType.ScoreDice readonly type: OutgoingEventType.ScoreDice
readonly scoredDice: DieResult[] readonly scoredCombo: DiceCombo
readonly scoredDice: readonly DieResult[]
readonly scoreDelta: number readonly scoreDelta: number
} }
@ -153,16 +166,16 @@ export type OutgoingEvent =
| OutgoingStartRollEvent | OutgoingStartRollEvent
| OutgoingScoreDiceEvent | OutgoingScoreDiceEvent
export interface BaseEventResult { export interface BaseEventResult<T> {
readonly newState: GameData readonly newState: T
} }
export interface SuccessResult extends BaseEventResult { export interface SuccessResult<T> extends BaseEventResult<T> {
readonly events: OutgoingEvent[] readonly events: readonly OutgoingEvent[]
} }
export interface FailedResult extends BaseEventResult { export interface FailedResult<T> extends BaseEventResult<T> {
readonly error: GameError readonly error: GameError
} }
export type EventResult = SuccessResult | FailedResult export type EventResult<T> = SuccessResult<T> | FailedResult<T>

@ -1,88 +1,5 @@
import { RenderableText } from '../util/renderableText' import { RenderableText } from '../util/renderableText'
import { DiceCombo, DieResult, DieType } from './dieState'
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[]
}
export interface AIWeights { export interface AIWeights {
// How much this AI likes to choose actions that increase the damage bonus // How much this AI likes to choose actions that increase the damage bonus
@ -165,11 +82,13 @@ export interface Difficulty {
readonly shortDescription?: string readonly shortDescription?: string
// Description of the difficulty when selected // Description of the difficulty when selected
readonly description?: string readonly description?: string
// The player who should start in this difficulty; defaults to random
readonly startingPlayer?: PlayerSide | null
// Starting values for the top // Starting values for each character
readonly topStats: PlayerStartingState readonly stats: {
// Starting values for the bottom readonly [x in PlayerSide]: PlayerStartingState
readonly bottomStats: PlayerStartingState }
} }
export interface PlayerText { export interface PlayerText {
@ -198,6 +117,8 @@ export interface GameTheme {
readonly shortDescription: string readonly shortDescription: string
readonly description: string readonly description: string
readonly diceCombos: readonly DiceCombo[]
readonly actions: readonly GameAction[] readonly actions: readonly GameAction[]
readonly difficulties: readonly Difficulty[] readonly difficulties: readonly Difficulty[]
@ -205,8 +126,10 @@ export interface GameTheme {
readonly commonText?: { readonly [key: string]: RenderableText | undefined } readonly commonText?: { readonly [key: string]: RenderableText | undefined }
readonly narratorName: string readonly narratorName: string
readonly topText: PlayerText
readonly bottomText: PlayerText readonly text: {
readonly [x in PlayerSide]: PlayerText
}
} }
export interface TriggeredText { export interface TriggeredText {
@ -257,10 +180,10 @@ export interface ActionText {
} }
export interface GameAction { export interface GameAction {
// The text from the top's perspective, and for the top's actions // The text from each player's perspective.
readonly topText: ActionText readonly text: {
// The text from the bottom's perspective, and for the bottom's actions readonly [x in PlayerSide]: ActionText
readonly bottomText: ActionText }
// The dice that are rolled for this action // The dice that are rolled for this action
readonly dice: readonly DieType[] readonly dice: readonly DieType[]
@ -276,6 +199,11 @@ export interface GameAction {
readonly canFinishBottom: boolean readonly canFinishBottom: boolean
} }
export enum PlayerSide {
TOP = 'top',
BOTTOM = 'bottom',
}
export interface PlayerState { export interface PlayerState {
// This player's name // This player's name
readonly name: string readonly name: string
@ -301,49 +229,45 @@ export interface PlayerState {
readonly timesRecovered: number readonly timesRecovered: number
} }
export enum GameState { export enum GamePhase {
ONGOING = 'ongoing', ROUND_START = 'round_start',
TURN_START = 'turn_start',
TURN_ROLLED = 'turn_rolled',
TURN_FAILED = 'turn_failed',
ABORTED = 'aborted', ABORTED = 'aborted',
TOP_WINS = 'top_wins', VICTORY = 'victory',
BOTTOM_WINS = 'bottom_wins',
} }
export interface GameData { export interface GameState {
// 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
// The theme that defines this game for the players. // The theme that defines this game for the players.
readonly theme: GameTheme readonly theme: GameTheme
// The difficulty selected for this game. // The difficulty selected for this game.
readonly difficulty: Difficulty readonly difficulty: Difficulty
// If true, the top chose/is choosing the action this round. // The current phase of the game.
readonly isTopRound: boolean readonly gamePhase: GamePhase
// The action chosen for this round, or null if the current player has not chosen an action yet. // The owner of the current phase, or null if the phase is Aborted
// The action is locked in if lastRoll is not null or lastTurnTotal is not 0. readonly phaseOwner: PlayerSide | null
readonly action: GameAction | null
// The state of each player in the game.
readonly players: {
readonly [x in PlayerSide]: PlayerState
}
// If true, the top is rolling this turn. // The action chosen for this round. Defaults to the first action in the list.
readonly isTopTurn: boolean readonly action: GameAction
// The total for the previous turn, or 0 if the current player is taking the first turn // 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. // 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 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. // this player has more damage than their maximum.
readonly lastTurnTotal: number readonly lastTurnTotal: number
// The dice available for the current turn, or null if the current player has not rolled yet this turn. // The dice available for the current turn
readonly lastRoll: readonly DieResult[] | null readonly lastRoll: readonly DieResult[]
// The total value of the selected dice, or 0 if the selected dice cannot be scored. // The total value of the selected dice, or 0 if the selected dice cannot be scored.
readonly selectedDiceValue: number readonly selectedDiceValue: number
// Whether the selected dice are sufficient to end the turn. // Whether the selected dice are sufficient to end the turn.
readonly selectedDiceEndTurn: boolean 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 readonly countedFails: number
// The total of the dice that have been scored so far this turn // The total of the dice that have been scored so far this turn
readonly currentTurnTotal: number readonly currentTurnTotal: number

@ -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<GameState> {
try {
switch (event.type) {
case IncomingEventType.SelectAction:
return handleSelectActionEvent<GameState>(state, event)
case IncomingEventType.ToggleSelectDie:
return handleToggleSelectIndexEvent<DieResult, GameState>(state, event)
case IncomingEventType.SelectAllDice:
return handleSelectAllEvent<DieResult, GameState>(state, true)
case IncomingEventType.DeselectAllDice:
return handleSelectAllEvent<DieResult, GameState>(state, false)
case IncomingEventType.HoldSelectedDice:
return handleHoldDiceEvent<DieResult, GameState>(state, true)
case IncomingEventType.ReleaseSelectedDice:
return handleHoldDiceEvent<DieResult, GameState>(state, false)
case IncomingEventType.Abort:
return handleAbortEvent<GameState>(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<A> {
readonly gamePhase: GamePhase
readonly theme: {
readonly actions: readonly A[]
}
readonly action: A
}
export function handleSelectActionEvent<T extends SelectActionState<unknown>>(
state: T,
{ index }: { readonly index: number },
): EventResult<T> {
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<D extends { readonly state: DieState }> {
readonly gamePhase: GamePhase
readonly lastRoll: readonly D[]
}
export function handleToggleSelectIndexEvent<D extends { readonly state: DieState }, T extends DiceStatesState<D>>(
state: T,
{ index }: { readonly index: number },
): EventResult<T> {
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<D extends { readonly state: DieState }, T extends DiceStatesState<D>>(
state: T,
select: boolean,
): EventResult<T> {
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<D>((x) => ({
...x,
state: select ? setSelected(x.state) : setDeselected(x.state),
})),
},
events: [],
}
}
function handleHoldDiceEvent<D extends { readonly state: DieState }, T extends DiceStatesState<D>>(
state: T,
hold: boolean,
): EventResult<T> {
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<D>((v) => {
if (isSelectedState(v.state)) {
return {
...v,
state: hold ? DieState.HELD_SELECTED : DieState.SELECTED,
}
} else {
return v
}
}),
},
events: [],
}
}
function handleAbortEvent<T extends { readonly gamePhase: GamePhase }>(state: T): EventResult<T> {
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: [],
}
}

@ -0,0 +1,3 @@
export function isValidIndex(index: number, length: number): boolean {
return Number.isInteger(index) && index >= 0 && index < length
}

@ -1,32 +1,32 @@
const path = require('path'); import { join } from 'path'
module.exports = { export default {
output: { output: {
filename: 'worker.js', filename: 'worker.js',
path: path.join(__dirname, 'dist') path: join(__dirname, 'dist'),
}, },
mode: 'production', mode: 'production',
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.js'], extensions: ['.ts', '.tsx', '.js'],
plugins: [], plugins: [],
fallback: { fallback: {
zlib: false, zlib: false,
https: false, https: false,
fs: false, fs: false,
fastify: false, fastify: false,
express: false, express: false,
path: false path: false,
} },
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
loader: 'ts-loader', loader: 'ts-loader',
options: { options: {
transpileOnly: true transpileOnly: true,
} },
} },
] ],
} },
}; }

Loading…
Cancel
Save