general cleanup
This commit is contained in:
parent
12638b4131
commit
21ee80b08a
|
@ -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’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>
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
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,7 +114,8 @@ export class GameState {
|
|||
}
|
||||
|
||||
rotate(): void {
|
||||
ifSome(this.heldPiece, (held) => {
|
||||
if (this.heldPiece === null) return;
|
||||
const held = this.heldPiece;
|
||||
const style = held.schema.rotation;
|
||||
if (style === RotationStyle.None) {
|
||||
return;
|
||||
|
@ -130,39 +130,41 @@ export class GameState {
|
|||
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.heldPiece.tiles = rotated;
|
||||
this.reDraw = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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]);
|
||||
if (
|
||||
this.allPointsInEmptyGridSpace(this.heldPieceTilesAdjusted([dx, dy]))
|
||||
) {
|
||||
held.pos = newPos;
|
||||
this.reDraw = true;
|
||||
success = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return success;
|
||||
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,7 +174,10 @@ export class GameState {
|
|||
}
|
||||
|
||||
private flashHeldToGrid(): void {
|
||||
ifSome(this.heldPiece, (held) => {
|
||||
if (this.heldPiece === null) {
|
||||
return;
|
||||
}
|
||||
const held = this.heldPiece;
|
||||
this.heldPieceTilesAdjusted().forEach((pos) => {
|
||||
const index = posToGridIndex(...pos);
|
||||
this.grid[index] = held.schema.color;
|
||||
|
@ -181,14 +186,13 @@ export class GameState {
|
|||
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,11 +208,14 @@ 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 true;
|
||||
}
|
||||
|
||||
private static drawTile(
|
||||
x: number,
|
||||
|
|
|
@ -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);
|
||||
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) {
|
||||
if (!v.polled && v.eventType === KeyEventType.OnlyOnce) {
|
||||
callback();
|
||||
} else if (vv.eventType !== KeyEventType.OnlyOnce) {
|
||||
} else if (v.eventType !== KeyEventType.OnlyOnce) {
|
||||
callback();
|
||||
}
|
||||
vv.polled = true;
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) => {
|
||||
): null | { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
|
||||
const canvas = document.createElement("canvas");
|
||||
if (canvas === null) {
|
||||
return null;
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
ifSome(canvas.getContext("2d"), (ctx) => {
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx === null) {
|
||||
return null;
|
||||
}
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ret = { canvas, ctx };
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
return { canvas, ctx };
|
||||
|
||||
}
|
||||
|
||||
export function clearBody(): void {
|
||||
|
|
|
@ -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.
Loading…
Reference in a new issue