diff --git a/public/assets/toolbar.png b/public/assets/toolbar.png index c0e8017..32c5fc8 100644 Binary files a/public/assets/toolbar.png and b/public/assets/toolbar.png differ diff --git a/src/core/bonus.ts b/src/core/bonus.ts index 474f690..ed52a14 100644 --- a/src/core/bonus.ts +++ b/src/core/bonus.ts @@ -1,9 +1,10 @@ +import { Draft } from 'immer'; import { Point } from '../util/types'; -import { point_hash } from '../util/util'; +import { point_hash, unreachable } from '../util/util'; import { vsnorm } from '../util/vutil'; import { deterministicLetterSample, getSample } from './distribution'; import { Layer, mkLayer } from './layer'; -import { Tile, TileEntity } from './state'; +import { CoreState, Tile, TileEntity } from './state'; import { MoveTile } from './state-helpers'; export type Bonus = @@ -12,6 +13,8 @@ export type Bonus = | { t: 'empty' } | { t: 'block' } | { t: 'required', letter: string } + | { t: 'consonant' } + | { t: 'vowel' } ; export function bonusGenerator(p: Point, seed: number): Bonus { @@ -20,8 +23,15 @@ export function bonusGenerator(p: Point, seed: number): Bonus { } if (point_hash(p, seed) < 0.1) { const ph = point_hash(p, seed + 1000); - if (ph < 0.1) + if (ph < 0.1) { return { t: 'bomb' }; + } + else if (ph < 0.15) { + return { t: 'consonant' }; + } + else if (ph < 0.2) { + return { t: 'vowel' }; + } else return { t: 'bonus' }; } @@ -54,6 +64,44 @@ 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 } + ; + +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 }]; + default: return []; + } +} + +export function overlapScoringOfBonus(bonus: Bonus, p: Point): Scoring[] { + switch (bonus.t) { + case 'required': return [{ t: 'required', p }]; + default: return []; + } +} + +export function resolveScoring(state: Draft, scoring: Scoring): void { + switch (scoring.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 + } + unreachable(scoring); +} + +// Bonus Layer Generation + export type BonusLayerId = string; const _cachedBonusLayer: Record> = {}; diff --git a/src/core/distribution.ts b/src/core/distribution.ts index 1e51770..953788c 100644 --- a/src/core/distribution.ts +++ b/src/core/distribution.ts @@ -5,6 +5,8 @@ import { CoreState, GameState } from "./state"; type LetterClass = 0 | 1; +export type DrawForce = undefined | 'vowel' | 'consonant'; + export function getClass(index: number): LetterClass { return [0, 4, 8, 14, 20].includes(index) ? 0 : 1; } @@ -118,20 +120,30 @@ export function getSample(seed0: number, probs: Probs): { sample: number, seed: return { sample, seed }; } -export function getLetterSampleOf(seed0: number, energies0: Energies, letterDistribution: Record, classDistribution: number[], alphabet: string[], beta: number, increment: number): { seed: number, letter: string, energies: Energies } { +function forceClassSample(classSample: number, drawForce: DrawForce): number { + if (drawForce === undefined) + return classSample; + switch (drawForce) { + case 'vowel': return 0; + case 'consonant': return 1; + } +} + +export function getLetterSampleOf(seed0: number, energies0: Energies, letterDistribution: Record, classDistribution: number[], alphabet: string[], beta: number, increment: number, drawForce: DrawForce): { seed: number, letter: string, energies: Energies } { const { seed: seed1, sample: classSample } = getSample(seed0, distributionOf(energies0.byClass, beta)); - const modifiedLetterEnergies = energies0.byLetter.map((energy, ix) => getClass(ix) == classSample ? energy : Infinity); + const forcedClassSample = forceClassSample(classSample, drawForce); + const modifiedLetterEnergies = energies0.byLetter.map((energy, ix) => getClass(ix) == forcedClassSample ? energy : Infinity); const { seed: seed2, sample: letterSample } = getSample(seed1, distributionOf(modifiedLetterEnergies, beta)); const letter = alphabet[letterSample]; const energies = produce(energies0, e => { - e.byClass[classSample] += increment / classDistribution[classSample]; + e.byClass[forcedClassSample] += increment / classDistribution[forcedClassSample]; e.byLetter[letterSample] += increment / letterDistribution[letter]; }); return { seed: seed2, energies, letter }; } -export function getLetterSample(seed0: number, energies0: Energies): { seed: number, letter: string, energies: Energies } { - return getLetterSampleOf(seed0, energies0, letterDistribution, classDistribution, alphabet, default_beta, default_increment); +export function getLetterSample(seed0: number, energies0: Energies, drawForce: DrawForce): { seed: number, letter: string, energies: Energies } { + return getLetterSampleOf(seed0, energies0, letterDistribution, classDistribution, alphabet, default_beta, default_increment, drawForce); } diff --git a/src/core/reduce.ts b/src/core/reduce.ts index ebead0b..e2e7dae 100644 --- a/src/core/reduce.ts +++ b/src/core/reduce.ts @@ -16,7 +16,7 @@ import { GameState, HAND_TILE_LIMIT, Location, SceneState, SelectionState, TileE import { MoveTile, addWorldTiles, bonusOfStatePoint, checkValid, drawOfState, filterExpiredAnimations, isCollision, isOccupied, isTilePinned, unpauseState } from './state-helpers'; import { tryKillTileOfState } from './kill-helpers'; import { getTileId, get_hand_tiles, get_main_tiles, get_tiles, putTileInHand, putTileInWorld, putTilesInHand, removeAllTiles, setTileLoc } from "./tile-helpers"; -import { Tool, bombIntent, dynamiteIntent, getCurrentTool } from './tools'; +import { Tool, bombIntent, dynamiteIntent, getCurrentTool, reduceToolSelect } from './tools'; function resolveMouseup(state: GameState): GameState { // FIXME: Setting the mouse state to up *before* calling @@ -202,6 +202,8 @@ function getIntentOfMouseDown(tool: Tool, wp: WidgetPoint, button: number, mods: } case 'bomb': 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`); } } @@ -300,7 +302,7 @@ function reduceMouseDownInHand(state: GameState, wp: WidgetPoint & { t: 'hand' } function reduceMouseDownInToolbar(state: GameState, wp: WidgetPoint & { t: 'toolbar' }, button: number, mods: Set): GameState { const tool = wp.tool; if (tool !== undefined) { - return produce(vacuous_down(state, wp), s => { s.coreState.currentTool = wp.tool; }); + return reduceToolSelect(vacuous_down(state, wp), wp.tool); } else { return vacuous_down(state, wp); @@ -357,6 +359,8 @@ function reduceGameAction(state: GameState, action: GameAction): effectful.Resul return gs(checkValid(produce(addWorldTiles(removeAllTiles(state), debugTiles()), s => { s.coreState.score = 1000; s.coreState.inventory.bombs = 15; + s.coreState.inventory.vowels = 15; + s.coreState.inventory.consonants = 15; }))); } return gs(state); diff --git a/src/core/state-helpers.ts b/src/core/state-helpers.ts index 2c3fb2c..e4d8055 100644 --- a/src/core/state-helpers.ts +++ b/src/core/state-helpers.ts @@ -3,12 +3,13 @@ import { logger } from "../util/debug"; import { produce } from "../util/produce"; import { compose, translate } from '../util/se1'; import { Point } from "../util/types"; +import { unreachable } from '../util/util'; import { vadd, vequal } from "../util/vutil"; import { Animation, mkPointDecayAnimation } from './animations'; import { getAssets } from "./assets"; -import { Bonus, getBonusLayer, isBlocking } from "./bonus"; +import { Bonus, adjacentScoringOfBonus, getBonusLayer, isBlocking, overlapScoringOfBonus, resolveScoring } from "./bonus"; import { PauseData, now_in_game } from "./clock"; -import { getLetterSample } from "./distribution"; +import { DrawForce, getLetterSample } from "./distribution"; import { checkConnected, checkGridWords, mkGridOfMainTiles } from "./grid"; import { Layer, Overlay, getOverlayLayer, mkOverlayFrom, overlayAny, overlayPoints, setOverlay } from "./layer"; import { CoreState, GameState, HAND_TILE_LIMIT, Location, Tile, TileEntity } from "./state"; @@ -51,11 +52,11 @@ export function isOccupiedTiles(tiles: TileEntity[], p: Point): boolean { return tiles.some(tile => tile.loc.t == 'world' && vequal(tile.loc.p_in_world_int, p)); } -export function drawOfState(state: GameState): GameState { +export function drawOfState(state: GameState, drawForce?: DrawForce): GameState { const handLength = get_hand_tiles(state).length; if (handLength >= HAND_TILE_LIMIT) return state; - const { letter, energies, seed } = getLetterSample(state.coreState.seed, state.coreState.energies); + const { letter, energies, seed } = getLetterSample(state.coreState.seed, state.coreState.energies, drawForce); return checkValid(produce(state, s => { s.coreState.seed = seed; s.coreState.energies = energies; @@ -65,35 +66,6 @@ export function drawOfState(state: GameState): GameState { const directions: Point[] = [[1, 0], [-1, 0], [0, 1], [0, -1]].map(([x, y]) => ({ x, y })); -export type Scoring = - | { t: 'bonus', p: Point } - | { t: 'bomb', p: Point } - | { t: 'required', p: Point } - ; - -function adjacentScoringOfBonus(bonus: Bonus, p: Point): Scoring[] { - switch (bonus.t) { - case 'bonus': return [{ t: 'bonus', p }]; - case 'bomb': return [{ t: 'bomb', p }]; - default: return []; - } -} - -function overlapScoringOfBonus(bonus: Bonus, p: Point): Scoring[] { - switch (bonus.t) { - case 'required': return [{ t: 'required', p }]; - default: return []; - } -} - -function resolveScoring(state: Draft, scoring: Scoring): void { - switch (scoring.t) { - case 'bonus': state.score++; break; - case 'bomb': state.inventory.bombs++; break; - case 'required': state.score += 10; break; - } -} - export function resolveValid(state: GameState): GameState { const tiles = get_main_tiles(state); logger('words', 'grid valid'); diff --git a/src/core/state.ts b/src/core/state.ts index 4524ed7..c1926d3 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -95,6 +95,8 @@ export type CoreState = { game_from_clock: SE1, inventory: { bombs: number, + vowels: number, + consonants: number, } bonusLayerName: string, }; @@ -138,6 +140,8 @@ export function mkGameState(seed?: number): GameState { game_from_clock: se1.translate(-Date.now()), inventory: { bombs: 0, + vowels: 0, + consonants: 0, }, bonusLayerName: 'game', }, diff --git a/src/core/tools.ts b/src/core/tools.ts index eef5e12..4131090 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -1,6 +1,8 @@ +import { produce } from "../util/produce"; import { Rect } from "../util/types"; import { Intent } from "./reduce"; import { GameState, State } from "./state"; +import { drawOfState } from "./state-helpers"; export const TOOL_IMAGE_WIDTH = 32; @@ -9,6 +11,8 @@ const tools = [ 'hand', 'dynamite', 'bomb', + 'vowel', + 'consonant', ] as const; export type Tool = (typeof tools)[number]; @@ -43,6 +47,12 @@ export function getCurrentTools(state: GameState): Tool[] { if (state.coreState.inventory.bombs > 0) { tools.push('bomb'); } + if (state.coreState.inventory.vowels > 0) { + tools.push('vowel'); + } + if (state.coreState.inventory.consonants > 0) { + tools.push('consonant'); + } return tools; } @@ -51,3 +61,23 @@ export function rectOfTool(tool: Tool): Rect { const ix_in_image = indexOfTool(tool); return { p: { x: 0, y: S_in_image * ix_in_image }, sz: { x: S_in_image, y: S_in_image } }; } + +export function reduceToolSelect(state: GameState, tool: Tool): GameState { + switch (tool) { + case 'consonant': { + const newState = drawOfState(state, 'consonant'); + if (newState == state) return newState; + return produce(newState, s => { + s.coreState.inventory.consonants--; + }); + } + case 'vowel': { + const newState = drawOfState(state, 'vowel'); + if (newState == state) return newState; + return produce(newState, s => { + s.coreState.inventory.vowels--; + }); + } + default: return produce(state, s => { s.coreState.currentTool = tool; }); + } +} diff --git a/src/ui/drawAnimation.ts b/src/ui/drawAnimation.ts index 3273f24..2775ddb 100644 --- a/src/ui/drawAnimation.ts +++ b/src/ui/drawAnimation.ts @@ -4,7 +4,7 @@ import { apply_to_rect } from "../util/se2-extra"; import { Point } from "../util/types"; import { unreachable } from "../util/util"; import { vscale, vsub } from "../util/vutil"; -import { drawBonus } from "./drawBonus"; +import { drawBonusPoint } from "./drawBonus"; export function drawAnimation(d: CanvasRenderingContext2D, pan_canvas_from_world: SE2, time_ms: number, anim: Animation): void { switch (anim.t) { @@ -27,7 +27,7 @@ export function drawAnimation(d: CanvasRenderingContext2D, pan_canvas_from_world } break; case 'point-decay': { const fraction = Math.min(1, Math.max(0, 1 - (time_ms - anim.start_in_game) / anim.duration_ms)); - drawBonus(d, pan_canvas_from_world, anim.p_in_world_int, fraction); + drawBonusPoint(d, pan_canvas_from_world, anim.p_in_world_int, fraction); return; } break; } diff --git a/src/ui/drawBonus.ts b/src/ui/drawBonus.ts index 8b94e13..0e970ff 100644 --- a/src/ui/drawBonus.ts +++ b/src/ui/drawBonus.ts @@ -1,12 +1,14 @@ import { getAssets } from '../core/assets'; +import { Bonus } from '../core/bonus'; import { rectOfTool } from '../core/tools'; -import { drawImage } from '../util/dutil'; +import { drawImage, fillRect } from '../util/dutil'; import { SE2 } from '../util/se2'; import { apply_to_rect } from "../util/se2-extra"; import { Point } from "../util/types"; -import { midpointOfRect } from "../util/util"; +import { midpointOfRect, unreachable } from "../util/util"; +import { drawTileLetter } from './render'; -export function drawBonus(d: CanvasRenderingContext2D, pan_canvas_from_world: SE2, p: Point, fraction: number = 1) { +export function drawBonusPoint(d: CanvasRenderingContext2D, pan_canvas_from_world: SE2, p: Point, fraction: number = 1) { const rect_in_canvas = apply_to_rect(pan_canvas_from_world, { p, sz: { x: 1, y: 1 } }); d.fillStyle = 'rgba(0,0,255,0.5)'; d.beginPath(); @@ -26,3 +28,37 @@ export function drawBonusBomb(d: CanvasRenderingContext2D, pan_canvas_from_world const toolbarImg = getAssets().toolbarImg; drawImage(d, toolbarImg, rectOfTool('bomb'), rect_in_canvas); } + +export function drawBonus(d: CanvasRenderingContext2D, bonus: Bonus, pan_canvas_from_world: SE2, p: Point, fraction: number = 1) { + const toolbarImg = getAssets().toolbarImg; + const rect_in_canvas = apply_to_rect(pan_canvas_from_world, { p, sz: { x: 1, y: 1 } }); + + switch (bonus.t) { + case 'bonus': + drawBonusPoint(d, pan_canvas_from_world, p); + return; + case 'bomb': + drawImage(d, toolbarImg, rectOfTool('bomb'), rect_in_canvas); + return; + case 'empty': + return; + case 'block': { + fillRect(d, rect_in_canvas, 'gray'); + return; + } + case 'required': { + const rect_in_canvas = apply_to_rect(pan_canvas_from_world, { p, sz: { x: 1, y: 1 } }); + drawTileLetter(d, bonus.letter, rect_in_canvas, '#aaa'); + } return; + case 'consonant': { + drawImage(d, toolbarImg, rectOfTool('consonant'), rect_in_canvas); + return; + } + case 'vowel': { + drawImage(d, toolbarImg, rectOfTool('vowel'), rect_in_canvas); + return; + } + + } + unreachable(bonus); +} diff --git a/src/ui/instructions.tsx b/src/ui/instructions.tsx index c365c16..5f4b77b 100644 --- a/src/ui/instructions.tsx +++ b/src/ui/instructions.tsx @@ -137,6 +137,8 @@ function exampleState(): GameState { paused: undefined, inventory: { bombs: 3, + vowels: 0, + consonants: 0, }, }, mouseState: { diff --git a/src/ui/render.ts b/src/ui/render.ts index eb3386f..70d090e 100644 --- a/src/ui/render.ts +++ b/src/ui/render.ts @@ -11,10 +11,10 @@ import { drawImage, fillRect, fillText, pathRectCircle, strokeRect } from "../ut import { SE2, apply, compose, inverse, translate } from '../util/se2'; import { apply_to_rect } from "../util/se2-extra"; import { Point, Rect } from "../util/types"; -import { boundRect, midpointOfRect, scaleRectToCenter } from "../util/util"; +import { boundRect, midpointOfRect, scaleRectToCenter, unreachable } from "../util/util"; import { vadd, vdiv, vm, vscale, vsub, vtrans } from "../util/vutil"; import { drawAnimation } from "./drawAnimation"; -import { drawBonus, drawBonusBomb } from "./drawBonus"; +import { drawBonusPoint, drawBonusBomb, drawBonus } from "./drawBonus"; import { CanvasInfo } from "./use-canvas"; import { canvas_from_drag_tile, pan_canvas_from_world_of_state } from "./view-helpers"; import { canvas_bds_in_canvas, canvas_from_hand, canvas_from_toolbar, hand_bds_in_canvas, pause_button_bds_in_canvas, shuffle_button_bds_in_canvas, toolbar_bds_in_canvas, world_bds_in_canvas } from "./widget-helpers"; @@ -73,6 +73,12 @@ function drawToolbar(d: CanvasRenderingContext2D, state: GameState): void { if (tool == 'bomb') { drawToolbarCount(d, rect_in_canvas, state.coreState.inventory.bombs); } + else if (tool == 'vowel') { + drawToolbarCount(d, rect_in_canvas, state.coreState.inventory.vowels); + } + else if (tool == 'consonant') { + drawToolbarCount(d, rect_in_canvas, state.coreState.inventory.consonants); + } // indicate current tool if (tool == currentTool) { @@ -163,24 +169,7 @@ export function rawPaint(ci: CanvasInfo, state: GameState) { for (let j = top_left_in_world.y; j <= bot_right_in_world.y; j++) { const p: Point = { x: i, y: j }; const bonus = bonusOfStatePoint(cs, p); - switch (bonus.t) { - case 'bonus': - drawBonus(d, pan_canvas_from_world, p); - break; - case 'bomb': - drawBonusBomb(d, pan_canvas_from_world, p); - break; - case 'empty': - break; - case 'block': { - const rect_in_canvas = apply_to_rect(pan_canvas_from_world, { p, sz: { x: 1, y: 1 } }); - fillRect(d, rect_in_canvas, 'gray'); - } break; - case 'required': { - const rect_in_canvas = apply_to_rect(pan_canvas_from_world, { p, sz: { x: 1, y: 1 } }); - drawTileLetter(d, bonus.letter, rect_in_canvas, '#aaa'); - } break; - } + drawBonus(d, bonus, pan_canvas_from_world, p); } } diff --git a/tests/test-distribution.ts b/tests/test-distribution.ts index 0de1f81..779e51d 100644 --- a/tests/test-distribution.ts +++ b/tests/test-distribution.ts @@ -46,7 +46,7 @@ describe('getLetterSample', () => { let seed = 121; let sample = 0; for (let i = 0; i < 1000; i++) { - const b = getLetterSampleOf(seed, energies, letterDistribution, classDistribution, alphabet, 1, 10); + const b = getLetterSampleOf(seed, energies, letterDistribution, classDistribution, alphabet, 1, 10, undefined); seed = b.seed; energies = b.energies; samples.push(b.letter);