general cleanup

This commit is contained in:
Alexander Bass 2024-02-09 00:34:25 -05:00
parent 12638b4131
commit 21ee80b08a
7 changed files with 139 additions and 168 deletions

View file

@ -96,7 +96,7 @@
font-size: medium; font-size: medium;
} }
#infobox ul { #infobox ul {
margin: 0 0.5vw; margin: 0;
padding: 0; padding: 0;
list-style-type: none; list-style-type: none;
} }
@ -105,17 +105,20 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
#infobox h2 {
border-bottom: 1px solid gray;
margin-bottom: 5px;
}
kbd { kbd {
background-color: #eee; background-color: #eee;
border-radius: 3px; border-radius: 3px;
text-align: center; text-align: center;
height: 1.8vw; padding: 4px 6px;
width: 1.8vw; margin: 1px 2px;
font-size: 1.5vw;
margin-block: 2px;
color: black; color: black;
border: 1px solid #b4b4b4; border: 1px solid #b4b4b4;
} }
.with-border { .with-border {
margin: 4px; margin: 4px;
border: 4px solid #525252; border: 4px solid #525252;
@ -131,6 +134,12 @@
flex-direction: column; flex-direction: column;
flex-wrap: wrap; flex-wrap: wrap;
} }
#quitButton a {
color: black;
}
h2 {
margin-block: 0.5vw;
}
</style> </style>
<script type="module" src="main.js"></script> <script type="module" src="main.js"></script>
</head> </head>
@ -147,10 +156,11 @@
<div id="infobox" class="with-border"> <div id="infobox" class="with-border">
<h2>Controls</h2> <h2>Controls</h2>
<ul> <ul>
<li><kbd></kbd> Rotate</li> <li><kbd></kbd> <span>Rotate</span></li>
<li><kbd></kbd> Drop</li> <li><kbd></kbd> <span>Drop</span></li>
<li><kbd></kbd> Left</li> <li><kbd></kbd> <span>Left</span></li>
<li><kbd></kbd> Right</li> <li><kbd></kbd> <span>Right</span></li>
<!-- <li><kbd>CTRL</kbd> + <kbd>↓</kbd> Hard Drop</li> -->
</ul> </ul>
</div> </div>
</div> </div>
@ -160,6 +170,7 @@
<h1>You&rsquo;ve been froobed!</h1> <h1>You&rsquo;ve been froobed!</h1>
<div id="scoreCounter">With a score of</div> <div id="scoreCounter">With a score of</div>
<h2 id="tryAgain">try again?</h2> <h2 id="tryAgain">try again?</h2>
<h2 id="quitButton"><a href="../">quit</a></h2>
</div> </div>
</div> </div>
</body> </body>

View file

