235 lines
5.6 KiB
TypeScript
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);
|
|
}
|
|
}
|