From da0b307013b62f6e726f988881d72d48901b25c5 Mon Sep 17 00:00:00 2001 From: Jason Reed Date: Thu, 23 Nov 2023 10:58:24 -0500 Subject: [PATCH] Bonus to copy existing tile from board (fix #65) --- public/assets/copy-cursor.png | Bin 0 -> 2280 bytes public/assets/toolbar.png | Bin 4321 -> 5030 bytes src/app.tsx | 3 +++ src/core/bonus.ts | 42 ++++++++++++++++++++-------------- src/core/intent.ts | 23 +++++++++++++++++-- src/core/reduce.ts | 1 + src/core/shortcuts.ts | 5 ++++ src/core/state-helpers.ts | 9 ++++++++ src/core/state.ts | 2 ++ src/core/tools.ts | 9 ++++++-- src/ui/drawBonus.ts | 5 +++- src/ui/instructions.tsx | 1 + src/ui/render.ts | 19 +++++++++------ 13 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 public/assets/copy-cursor.png diff --git a/public/assets/copy-cursor.png b/public/assets/copy-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..12a5f49a297f608d8803234e480b254de4dc4360 GIT binary patch literal 2280 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

^vns3)wx7r03eMYK!4A30*%y4hHj-n= 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 diff --git a/src/app.tsx b/src/app.tsx index a65bc73..1155560 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -152,6 +152,9 @@ export function Game(props: GameProps): JSX.Element { if (tool == 'bomb') { return 'url(assets/bomb-cursor.png) 16 16, pointer'; } + if (tool == 'copy') { + return 'url(assets/copy-cursor.png) 16 16, pointer'; + } if (tool == 'hand') { return 'grab'; } diff --git a/src/core/bonus.ts b/src/core/bonus.ts index ed52a14..9241710 100644 --- a/src/core/bonus.ts +++ b/src/core/bonus.ts @@ -7,14 +7,19 @@ import { Layer, mkLayer } from './layer'; import { CoreState, Tile, TileEntity } from './state'; import { MoveTile } from './state-helpers'; -export type Bonus = +export type ScoringBonus = | { t: 'bonus' } | { t: 'bomb' } - | { t: 'empty' } - | { t: 'block' } | { t: 'required', letter: string } | { t: 'consonant' } | { t: 'vowel' } + | { t: 'copy' } + ; + +export type Bonus = + | ScoringBonus + | { t: 'empty' } + | { t: 'block' } ; export function bonusGenerator(p: Point, seed: number): Bonus { @@ -44,6 +49,9 @@ export function bonusGenerator(p: Point, seed: number): Bonus { if (ph < 0.5) { return { t: 'required', letter: deterministicLetterSample(ph * 1e9) }; } + else if (ph < 0.53) { + return { t: 'copy' }; + } else { return { t: 'block' }; } @@ -64,40 +72,40 @@ export function isBlocking(tile: MoveTile, bonus: Bonus): boolean { return true; } -type Scoring = - | { t: 'bonus', p: Point } - | { t: 'bomb', p: Point } - | { t: 'required', p: Point } - | { t: 'vowel', p: Point } - | { t: 'consonant', p: Point } - ; +type Scoring = { + bonus: ScoringBonus, + p: Point +}; export function adjacentScoringOfBonus(bonus: Bonus, p: Point): Scoring[] { switch (bonus.t) { - case 'bonus': return [{ t: 'bonus', p }]; - case 'bomb': return [{ t: 'bomb', p }]; - case 'vowel': return [{ t: 'vowel', p }]; - case 'consonant': return [{ t: 'consonant', p }]; + case 'bonus': return [{ bonus, p }]; + case 'bomb': return [{ bonus, p }]; + case 'vowel': return [{ bonus, p }]; + case 'consonant': return [{ bonus, p }]; + case 'copy': return [{ bonus, p }]; default: return []; } } export function overlapScoringOfBonus(bonus: Bonus, p: Point): Scoring[] { switch (bonus.t) { - case 'required': return [{ t: 'required', p }]; + case 'required': return [{ bonus, p }]; default: return []; } } export function resolveScoring(state: Draft, scoring: Scoring): void { - switch (scoring.t) { + const bonus = scoring.bonus; + switch (bonus.t) { case 'bonus': state.score++; return; case 'bomb': state.inventory.bombs++; return; case 'required': state.score += 10; return; case 'vowel': state.inventory.vowels += 5; return case 'consonant': state.inventory.consonants += 5; return + case 'copy': state.inventory.copies += 3; return } - unreachable(scoring); + unreachable(bonus); } // Bonus Layer Generation diff --git a/src/core/intent.ts b/src/core/intent.ts index e802ca6..1f3c3b3 100644 --- a/src/core/intent.ts +++ b/src/core/intent.ts @@ -3,10 +3,11 @@ import { produce } from '../util/produce'; import { vm } from '../util/vutil'; import { GameState, TileEntity } from './state'; import { tryKillTileOfState } from './kill-helpers'; -import { Tool, bombIntent, dynamiteIntent } from './tools'; +import { Tool, bombIntent, copyIntent, dynamiteIntent } from './tools'; import { SelectionOperation, selectionOperationOfMods } from './selection'; import { vacuous_down, deselect } from './reduce'; -import { withCoreState } from './state-helpers'; +import { drawSpecificOfState, withCoreState } from './state-helpers'; +import { tileAtPoint } from './tile-helpers'; export type KillIntent = | { t: 'kill', radius: number, cost: number } @@ -18,6 +19,7 @@ export type Intent = | { t: 'panWorld' } | { t: 'exchangeTiles', id: string } | { t: 'startSelection', opn: SelectionOperation } + | { t: 'copy' } | KillIntent ; @@ -50,6 +52,7 @@ export function getIntentOfMouseDown(tool: Tool, wp: WidgetPoint, button: number return bombIntent; case 'vowel': throw new Error(`shoudn't be able have vowel tool active`); case 'consonant': throw new Error(`shoudn't be able have consonant tool active`); + case 'copy': return copyIntent; } } @@ -110,5 +113,21 @@ export function reduceIntent(state: GameState, intent: Intent, wp: WidgetPoint): opn: intent.opn, }; }); + case 'copy': { + if (wp.t == 'world') { + const hoverTile = tileAtPoint(state.coreState, wp.p_in_local); + if (hoverTile == undefined) + return state; + const newCs = drawSpecificOfState(state.coreState, hoverTile.letter); + if (newCs == state.coreState) return state; + return withCoreState(state, cs => produce(newCs, s => { + s.inventory.copies--; + })); + } + else { + return state; + } + + } } } diff --git a/src/core/reduce.ts b/src/core/reduce.ts index 68399ee..48b4a2f 100644 --- a/src/core/reduce.ts +++ b/src/core/reduce.ts @@ -275,6 +275,7 @@ function reduceGameAction(state: GameState, action: GameAction): effectful.Resul s.inventory.bombs = 15; s.inventory.vowels = 15; s.inventory.consonants = 15; + s.inventory.copies = 15; })))); } return gs(state); diff --git a/src/core/shortcuts.ts b/src/core/shortcuts.ts index 55b255c..5eb4d6b 100644 --- a/src/core/shortcuts.ts +++ b/src/core/shortcuts.ts @@ -34,5 +34,10 @@ export function tryReduceShortcut(state: GameState, code: string): GameState | u return withCoreState(state, cs => reduceToolSelect(cs, 'consonant')); } } + if (code == 'x') { + if (state.coreState.inventory.copies >= 1) { + return withCoreState(state, cs => reduceToolSelect(cs, 'copy')); + } + } return undefined; } diff --git a/src/core/state-helpers.ts b/src/core/state-helpers.ts index 9e9d5c6..eb2a424 100644 --- a/src/core/state-helpers.ts +++ b/src/core/state-helpers.ts @@ -65,6 +65,15 @@ export function drawOfState(state: CoreState, drawForce?: DrawForce): CoreState })); } +export function drawSpecificOfState(state: CoreState, letter: string): CoreState { + const handLength = get_hand_tiles(state).length; + if (handLength >= HAND_TILE_LIMIT) + return state; + return checkValid(produce(state, s => { + addHandTile(s, ensureTileId({ letter, p_in_world_int: { x: 0, y: handLength } })); + })); +} + const directions: Point[] = [[1, 0], [-1, 0], [0, 1], [0, -1]].map(([x, y]) => ({ x, y })); export function resolveValid(state: CoreState): CoreState { diff --git a/src/core/state.ts b/src/core/state.ts index df42d5b..0dc3f1d 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -94,6 +94,7 @@ export type CoreState = { bombs: number, vowels: number, consonants: number, + copies: number, } bonusLayerName: string, }; @@ -139,6 +140,7 @@ export function mkGameState(seed?: number): GameState { bombs: 0, vowels: 0, consonants: 0, + copies: 0, }, bonusLayerName: 'game', }, diff --git a/src/core/tools.ts b/src/core/tools.ts index 2274560..78abf89 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -13,6 +13,7 @@ const tools = [ 'bomb', 'vowel', 'consonant', + 'copy', ] as const; export type Tool = (typeof tools)[number]; @@ -35,6 +36,7 @@ export function getCurrentTool(state: CoreState): Tool { export const dynamiteIntent: Intent & { t: 'kill' } = { t: 'kill', radius: 0, cost: 1 }; export const BOMB_RADIUS = 2; export const bombIntent: Intent & { t: 'bomb' } = { t: 'bomb' }; +export const copyIntent: Intent & { t: 'copy' } = { t: 'copy' }; export function getCurrentTools(state: CoreState): Tool[] { if (state.lost) { @@ -53,6 +55,9 @@ export function getCurrentTools(state: CoreState): Tool[] { if (state.inventory.consonants > 0) { tools.push('consonant'); } + if (state.inventory.copies > 0) { + tools.push('copy'); + } return tools; } @@ -66,14 +71,14 @@ export function reduceToolSelect(state: CoreState, tool: Tool): CoreState { switch (tool) { case 'consonant': { const newState = drawOfState(state, 'consonant'); - if (newState == state) return newState; + if (newState == state) return state; return produce(newState, s => { s.inventory.consonants--; }); } case 'vowel': { const newState = drawOfState(state, 'vowel'); - if (newState == state) return newState; + if (newState == state) return state; return produce(newState, s => { s.inventory.vowels--; }); diff --git a/src/ui/drawBonus.ts b/src/ui/drawBonus.ts index 0e970ff..a405f3a 100644 --- a/src/ui/drawBonus.ts +++ b/src/ui/drawBonus.ts @@ -58,7 +58,10 @@ export function drawBonus(d: CanvasRenderingContext2D, bonus: Bonus, pan_canvas_ drawImage(d, toolbarImg, rectOfTool('vowel'), rect_in_canvas); return; } - + case 'copy': { + drawImage(d, toolbarImg, rectOfTool('copy'), rect_in_canvas); + return; + } } unreachable(bonus); } diff --git a/src/ui/instructions.tsx b/src/ui/instructions.tsx index 1564e3d..4b51444 100644 --- a/src/ui/instructions.tsx +++ b/src/ui/instructions.tsx @@ -139,6 +139,7 @@ function exampleState(): GameState { bombs: 3, vowels: 0, consonants: 0, + copies: 0, }, }, mouseState: { diff --git a/src/ui/render.ts b/src/ui/render.ts index a01980c..3fa6c1b 100644 --- a/src/ui/render.ts +++ b/src/ui/render.ts @@ -83,6 +83,9 @@ function drawToolbar(d: CanvasRenderingContext2D, state: CoreState): void { else if (tool == 'consonant') { drawToolbarCount(d, rect_in_canvas, state.inventory.consonants); } + else if (tool == 'copy') { + drawToolbarCount(d, rect_in_canvas, state.inventory.copies); + } // indicate current tool if (tool == currentTool) { @@ -193,13 +196,15 @@ export function rawPaint(ci: CanvasInfo, state: GameState) { } function drawPauseButton() { - d.textAlign = 'center'; - d.textBaseline = 'middle'; - if (!cs.lost) { - fillText(d, '⏸', midpointOfRect(pause_button_bds_in_canvas), 'black', '48px sans-serif'); - } - else { - fillText(d, '⟳', midpointOfRect(pause_button_bds_in_canvas), 'black', '48px sans-serif'); + if (cs.panic) { + d.textAlign = 'center'; + d.textBaseline = 'middle'; + if (!cs.lost) { + fillText(d, '⏸', midpointOfRect(pause_button_bds_in_canvas), 'black', '48px sans-serif'); + } + else { + fillText(d, '⟳', midpointOfRect(pause_button_bds_in_canvas), 'black', '48px sans-serif'); + } } }