parent
d312577036
commit
381ca59c82
@ -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> |
@ -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]>([ |
@ -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 |
||||
} |
@ -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 = { |
||||
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, |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
} |
||||
|
Loading…
Reference in new issue