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 { 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 |
||||||
|
} |
@ -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…
Reference in new issue