Skip to content

Commit

Permalink
Bonus to give a vowel or consonant by choice (fix #66)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcreedcmu committed Nov 22, 2023
1 parent 7be1bb0 commit fa7371e
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 69 deletions.
Binary file modified public/assets/toolbar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 51 additions & 3 deletions src/core/bonus.ts
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -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 {
Expand All @@ -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' };
}
Expand Down Expand Up @@ -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<CoreState>, 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<BonusLayerId, Layer<Bonus>> = {};

Expand Down
22 changes: 17 additions & 5 deletions src/core/distribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<string, number>, 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<string, number>, 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);
}
8 changes: 6 additions & 2 deletions src/core/reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`);
}
}

Expand Down Expand Up @@ -300,7 +302,7 @@ function reduceMouseDownInHand(state: GameState, wp: WidgetPoint & { t: 'hand' }
function reduceMouseDownInToolbar(state: GameState, wp: WidgetPoint & { t: 'toolbar' }, button: number, mods: Set<string>): 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);
Expand Down Expand Up @@ -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);
Expand Down
38 changes: 5 additions & 33 deletions src/core/state-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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<CoreState>, 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');
Expand Down
4 changes: 4 additions & 0 deletions src/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export type CoreState = {
game_from_clock: SE1,
inventory: {
bombs: number,
vowels: number,
consonants: number,
}
bonusLayerName: string,
};
Expand Down Expand Up @@ -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',
},
Expand Down
30 changes: 30 additions & 0 deletions src/core/tools.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -9,6 +11,8 @@ const tools = [
'hand',
'dynamite',
'bomb',
'vowel',
'consonant',
] as const;

export type Tool = (typeof tools)[number];
Expand Down Expand Up @@ -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;
}

Expand All @@ -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; });
}
}
4 changes: 2 additions & 2 deletions src/ui/drawAnimation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down
42 changes: 39 additions & 3 deletions src/ui/drawBonus.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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);
}
2 changes: 2 additions & 0 deletions src/ui/instructions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ function exampleState(): GameState {
paused: undefined,
inventory: {
bombs: 3,
vowels: 0,
consonants: 0,
},
},
mouseState: {
Expand Down
Loading

0 comments on commit fa7371e

Please sign in to comment.