@ -1,5 +1,4 @@
import { RotationStyle, TileColor } from "./tetrisPieces"; import { RotationStyle, TileColor } from "./tetrisPieces";
import { Option, ifSome, isNone, isSome } from "./lib/option";
import { Point, addPoint } from "./lib/util"; import { Point, addPoint } from "./lib/util";
import { TextureStore } from "./textureStore"; import { TextureStore } from "./textureStore";
import { Sidebar } from "./sidebar"; import { Sidebar } from "./sidebar";
@ -33,10 +32,10 @@ function posToGridIndex(x: number, y: number): number {
export class GameState { export class GameState {
// Whenever an operation is performed which requires the board to be redrawn, reDraw is set to true // Whenever an operation is performed which requires the board to be redrawn, reDraw is set to true
reDraw: boolean; reDraw: boolean;
heldPiece: Option<HeldPiece>; heldPiece: null | HeldPiece;
fallTimer: number; fallTimer: number;
movementTimer: number; movementTimer: number;
grid: Array<Option<TileColor>>; grid: Array<TileColor | null>;
sidebarRef: Sidebar; sidebarRef: Sidebar;
constructor(sidebarRef: Sidebar) { constructor(sidebarRef: Sidebar) {
@ -57,24 +56,22 @@ export class GameState {
PLAYFIELD_HEIGHT PLAYFIELD_HEIGHT
); );
for (const [i, tile] of this.grid.entries()) { for (const [i, tile] of this.grid.entries()) {
if (isNone(tile)) { if (tile === null) {
continue; continue;
} }
ifSome(tile, (tile) => {
GameState.drawTile(...gridIndexToPos(i), tile, ctx, textures); GameState.drawTile(...gridIndexToPos(i), tile, ctx, textures);
});
} }
ifSome(this.heldPiece, (held) => { if (this.heldPiece !== null ) {
for (const tileOffset of held.tiles) { for (const tileOffset of this.heldPiece.tiles) {
const pos = addPoint(held.pos, tileOffset); const pos = addPoint(this.heldPiece.pos, tileOffset);
GameState.drawTile(...pos, held.schema.color, ctx, textures); GameState.drawTile(...pos, this.heldPiece.schema.color, ctx, textures);
}
} }
});
this.reDraw = false; this.reDraw = false;
} }
fallTick(): void | boolean { fallTick(): void | boolean {
if (isNone(this.heldPiece)) { if (this.heldPiece === null) {
this.heldPiece = this.sidebarRef.getNextPiece(); this.heldPiece = this.sidebarRef.getNextPiece();
if (this.allPointsInEmptyGridSpace(this.heldPieceTilesAdjusted())) { if (this.allPointsInEmptyGridSpace(this.heldPieceTilesAdjusted())) {
this.reDraw = true; this.reDraw = true;
@ -86,12 +83,14 @@ export class GameState {
} }
if (!this.moveHeld(0, 1)) { if (!this.moveHeld(0, 1)) {
console.log("move failed");
this.flashHeldToGrid(); this.flashHeldToGrid();
} }
} }
inputTick(pressed: Array<KeyEvent>): void { inputTick(pressed: Array<KeyEvent>): void {
if (isNone(this.heldPiece)) return;
if (this.heldPiece === null) return;
for (const key of pressed) { for (const key of pressed) {
switch (key) { switch (key) {
case KeyEvent.Up: case KeyEvent.Up:
@ -115,7 +114,8 @@ export class GameState {
} }
rotate(): void { rotate(): void {
ifSome(this.heldPiece, (held) => { if (this.heldPiece === null) return;
const held = this.heldPiece;
const style = held.schema.rotation; const style = held.schema.rotation;
if (style === RotationStyle.None) { if (style === RotationStyle.None) {
return; return;
@ -130,39 +130,41 @@ export class GameState {
rotationFunc = (p): Point => [1 - p[1], p[0]]; rotationFunc = (p): Point => [1 - p[1], p[0]];
} }
const rotated = held.tiles.map(rotationFunc); const rotated = held.tiles.map(rotationFunc);
const rotatedAdjusted = rotated.map((p) => addPoint(p, held.pos)); const rotatedAdjusted = rotated.map((p) => addPoint(p, held.pos));
if (this.allPointsInEmptyGridSpace(rotatedAdjusted)) { if (this.allPointsInEmptyGridSpace(rotatedAdjusted)) {
held.tiles = rotated; this.heldPiece.tiles = rotated;
this.reDraw = true; this.reDraw = true;
} }
});
} }
private moveHeld(dx: number, dy: number): boolean { private moveHeld(dx: number, dy: number): boolean {
let success = false;
ifSome(this.heldPiece, (held) => { if (this.heldPiece === null) return false;
const held = this.heldPiece;
const newPos: Point = addPoint(held.pos, [dx, dy]); const newPos: Point = addPoint(held.pos, [dx, dy]);
if ( if (
this.allPointsInEmptyGridSpace(this.heldPieceTilesAdjusted([dx, dy])) this.allPointsInEmptyGridSpace(this.heldPieceTilesAdjusted([dx, dy]))
) { ) {
held.pos = newPos; held.pos = newPos;
this.reDraw = true; this.reDraw = true;
success = true; return true;
} }
}); return false;
return success;
} }
private allPointsInEmptyGridSpace(points: Array<Point>): boolean { private allPointsInEmptyGridSpace(points: Array<Point>): boolean {
return ( for (const p of points ) {
points if (this.isGridSquareFilled(...p)) {
.map((p): boolean => this.isGridSquareFilled(...p)) return false;
.find((v) => v) === undefined }
); }
return true;
} }
// Should only be called when held piece is non none; uses unsafe type assertion. // Should only be called when held piece is non none; uses unsafe type assertion.
private heldPieceTilesAdjusted(offset: Point = [0, 0]): Array<Point> { private heldPieceTilesAdjusted(offset: Point = [0, 0]): Array<Point> {
console.assert(this.heldPiece !== null);
const held = this.heldPiece as HeldPiece; const held = this.heldPiece as HeldPiece;
const adjusted: Array<Point> = held.tiles.map((t): Point => { const adjusted: Array<Point> = held.tiles.map((t): Point => {
@ -172,7 +174,10 @@ export class GameState {
} }
private flashHeldToGrid(): void { private flashHeldToGrid(): void {
ifSome(this.heldPiece, (held) => { if (this.heldPiece === null) {
return;
}
const held = this.heldPiece;
this.heldPieceTilesAdjusted().forEach((pos) => { this.heldPieceTilesAdjusted().forEach((pos) => {
const index = posToGridIndex(...pos); const index = posToGridIndex(...pos);
this.grid[index] = held.schema.color; this.grid[index] = held.schema.color;
@ -181,14 +186,13 @@ export class GameState {
this.heldPiece = null; this.heldPiece = null;
this.reDraw = true; this.reDraw = true;
this.checkForTetris(); this.checkForTetris();
});
} }
private checkForTetris(): void { private checkForTetris(): void {
// Iterate through rows, starting at bottom and going to top // Iterate through rows, starting at bottom and going to top
for (let i = 0; i < this.grid.length - GRID_WIDTH; i += GRID_WIDTH) { for (let i = 0; i < this.grid.length - GRID_WIDTH; i += GRID_WIDTH) {
// While the current row is completely filled, thus tetris // 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. // Remove all of the tiles from the current row.
this.grid.splice(i, GRID_WIDTH); this.grid.splice(i, GRID_WIDTH);
// Then fill the empty space with new tiles // Then fill the empty space with new tiles
@ -204,11 +208,14 @@ export class GameState {
if (x >= GRID_WIDTH || x < 0 || y >= GRID_HEIGHT) return true; if (x >= GRID_WIDTH || x < 0 || y >= GRID_HEIGHT) return true;
const index = posToGridIndex(x, y); 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( private static drawTile(
x: number, x: number,

View file

@ -1,4 +1,3 @@
import { ifSome } from "./option";
export const enum KeyEventType { export const enum KeyEventType {
IfCaught, IfCaught,
@ -57,21 +56,24 @@ export class Keyboard<T> {
} }
pollSpecificKey(key: T, callback: () => void): void { pollSpecificKey(key: T, callback: () => void): void {
const v = this.keyQueue.get(key); const v = this.keyQueue.get(key);
ifSome(v, (vv) => { if (v === null || v === undefined) {
if (!this.keysPressed.has(vv.outputValue)) { return;
if (vv.eventType === KeyEventType.IfCaught || vv.polled) { }
vv.polled = false; if (!this.keysPressed.has(v.outputValue)) {
this.keyQueue.delete(vv.outputValue); if (v.eventType === KeyEventType.IfCaught || v.polled) {
v.polled = false;
this.keyQueue.delete(v.outputValue);
return; return;
} }
} }
if (!vv.polled && vv.eventType === KeyEventType.OnlyOnce) { if (!v.polled && v.eventType === KeyEventType.OnlyOnce) {
callback(); callback();
} else if (vv.eventType !== KeyEventType.OnlyOnce) { } else if (v.eventType !== KeyEventType.OnlyOnce) {
callback(); callback();
} }
vv.polled = true; v.polled = true;
});
} }
poll(): Array<T> { poll(): Array<T> {
@ -100,16 +102,19 @@ export class Keyboard<T> {
private keyDown(key: string): void { private keyDown(key: string): void {
const mapped = this.keyMap.get(key); const mapped = this.keyMap.get(key);
ifSome(mapped, (m) => { if (mapped === null || mapped === undefined) {
this.keysPressed.add(m.outputValue); return;
if (!this.keyQueue.has(m.outputValue)) }
this.keyQueue.set(m.outputValue, m); this.keysPressed.add(mapped.outputValue);
}); if (!this.keyQueue.has(mapped.outputValue)) {
this.keyQueue.set(mapped.outputValue, mapped);
}
} }
private keyUp(key: string): void { private keyUp(key: string): void {
const mapped = this.keyMap.get(key); const mapped = this.keyMap.get(key);
ifSome(mapped, (m) => { if (mapped === null || mapped === undefined) {
this.keysPressed.delete(m.outputValue); return;
}); }
this.keysPressed.delete(mapped.outputValue);
} }
} }

View file

@ -1,67 +1,12 @@
// type Option<T> = null | undefined | T;
// Options (inspired by rust)
//
// Most methods like `document.getElementById("something")` will naturally return something of the type `Option<T>`
// 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<T> = null | undefined | T;
export function isNone<T>(input: Option<T>): boolean {
if (input === null || input === undefined) {
return true;
}
return false;
}
export function isSome<T>(input: Option<T>): boolean {
if (input === null || input === undefined) {
return false;
}
return true;
}
type SingleArgCallback<T> = (v: T) => void;
type EmptyCallback = () => void;
/**
* If the input `Option<T>` is some, then run the `doAfter` callback.
* Intended to mimic rust's conditional enum matching pattern: `if let Some(v) = opt {<do things with value v>}`
*/
export function ifSome<T>(
input: Option<T>,
doAfter: SingleArgCallback<T>
): void {
if (isSome(input)) {
doAfter(input as T);
}
}
export function ifNone<T>(input: Option<T>, doAfter: EmptyCallback): void {
if (isNone(input)) {
doAfter();
}
}
// Not sure how ergonomic this is in actual use. It may get axed.
export function ifEither<T>(
input: Option<T>,
doIfSome: SingleArgCallback<T>,
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`. 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 `expect()` is preferred to this function as it gives better error messages
*/ */
export function unwrap<T>(input: Option<T>): T { export function unwrap<T>(input: Option<T>): T {
if (isNone(input)) { if (input === null || input === undefined) {
throw new TypeError("Unwrap called on null/undefined value"); throw new TypeError("Unwrap called on null/undefined value");
} }
return input as T; return input as T;
@ -71,7 +16,7 @@ export function unwrap<T>(input: Option<T>): T {
Unwrap option of `null|undefined|T` to `T` throw error with `exceptionMessage` if value is not `T` Unwrap option of `null|undefined|T` to `T` throw error with `exceptionMessage` if value is not `T`
*/ */
export function expect<T>(input: Option<T>, exceptionMessage: string): T { export function expect<T>(input: Option<T>, exceptionMessage: string): T {
if (isNone(input)) { if (input === null || input === undefined) {
throw new TypeError(exceptionMessage); throw new TypeError(exceptionMessage);
} }
return input as T; return input as T;

View file

@ -1,4 +1,3 @@
import { Option, ifSome } from "./option";
export type Point = [number, number]; export type Point = [number, number];
export function addPoint(p1: Point, p2: Point): Point { export function addPoint(p1: Point, p2: Point): Point {
@ -10,24 +9,28 @@ export function addPoint(p1: Point, p2: Point): Point {
// //
// Who needs Jquery? // Who needs Jquery?
export function $(elementId: string): Option<HTMLElement> { export function $(elementId: string): null | HTMLElement {
return document.getElementById(elementId); return document.getElementById(elementId);
} }
export function createCanvas( export function createCanvas(
width: number, width: number,
height: number height: number
): Option<{ canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D }> { ): null | { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
let ret = null; const canvas = document.createElement("canvas");
ifSome(document.createElement("canvas"), (canvas) => { if (canvas === null) {
return null;
}
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
ifSome(canvas.getContext("2d"), (ctx) => {
const ctx = canvas.getContext("2d");
if (ctx === null) {
return null;
}
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
ret = { canvas, ctx }; return { canvas, ctx };
});
});
return ret;
} }
export function clearBody(): void { export function clearBody(): void {

View file

@ -1,6 +1,6 @@
import { SIDEBAR_HEIGHT, SIDEBAR_WIDTH, TILE_SIZE } from "./constants"; import { SIDEBAR_HEIGHT, SIDEBAR_WIDTH, TILE_SIZE } from "./constants";
import { HeldPiece } from "./heldPiece"; 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 { TETRIS_PIECE, TETRIS_PIECE_LIST } from "./tetrisPieces";
import { TextureStore } from "./textureStore"; import { TextureStore } from "./textureStore";

Binary file not shown.