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;
}
#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;
}
</style>
<script type="module" src="main.js"></script>
</head>
@ -147,10 +156,11 @@
<div id="infobox" class="with-border">
<h2>Controls</h2>
<ul>
<li><kbd></kbd> Rotate</li>
<li><kbd></kbd> Drop</li>
<li><kbd></kbd> Left</li>
<li><kbd></kbd> Right</li>
<li><kbd></kbd> <span>Rotate</span></li>
<li><kbd></kbd> <span>Drop</span></li>
<li><kbd></kbd> <span>Left</span></li>
<li><kbd></kbd> <span>Right</span></li>
<!-- <li><kbd>CTRL</kbd> + <kbd>↓</kbd> Hard Drop</li> -->
</ul>
</div>
</div>
@ -160,6 +170,7 @@
<h1>You&rsquo;ve been froobed!</h1>
<div id="scoreCounter">With a score of</div>
<h2 id="tryAgain">try again?</h2>
<h2 id="quitButton"><a href="../">quit</a></h2>
</div>
</div>
</body>

View file

@ -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>;
heldPiece: null | HeldPiece;
fallTimer: number;
movementTimer: number;
grid: Array<Option<TileColor>>;
grid: Array<TileColor | null>;
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<KeyEvent>): 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<Point>): 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<Point> {
console.assert(this.heldPiece !== null);
const held = this.heldPiece as HeldPiece;
const adjusted: Array<Point> = 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(

View file

@ -1,4 +1,3 @@
import { ifSome } from "./option";
export const enum KeyEventType {
IfCaught,
@ -57,21 +56,24 @@ export class Keyboard<T> {
}
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<T> {
@ -100,16 +102,19 @@ export class Keyboard<T> {
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);
}
}

View file

@ -1,67 +1,12 @@
//
// Options (inspired by rust)
//
type Option<T> = null | undefined | T;
// 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`.
`expect()` is preferred to this function as it gives better error messages
*/
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");
}
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`
*/
export function expect<T>(input: Option<T>, exceptionMessage: string): T {
if (isNone(input)) {
if (input === null || input === undefined) {
throw new TypeError(exceptionMessage);
}
return input as T;

View file

@ -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<HTMLElement> {
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 {

View file

@ -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";

Binary file not shown.