Froobtris/src/gameState.ts
Alexander Bass 9b528ab2e8 init
2023-07-28 14:01:50 -04:00

235 lines
5.6 KiB
TypeScript

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";
import { HeldPiece } from "./heldPiece";
import { KeyEvent } from "./keyEvent";
import {
BORDER_THICKNESS,
GRID_HEIGHT,
GRID_WIDTH,
PLAYFIELD_HEIGHT,
PLAYFIELD_WIDTH,
TILE_SIZE,
} from "./constants";
function gridIndexToPos(index: number): Point {
const x = index % GRID_WIDTH;
const y = GRID_HEIGHT - Math.floor(index / GRID_WIDTH);
return [x, y];
}
function posToGridIndex(x: number, y: number): number {
return x + (GRID_HEIGHT - y) * GRID_WIDTH;
}
export class GameState {
reDraw: boolean;
gameOver: boolean;
heldPiece: Option<HeldPiece>;
fallTimer: number;
movementTimer: number;
grid: Array<Option<TileColor>>;
sidebar: Sidebar;
constructor(sidebar: Sidebar) {
this.reDraw = true;
this.gameOver = false;
this.sidebar = sidebar;
this.heldPiece = null;
this.fallTimer = 0;
this.movementTimer = 0;
this.sidebar.getNextPiece();
this.grid = Array(GRID_WIDTH * GRID_HEIGHT).fill(null);
}
drawPlayfield(ctx: CanvasRenderingContext2D, textures: TextureStore): void {
ctx.drawImage(
textures.playfieldBg,
0,
0,
PLAYFIELD_WIDTH,
PLAYFIELD_HEIGHT
);
for (const [i, tile] of this.grid.entries()) {
if (isNone(tile)) {
continue;
}
ifSome(tile, (tile) => {
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);
}
});
this.reDraw = false;
}
fallTick(): void | boolean {
if (isNone(this.heldPiece)) {
this.heldPiece = this.sidebar.getNextPiece();
if (this.allPointsInEmptyGridSpace(this.heldPieceTilesAdjusted())) {
this.reDraw = true;
} else {
this.heldPiece = null;
return true;
}
return;
}
if (!this.moveHeld(0, 1)) {
this.flashHeldToGrid();
}
}
inputTick(pressed: Array<KeyEvent>): void {
if (isNone(this.heldPiece)) return;
for (const key of pressed) {
switch (key) {
case KeyEvent.Up:
this.rotate();
return;
case KeyEvent.Down:
if (!this.moveHeld(0, 1)) {
this.flashHeldToGrid();
} else {
this.fallTimer = Date.now();
}
break;
case KeyEvent.Left:
this.moveHeld(-1, 0);
break;
case KeyEvent.Right:
this.moveHeld(1, 0);
break;
}
}
}
rotate(): void {
ifSome(this.heldPiece, (held) => {
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;
}
});
}
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;
}
private allPointsInEmptyGridSpace(points: Array<Point>): boolean {
return (
points
.map((p): boolean => this.isGridSquareFilled(...p))
.find((v) => v) === undefined
);
}
// Should only be called when held piece is non none
private heldPieceTilesAdjusted(offset: Point = [0, 0]): Array<Point> {
const held = this.heldPiece as HeldPiece;
const adjusted: Array<Point> = held.tiles.map((t): Point => {
return addPoint(addPoint(t, offset), held.pos);
});
return adjusted;
}
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();
});
}
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))) {
// Remove all of the tiles from the current row.
this.grid.splice(i, GRID_WIDTH);
// Then fill the empty space with new tiles
this.grid.push(...Array(GRID_WIDTH).fill(null));
// Seven seems like a good number
this.sidebar.score += 7;
}
}
}
private isGridSquareFilled(x: number, y: number): boolean {
// If invalid position, return true
if (x >= GRID_WIDTH || x < 0 || y >= GRID_HEIGHT) return true;
const index = posToGridIndex(x, y);
if (isSome(this.grid[index])) {
return true;
}
return false;
}
private static drawTile(
x: number,
y: number,
tileColor: TileColor,
ctx: CanvasRenderingContext2D,
textures: TextureStore
): void {
if (y < 0) return;
ctx.drawImage(
textures.tile[tileColor],
x * TILE_SIZE + BORDER_THICKNESS,
y * TILE_SIZE + BORDER_THICKNESS,
TILE_SIZE,
TILE_SIZE
);
}
reset(): void {
this.sidebar.reset();
this.reDraw = true;
this.gameOver = false;
this.heldPiece = this.sidebar.getNextPiece();
[this.fallTimer, this.movementTimer] = [0, 0];
this.grid.fill(null);
}
}