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; fallTimer: number; movementTimer: number; grid: Array>; 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): 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): 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 { const held = this.heldPiece as HeldPiece; const adjusted: Array = 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); } }