diff --git a/src/assets/froob.html b/src/assets/index.html similarity index 85% rename from src/assets/froob.html rename to src/assets/index.html index 8688583..d9c41a0 100644 --- a/src/assets/froob.html +++ b/src/assets/index.html @@ -96,7 +96,7 @@ font-size: medium; } #infobox ul { - margin: 0 0.5vw; + margin: 0; padding: 0; list-style-type: none; } @@ -105,17 +105,20 @@ align-items: center; justify-content: space-between; } + #infobox h2 { + border-bottom: 1px solid gray; + margin-bottom: 5px; + } kbd { background-color: #eee; border-radius: 3px; text-align: center; - height: 1.8vw; - width: 1.8vw; - font-size: 1.5vw; - margin-block: 2px; + padding: 4px 6px; + margin: 1px 2px; color: black; border: 1px solid #b4b4b4; } + .with-border { margin: 4px; border: 4px solid #525252; @@ -131,6 +134,12 @@ flex-direction: column; flex-wrap: wrap; } + #quitButton a { + color: black; + } + h2 { + margin-block: 0.5vw; + } @@ -147,10 +156,11 @@

Controls

@@ -160,6 +170,7 @@

You’ve been froobed!

With a score of

try again?

+

quit

diff --git a/src/gameState.ts b/src/gameState.ts index ede3c28..19eb5be 100644 --- a/src/gameState.ts +++ b/src/gameState.ts @@ -1,5 +1,4 @@ import { RotationStyle, TileColor } from "./tetrisPieces"; -import { Option, ifSome, isNone, isSome } from "./lib/option"; import { Point, addPoint } from "./lib/util"; import { TextureStore } from "./textureStore"; import { Sidebar } from "./sidebar"; @@ -33,10 +32,10 @@ function posToGridIndex(x: number, y: number): number { export class GameState { // Whenever an operation is performed which requires the board to be redrawn, reDraw is set to true reDraw: boolean; - heldPiece: Option; + heldPiece: null | HeldPiece; fallTimer: number; movementTimer: number; - grid: Array>; + grid: Array; sidebarRef: Sidebar; constructor(sidebarRef: Sidebar) { @@ -57,24 +56,22 @@ export class GameState { PLAYFIELD_HEIGHT ); for (const [i, tile] of this.grid.entries()) { - if (isNone(tile)) { + if (tile === null) { continue; } - ifSome(tile, (tile) => { - GameState.drawTile(...gridIndexToPos(i), tile, ctx, textures); - }); + GameState.drawTile(...gridIndexToPos(i), tile, ctx, textures); } - ifSome(this.heldPiece, (held) => { - for (const tileOffset of held.tiles) { - const pos = addPoint(held.pos, tileOffset); - GameState.drawTile(...pos, held.schema.color, ctx, textures); + if (this.heldPiece !== null ) { + for (const tileOffset of this.heldPiece.tiles) { + const pos = addPoint(this.heldPiece.pos, tileOffset); + GameState.drawTile(...pos, this.heldPiece.schema.color, ctx, textures); } - }); + } this.reDraw = false; } fallTick(): void | boolean { - if (isNone(this.heldPiece)) { + if (this.heldPiece === null) { this.heldPiece = this.sidebarRef.getNextPiece(); if (this.allPointsInEmptyGridSpace(this.heldPieceTilesAdjusted())) { this.reDraw = true; @@ -86,12 +83,14 @@ export class GameState { } if (!this.moveHeld(0, 1)) { + console.log("move failed"); this.flashHeldToGrid(); } } inputTick(pressed: Array): void { - if (isNone(this.heldPiece)) return; + + if (this.heldPiece === null) return; for (const key of pressed) { switch (key) { case KeyEvent.Up: @@ -115,54 +114,57 @@ export class GameState { } rotate(): void { - ifSome(this.heldPiece, (held) => { - const style = held.schema.rotation; - if (style === RotationStyle.None) { - return; - } + if (this.heldPiece === null) return; + const held = this.heldPiece; + const style = held.schema.rotation; + if (style === RotationStyle.None) { + return; + } - let rotationFunc: (p: Point) => Point; - if (style === RotationStyle.Center) { - // For 3x2 - rotationFunc = (p): Point => [-p[1], p[0]]; - } else { - // For 1x4 - rotationFunc = (p): Point => [1 - p[1], p[0]]; - } - const rotated = held.tiles.map(rotationFunc); - const rotatedAdjusted = rotated.map((p) => addPoint(p, held.pos)); - if (this.allPointsInEmptyGridSpace(rotatedAdjusted)) { - held.tiles = rotated; - this.reDraw = true; - } - }); + let rotationFunc: (p: Point) => Point; + if (style === RotationStyle.Center) { + // For 3x2 + rotationFunc = (p): Point => [-p[1], p[0]]; + } else { + // For 1x4 + rotationFunc = (p): Point => [1 - p[1], p[0]]; + } + const rotated = held.tiles.map(rotationFunc); + + const rotatedAdjusted = rotated.map((p) => addPoint(p, held.pos)); + if (this.allPointsInEmptyGridSpace(rotatedAdjusted)) { + this.heldPiece.tiles = rotated; + this.reDraw = true; + } } private moveHeld(dx: number, dy: number): boolean { - let success = false; - ifSome(this.heldPiece, (held) => { - const newPos: Point = addPoint(held.pos, [dx, dy]); - if ( - this.allPointsInEmptyGridSpace(this.heldPieceTilesAdjusted([dx, dy])) - ) { - held.pos = newPos; - this.reDraw = true; - success = true; - } - }); - return success; + + if (this.heldPiece === null) return false; + const held = this.heldPiece; + const newPos: Point = addPoint(held.pos, [dx, dy]); + if ( + this.allPointsInEmptyGridSpace(this.heldPieceTilesAdjusted([dx, dy])) + ) { + held.pos = newPos; + this.reDraw = true; + return true; + } + return false; } private allPointsInEmptyGridSpace(points: Array): boolean { - return ( - points - .map((p): boolean => this.isGridSquareFilled(...p)) - .find((v) => v) === undefined - ); + for (const p of points ) { + if (this.isGridSquareFilled(...p)) { + return false; + } + } + return true; } // Should only be called when held piece is non none; uses unsafe type assertion. private heldPieceTilesAdjusted(offset: Point = [0, 0]): Array { + console.assert(this.heldPiece !== null); const held = this.heldPiece as HeldPiece; const adjusted: Array = held.tiles.map((t): Point => { @@ -172,23 +174,25 @@ export class GameState { } private flashHeldToGrid(): void { - ifSome(this.heldPiece, (held) => { - this.heldPieceTilesAdjusted().forEach((pos) => { - const index = posToGridIndex(...pos); - this.grid[index] = held.schema.color; - }); - - this.heldPiece = null; - this.reDraw = true; - this.checkForTetris(); + if (this.heldPiece === null) { + return; + } + const held = this.heldPiece; + this.heldPieceTilesAdjusted().forEach((pos) => { + const index = posToGridIndex(...pos); + this.grid[index] = held.schema.color; }); + + this.heldPiece = null; + this.reDraw = true; + this.checkForTetris(); } private checkForTetris(): void { // Iterate through rows, starting at bottom and going to top for (let i = 0; i < this.grid.length - GRID_WIDTH; i += GRID_WIDTH) { // While the current row is completely filled, thus tetris - while (this.grid.slice(i, i + GRID_WIDTH).every((t) => isSome(t))) { + while (this.grid.slice(i, i + GRID_WIDTH).every((t) => t !== null)) { // Remove all of the tiles from the current row. this.grid.splice(i, GRID_WIDTH); // Then fill the empty space with new tiles @@ -204,10 +208,13 @@ export class GameState { if (x >= GRID_WIDTH || x < 0 || y >= GRID_HEIGHT) return true; const index = posToGridIndex(x, y); - if (isSome(this.grid[index])) { - return true; + + // There's a bug here where sometimes a tile can be undefined. It's supposed to only ever be a valid tile or null. + // Couldn't find it so just check for undefined too. + if (this.grid[index] === null || this.grid[index] === undefined) { + return false; } - return false; + return true; } private static drawTile( diff --git a/src/lib/controller.ts b/src/lib/controller.ts index 265d05d..390148c 100644 --- a/src/lib/controller.ts +++ b/src/lib/controller.ts @@ -1,4 +1,3 @@ -import { ifSome } from "./option"; export const enum KeyEventType { IfCaught, @@ -57,21 +56,24 @@ export class Keyboard { } pollSpecificKey(key: T, callback: () => void): void { const v = this.keyQueue.get(key); - ifSome(v, (vv) => { - if (!this.keysPressed.has(vv.outputValue)) { - if (vv.eventType === KeyEventType.IfCaught || vv.polled) { - vv.polled = false; - this.keyQueue.delete(vv.outputValue); - return; - } + if (v === null || v === undefined) { + return; + } + if (!this.keysPressed.has(v.outputValue)) { + if (v.eventType === KeyEventType.IfCaught || v.polled) { + v.polled = false; + this.keyQueue.delete(v.outputValue); + return; } - if (!vv.polled && vv.eventType === KeyEventType.OnlyOnce) { - callback(); - } else if (vv.eventType !== KeyEventType.OnlyOnce) { - callback(); - } - vv.polled = true; - }); + } + if (!v.polled && v.eventType === KeyEventType.OnlyOnce) { + callback(); + } else if (v.eventType !== KeyEventType.OnlyOnce) { + callback(); + } + v.polled = true; + + } poll(): Array { @@ -100,16 +102,19 @@ export class Keyboard { private keyDown(key: string): void { const mapped = this.keyMap.get(key); - ifSome(mapped, (m) => { - this.keysPressed.add(m.outputValue); - if (!this.keyQueue.has(m.outputValue)) - this.keyQueue.set(m.outputValue, m); - }); + if (mapped === null || mapped === undefined) { + return; + } + this.keysPressed.add(mapped.outputValue); + if (!this.keyQueue.has(mapped.outputValue)) { + this.keyQueue.set(mapped.outputValue, mapped); + } } private keyUp(key: string): void { const mapped = this.keyMap.get(key); - ifSome(mapped, (m) => { - this.keysPressed.delete(m.outputValue); - }); + if (mapped === null || mapped === undefined) { + return; + } + this.keysPressed.delete(mapped.outputValue); } } diff --git a/src/lib/option.ts b/src/lib/option.ts index 6c04f8a..2fcd439 100644 --- a/src/lib/option.ts +++ b/src/lib/option.ts @@ -1,67 +1,12 @@ -// -// Options (inspired by rust) -// +type Option = null | undefined | T; -// Most methods like `document.getElementById("something")` will naturally return something of the type `Option` -// This type has been chosen to remain compatible with most vanilla JS methods. -// A more capable option type could have been made by creating an option object, but would lose compatibility with JS. -export type Option = null | undefined | T; - -export function isNone(input: Option): boolean { - if (input === null || input === undefined) { - return true; - } - return false; -} - -export function isSome(input: Option): boolean { - if (input === null || input === undefined) { - return false; - } - return true; -} - -type SingleArgCallback = (v: T) => void; -type EmptyCallback = () => void; - -/** - * If the input `Option` is some, then run the `doAfter` callback. - * Intended to mimic rust's conditional enum matching pattern: `if let Some(v) = opt {}` - */ -export function ifSome( - input: Option, - doAfter: SingleArgCallback -): void { - if (isSome(input)) { - doAfter(input as T); - } -} - -export function ifNone(input: Option, doAfter: EmptyCallback): void { - if (isNone(input)) { - doAfter(); - } -} - -// Not sure how ergonomic this is in actual use. It may get axed. -export function ifEither( - input: Option, - doIfSome: SingleArgCallback, - doIfNone: EmptyCallback -): void { - if (isSome(input)) { - doIfSome(input as T); - return; - } - doIfNone(); -} /** Unwrap option of `null|undefined|T` to `T` throw error if value is not `T`. `expect()` is preferred to this function as it gives better error messages */ export function unwrap(input: Option): T { - if (isNone(input)) { + if (input === null || input === undefined) { throw new TypeError("Unwrap called on null/undefined value"); } return input as T; @@ -71,7 +16,7 @@ export function unwrap(input: Option): T { Unwrap option of `null|undefined|T` to `T` throw error with `exceptionMessage` if value is not `T` */ export function expect(input: Option, exceptionMessage: string): T { - if (isNone(input)) { + if (input === null || input === undefined) { throw new TypeError(exceptionMessage); } return input as T; diff --git a/src/lib/util.ts b/src/lib/util.ts index 6035f21..52014c9 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1,4 +1,3 @@ -import { Option, ifSome } from "./option"; export type Point = [number, number]; export function addPoint(p1: Point, p2: Point): Point { @@ -10,24 +9,28 @@ export function addPoint(p1: Point, p2: Point): Point { // // Who needs Jquery? -export function $(elementId: string): Option { +export function $(elementId: string): null | HTMLElement { return document.getElementById(elementId); } export function createCanvas( width: number, height: number -): Option<{ canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D }> { - let ret = null; - ifSome(document.createElement("canvas"), (canvas) => { - canvas.width = width; - canvas.height = height; - ifSome(canvas.getContext("2d"), (ctx) => { - ctx.imageSmoothingEnabled = false; - ret = { canvas, ctx }; - }); - }); - return ret; +): null | { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } { + const canvas = document.createElement("canvas"); + if (canvas === null) { + return null; + } + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext("2d"); + if (ctx === null) { + return null; + } + ctx.imageSmoothingEnabled = false; + return { canvas, ctx }; + } export function clearBody(): void { diff --git a/src/sidebar.ts b/src/sidebar.ts index e20708c..17431c4 100644 --- a/src/sidebar.ts +++ b/src/sidebar.ts @@ -1,6 +1,6 @@ import { SIDEBAR_HEIGHT, SIDEBAR_WIDTH, TILE_SIZE } from "./constants"; import { HeldPiece } from "./heldPiece"; -import { addPoint, minMax, pickRandom } from "./lib/util"; +import { minMax, pickRandom } from "./lib/util"; import { TETRIS_PIECE, TETRIS_PIECE_LIST } from "./tetrisPieces"; import { TextureStore } from "./textureStore"; diff --git a/tetris_mockup.xcf b/tetris_mockup.xcf deleted file mode 100644 index d5f722c..0000000 Binary files a/tetris_mockup.xcf and /dev/null differ