765 lines
18 KiB
TypeScript
765 lines
18 KiB
TypeScript
type GameState = 'running' | 'paused' | 'not-started' | 'finished';
|
|
|
|
const nope = (x: never): void => {};
|
|
|
|
type PieceCell = 0 | 1;
|
|
type PieceLayout = PieceCell[][];
|
|
|
|
interface UIElement {
|
|
render(context: CanvasRenderingContext2D, options: UIElementRenderOptions): void;
|
|
}
|
|
|
|
interface Dimensions {
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
interface GlobalUIOptions {
|
|
pixelSize: number;
|
|
padding: number;
|
|
canvas: HTMLCanvasElement;
|
|
context: CanvasRenderingContext2D;
|
|
gridLines: false | number;
|
|
borderWidth: number;
|
|
hud: Dimensions;
|
|
playArea: Dimensions;
|
|
}
|
|
|
|
interface UIElementRenderOptions extends GlobalUIOptions {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
interface PieceTranslation {
|
|
xDelta: number;
|
|
yDelta: number;
|
|
}
|
|
|
|
interface TetrissimoCoordinates {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
interface RectangleCoordinates extends TetrissimoCoordinates, Dimensions {}
|
|
|
|
type PieceActionType =
|
|
'moveLeft' |
|
|
'moveRight' |
|
|
'moveDown' |
|
|
'rotateLeft' |
|
|
'rotateRight';
|
|
|
|
interface PieceAction {
|
|
type: PieceActionType;
|
|
}
|
|
|
|
const getSize = (value: number, options: Pick<GlobalUIOptions, 'pixelSize'>): number => value * options.pixelSize;
|
|
|
|
const drawBorderedRect = (
|
|
position: Dimensions & TetrissimoCoordinates,
|
|
context: CanvasRenderingContext2D,
|
|
options: UIElementRenderOptions,
|
|
): void => {
|
|
const borderWidth = options.borderWidth;
|
|
if (!borderWidth) {
|
|
return;
|
|
}
|
|
|
|
const actualLineWidth = getSize(borderWidth, options);
|
|
context.lineWidth = actualLineWidth;
|
|
context.strokeStyle = 'white';
|
|
context.strokeRect(
|
|
getSize(position.x, options) - Math.round(actualLineWidth / 2),
|
|
getSize(position.y, options) - Math.round(actualLineWidth / 2),
|
|
getSize(position.width, options) + actualLineWidth,
|
|
getSize(position.height, options) + actualLineWidth,
|
|
);
|
|
};
|
|
|
|
class TetrissimoLevel {
|
|
public readonly order: number;
|
|
public readonly movementsPerSecond: number;
|
|
|
|
public static readonly level1 = new TetrissimoLevel(1, 0.5);
|
|
public static readonly level2 = new TetrissimoLevel(2, 0.75);
|
|
public static readonly level3 = new TetrissimoLevel(2, 1);
|
|
public static readonly level4 = new TetrissimoLevel(2, 1.5);
|
|
public static readonly level5 = new TetrissimoLevel(2, 2);
|
|
public static readonly level6 = new TetrissimoLevel(2, 3);
|
|
public static readonly level7 = new TetrissimoLevel(2, 4);
|
|
public static readonly level8 = new TetrissimoLevel(2, 6);
|
|
public static readonly level9 = new TetrissimoLevel(2, 8);
|
|
public static readonly level10 = new TetrissimoLevel(2, 10);
|
|
|
|
public constructor(order: number, speed: number) {
|
|
this.order = order;
|
|
this.movementsPerSecond = speed;
|
|
}
|
|
}
|
|
|
|
class TetrissimoGame {
|
|
private startedAt: Date | null = null;
|
|
private pausedAt: Date | null = null;
|
|
private finishedAt: Date | null = null;
|
|
private state: GameState = 'not-started';
|
|
private fps = 60;
|
|
private readonly ui: TetrissimoUI;
|
|
private lastFrameTimes: number[] = [];
|
|
private readonly pieces: TetrissimoPiece[] = [];
|
|
public currentLevel: TetrissimoLevel = TetrissimoLevel.level1;
|
|
private msUntilNextTick = 0;
|
|
private lastTick = new Date();
|
|
private lastUpdate = new Date();
|
|
|
|
public constructor(ui: TetrissimoUI) {
|
|
this.ui = ui;
|
|
}
|
|
|
|
public getState(): GameState {
|
|
return this.state;
|
|
}
|
|
|
|
public setTargetFps(fps: number) {
|
|
this.fps = fps;
|
|
}
|
|
|
|
public setLevel(level: TetrissimoLevel) {
|
|
this.currentLevel = level;
|
|
}
|
|
|
|
public getCurrentFps(): number {
|
|
switch (this.state) {
|
|
case 'running':
|
|
if (this.lastFrameTimes.length <= 1) {
|
|
return 0;
|
|
}
|
|
|
|
const firstFrame = this.lastFrameTimes[0];
|
|
const lastFrame = this.lastFrameTimes[this.lastFrameTimes.length - 1];
|
|
const elapsedMs = lastFrame - firstFrame;
|
|
const avgMsPerFrame = (elapsedMs / this.lastFrameTimes.length);
|
|
return 1000 / avgMsPerFrame;
|
|
case 'paused':
|
|
case 'not-started':
|
|
case 'finished':
|
|
return 0;
|
|
default:
|
|
nope(this.state);
|
|
throw new Error(`Invalid game state "${this.state}"`);
|
|
}
|
|
}
|
|
|
|
public performPieceAction(action: PieceAction) {
|
|
if (this.state !== 'running') {
|
|
return;
|
|
}
|
|
|
|
const piece = this.pieces.filter(piece => piece.state === 'in-play')[0];
|
|
if (!piece) {
|
|
// nothing to do, no pieces in play
|
|
return;
|
|
}
|
|
|
|
const movementIncrement = 1;
|
|
|
|
switch (action.type) {
|
|
case 'moveLeft':
|
|
this.moveIfValid(piece, -movementIncrement, 0);
|
|
break;
|
|
case 'moveRight':
|
|
this.moveIfValid(piece, movementIncrement, 0);
|
|
break;
|
|
case 'moveDown':
|
|
if (!this.moveIfValid(piece, 0, movementIncrement)) {
|
|
// make fixed to bottom of play area
|
|
piece.setFixed();
|
|
}
|
|
break;
|
|
case 'rotateLeft':
|
|
// TODO ensure it's a valid move
|
|
piece.rotateLeft();
|
|
break;
|
|
case 'rotateRight':
|
|
piece.rotateRight();
|
|
break;
|
|
default:
|
|
nope(action.type);
|
|
throw new Error(`invalid action "${action.type}"`);
|
|
}
|
|
}
|
|
|
|
public start() {
|
|
this.pausedAt = null;
|
|
|
|
switch (this.state) {
|
|
case 'running':
|
|
// no-op
|
|
break;
|
|
case 'paused':
|
|
case 'not-started':
|
|
this.lastTick = new Date();
|
|
this.lastUpdate = new Date();
|
|
this.startedAt = new Date();
|
|
this.state = 'running';
|
|
this.update();
|
|
break;
|
|
case 'finished':
|
|
throw new Error('Cannot start a finished game');
|
|
default:
|
|
nope(this.state);
|
|
throw new Error(`Invalid game state "${this.state}"`);
|
|
}
|
|
}
|
|
|
|
public pause() {
|
|
switch (this.state) {
|
|
case 'running':
|
|
case 'paused':
|
|
this.pausedAt = new Date();
|
|
this.state = 'paused';
|
|
this.lastFrameTimes = [];
|
|
break;
|
|
case 'not-started':
|
|
case 'finished':
|
|
throw new Error(`Cannot pause game while it's in state "${this.state}"`);
|
|
default:
|
|
nope(this.state);
|
|
throw new Error(`Invalid game state "${this.state}"`);
|
|
}
|
|
}
|
|
|
|
public update(): void {
|
|
const tickStart = Date.now();
|
|
this.lastFrameTimes.push(tickStart);
|
|
if (this.lastFrameTimes.length > 20) {
|
|
this.lastFrameTimes.shift();
|
|
}
|
|
|
|
switch (this.state) {
|
|
case 'running':
|
|
this.performUpdate();
|
|
break;
|
|
case 'paused':
|
|
case 'not-started':
|
|
case 'finished':
|
|
throw new Error(`Cannot advance game while in state "${this.state}"`);
|
|
default:
|
|
nope(this.state);
|
|
throw new Error(`Invalid game state "${this.state}"`);
|
|
}
|
|
|
|
const elapsed = Date.now() - tickStart;
|
|
const nextTickTimeout = Math.max(0, (1000 / this.fps) - elapsed);
|
|
setTimeout(() => {
|
|
switch (this.state) {
|
|
case 'running':
|
|
this.update();
|
|
break;
|
|
case 'paused':
|
|
case 'not-started':
|
|
case 'finished':
|
|
break;
|
|
default:
|
|
nope(this.state);
|
|
throw new Error(`Invalid game state "${this.state}"`);
|
|
}
|
|
}, nextTickTimeout);
|
|
}
|
|
|
|
private performUpdate(): void {
|
|
const sinceLastUpdate = Date.now() - this.lastUpdate.getTime();
|
|
this.msUntilNextTick -= sinceLastUpdate;
|
|
this.lastUpdate = new Date();
|
|
|
|
if (this.msUntilNextTick <= 0) {
|
|
this.tick();
|
|
}
|
|
|
|
// detect collision
|
|
// is every column filled? remove column
|
|
// is every row filled? end game
|
|
|
|
ui.update();
|
|
}
|
|
|
|
private moveIfValid(piece: TetrissimoPiece, xDelta: number, yDelta: number): boolean {
|
|
const translation: PieceTranslation = {
|
|
xDelta,
|
|
yDelta,
|
|
};
|
|
if (this.ui.canMovePiece(piece, translation)) {
|
|
piece.move(translation);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
private tick(): void {
|
|
const inPlayPieces = this.pieces.filter(piece => piece.state === 'in-play');
|
|
if (!inPlayPieces.length) {
|
|
const newPiece = TetrissimoPiece.T.clone();
|
|
newPiece.state = 'in-play';
|
|
this.pieces.push(newPiece);
|
|
this.ui.addPiece(newPiece);
|
|
} else {
|
|
inPlayPieces.forEach((piece) => {
|
|
if (!this.moveIfValid(piece, 0, 1)) {
|
|
piece.setFixed();
|
|
}
|
|
});
|
|
}
|
|
|
|
this.msUntilNextTick = Math.round(1000 / this.currentLevel.movementsPerSecond);
|
|
this.lastTick = new Date();
|
|
}
|
|
}
|
|
|
|
class TetrissimoUI {
|
|
public readonly playArea: TetrissimoPlayArea;
|
|
private readonly pieces: Array<Readonly<TetrissimoPiece>> = [];
|
|
public readonly nextBox: TetrissimoNextBox;
|
|
public readonly hud: TetrissimoHUD;
|
|
public readonly options: GlobalUIOptions;
|
|
|
|
public constructor(
|
|
canvas: HTMLCanvasElement,
|
|
playArea: TetrissimoPlayArea,
|
|
// nextBox: TetrissimoNextBox,
|
|
hud: TetrissimoHUD,
|
|
) {
|
|
this.playArea = playArea;
|
|
// this.nextBox = nextBox;
|
|
this.hud = hud;
|
|
|
|
const context = canvas.getContext('2d');
|
|
if (!context) {
|
|
throw new Error('failed to get 2d context');
|
|
}
|
|
this.options = {
|
|
borderWidth: 1,
|
|
canvas: canvas,
|
|
context,
|
|
gridLines: 4,
|
|
hud: {
|
|
width: 32,
|
|
height: 12,
|
|
},
|
|
playArea: {
|
|
width: 68,
|
|
height: 128,
|
|
},
|
|
padding: 2,
|
|
pixelSize: 4,
|
|
};
|
|
}
|
|
|
|
public setOptions(options: Omit<GlobalUIOptions, 'canvas' | 'context'>) {
|
|
Object.keys(options).forEach((key) => {
|
|
this.options[key] = options[key];
|
|
});
|
|
}
|
|
|
|
public addPiece(piece: TetrissimoPiece) {
|
|
this.pieces.push(piece);
|
|
}
|
|
|
|
private getPiecePosition(piece: Readonly<TetrissimoPiece>): TetrissimoCoordinates {
|
|
const origin = this.getPieceOrigin(piece);
|
|
const offset = piece.getOffset();
|
|
|
|
return {
|
|
x: origin.x + offset.x,
|
|
y: origin.y + offset.y,
|
|
};
|
|
}
|
|
|
|
public canMovePiece(piece: Readonly<TetrissimoPiece>, delta: PieceTranslation): boolean {
|
|
// check each block in layout's edge to see if it's in a valid area?s
|
|
|
|
const layout = piece.getLayout();
|
|
const position = this.getPiecePosition(piece);
|
|
|
|
for (let i = 0; i < layout.length; i++) {
|
|
const blockTopEdge = position.y + (i * TetrissimoPiece.blockLength) + (delta.yDelta * TetrissimoPiece.blockLength);
|
|
for (let j = 0; j < layout[i].length; j++) {
|
|
if (layout[i][j] === 0) {
|
|
continue;
|
|
}
|
|
|
|
const blockLeftEdge = position.x + (j * TetrissimoPiece.blockLength) + (delta.xDelta * TetrissimoPiece.blockLength);
|
|
|
|
const isCollision = this.hasCollision({
|
|
x: blockLeftEdge,
|
|
y: blockTopEdge,
|
|
width: TetrissimoPiece.blockLength,
|
|
height: TetrissimoPiece.blockLength,
|
|
});
|
|
|
|
if (isCollision) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private hasCollision(rect: RectangleCoordinates): boolean {
|
|
const playAreaLeftEdge = this.options.padding;
|
|
const playAreaRightEdge = playAreaLeftEdge + this.options.playArea.width;
|
|
const playAreaTopEdge = this.options.padding;
|
|
const playAreaBottomEdge = playAreaTopEdge + this.options.playArea.height;
|
|
|
|
const leftEdge = rect.x;
|
|
const rightEdge = rect.x + rect.width;
|
|
const topEdge = rect.y;
|
|
const bottomEdge = rect.y + rect.height;
|
|
|
|
const isInPlayArea =
|
|
leftEdge >= playAreaLeftEdge &&
|
|
rightEdge <= playAreaRightEdge &&
|
|
bottomEdge <= playAreaBottomEdge &&
|
|
topEdge >= playAreaTopEdge;
|
|
|
|
if (!isInPlayArea) {
|
|
return true;
|
|
}
|
|
|
|
const fixedPieces = this.pieces.filter(piece => piece.state === 'fixed');
|
|
for (const piece of fixedPieces) {
|
|
const position = this.getPiecePosition(piece);
|
|
const layout = piece.getLayout();
|
|
for (let i = 0; i < layout.length; i++) {
|
|
const blockTopEdge = position.y + (i * TetrissimoPiece.blockLength);
|
|
const blockBottomEdge = blockTopEdge + TetrissimoPiece.blockLength;
|
|
for (let j = 0; j < layout[i].length; j++) {
|
|
if (layout[i][j] === 0) {
|
|
continue;
|
|
}
|
|
|
|
const blockLeftEdge = position.x + (j * TetrissimoPiece.blockLength);
|
|
const blockRightEdge = blockLeftEdge + TetrissimoPiece.blockLength;
|
|
|
|
const isCollision =
|
|
leftEdge >= blockLeftEdge &&
|
|
rightEdge <= blockRightEdge &&
|
|
bottomEdge <= blockBottomEdge &&
|
|
topEdge >= blockTopEdge;
|
|
|
|
if (isCollision) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public update() {
|
|
const options = this.options;
|
|
const context = this.options.context;
|
|
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// fill background
|
|
context.fillStyle = 'rgb(224,158,107)';
|
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// draw some grid lines
|
|
if (typeof(options.gridLines) === 'number') {
|
|
context.strokeStyle = 'rgba(255, 255, 255, 0.25)';
|
|
context.lineWidth = 1;
|
|
const increment = options.pixelSize * options.gridLines;
|
|
for (let i = 0; i < options.canvas.width; i += increment) {
|
|
// draw vertical line
|
|
context.beginPath();
|
|
context.moveTo(i, 0);
|
|
context.lineTo(i, options.canvas.height);
|
|
context.stroke();
|
|
}
|
|
for (let i = 0; i < options.canvas.height; i += increment) {
|
|
// draw horizontal lines
|
|
context.beginPath();
|
|
context.moveTo(0, i);
|
|
context.lineTo(options.canvas.width, i);
|
|
context.stroke();
|
|
}
|
|
}
|
|
|
|
const totalWidth = Math.floor(canvas.width / options.pixelSize);
|
|
const padding = options.padding;
|
|
|
|
// draw HUD in top right corner
|
|
this.hud.render(context, {
|
|
...options,
|
|
x: totalWidth - padding - options.hud.width,
|
|
y: padding,
|
|
});
|
|
|
|
// draw play area
|
|
this.playArea.render(context, {
|
|
...options,
|
|
x: padding,
|
|
y: padding,
|
|
});
|
|
|
|
this.pieces.forEach((piece, i) => {
|
|
piece.render(context, {
|
|
...options,
|
|
...this.getPieceOrigin(piece),
|
|
});
|
|
});
|
|
}
|
|
|
|
private getPieceOrigin(piece: Readonly<TetrissimoPiece>): TetrissimoCoordinates {
|
|
const length = TetrissimoPiece.blockLength;
|
|
// TODO rotation will cause problems, need a "PieceDefinition" or something with a static layout
|
|
let translateX = Math.floor((piece.getBlockWidth() * length) / 2);
|
|
|
|
// we can only perform movements in multiples of the blockLength, so we must clamp it
|
|
// to ensure it's not offset
|
|
translateX -= (translateX % length);
|
|
const padding = this.options.padding;
|
|
return {
|
|
x: padding + Math.round(this.options.playArea.width / 2) - translateX,
|
|
y: padding,
|
|
};
|
|
}
|
|
}
|
|
|
|
class TetrissimoNextBox implements UIElement {
|
|
public render(context: CanvasRenderingContext2D, options: UIElementRenderOptions) {
|
|
|
|
}
|
|
}
|
|
|
|
class TetrissimoHUD implements UIElement {
|
|
public render(context: CanvasRenderingContext2D, options: UIElementRenderOptions) {
|
|
const position = {
|
|
x: options.x,
|
|
y: options.y,
|
|
width: options.hud.width,
|
|
height: options.hud.height,
|
|
};
|
|
drawBorderedRect(position, context, options);
|
|
}
|
|
}
|
|
|
|
class TetrissimoPlayArea implements UIElement {
|
|
public render(context: CanvasRenderingContext2D, options: UIElementRenderOptions) {
|
|
const position = {
|
|
x: options.x,
|
|
y: options.y,
|
|
width: options.playArea.width,
|
|
height: options.playArea.height,
|
|
};
|
|
drawBorderedRect(position, context, options);
|
|
}
|
|
}
|
|
|
|
type PieceState = 'in-play' | 'fixed';
|
|
|
|
class TetrissimoPiece implements UIElement {
|
|
public readonly name: string;
|
|
private readonly originalLayout: Readonly<PieceLayout>;
|
|
private layout: PieceLayout;
|
|
private offset: TetrissimoCoordinates = { x: 0, y: 0 };
|
|
private readonly color: string;
|
|
public static readonly blockLength = 4;
|
|
public state: PieceState = 'in-play';
|
|
|
|
public static readonly J = new TetrissimoPiece(
|
|
'J',
|
|
[
|
|
[0, 1],
|
|
[0, 1],
|
|
[1, 1],
|
|
],
|
|
'black',
|
|
);
|
|
public static readonly S: TetrissimoPiece = new TetrissimoPiece(
|
|
'S',
|
|
[
|
|
[0, 1, 1],
|
|
[1, 1, 0],
|
|
],
|
|
'green',
|
|
);
|
|
public static readonly T: TetrissimoPiece = new TetrissimoPiece(
|
|
'T',
|
|
[
|
|
[1, 1, 1],
|
|
[0, 1, 0],
|
|
],
|
|
'blue',
|
|
);
|
|
public static readonly Z: TetrissimoPiece = new TetrissimoPiece(
|
|
'Z',
|
|
[
|
|
[1, 1, 0,],
|
|
[0, 1, 1,],
|
|
],
|
|
'yellow',
|
|
);
|
|
public static readonly I: TetrissimoPiece = new TetrissimoPiece(
|
|
'I',
|
|
[
|
|
[1, 1, 1, 1],
|
|
],
|
|
'magenta',
|
|
);
|
|
public static readonly L: TetrissimoPiece = new TetrissimoPiece(
|
|
'L',
|
|
[
|
|
[1],
|
|
[1],
|
|
[1, 1],
|
|
],
|
|
'cyan',
|
|
);
|
|
public static readonly O: TetrissimoPiece = new TetrissimoPiece(
|
|
'O',
|
|
[
|
|
[1, 1],
|
|
[1, 1],
|
|
],
|
|
'red',
|
|
);
|
|
|
|
public constructor(name: string, layout: PieceLayout, color: string) {
|
|
this.name = name;
|
|
this.originalLayout = layout.map(row => row.concat([]));
|
|
this.layout = layout.concat([]);
|
|
this.color = color;
|
|
}
|
|
|
|
public getOffset(): Readonly<TetrissimoCoordinates> {
|
|
return this.offset;
|
|
}
|
|
|
|
public getLayout(): Readonly<PieceLayout> {
|
|
return this.layout;
|
|
}
|
|
|
|
public move(translation: PieceTranslation) {
|
|
if (this.state !== 'in-play') {
|
|
return;
|
|
}
|
|
|
|
this.offset.x += (translation.xDelta * TetrissimoPiece.blockLength);
|
|
this.offset.y += (translation.yDelta * TetrissimoPiece.blockLength);
|
|
}
|
|
|
|
public rotateLeft() {
|
|
if (this.state !== 'in-play') {
|
|
return;
|
|
}
|
|
|
|
// clockwise turn
|
|
const newLayout: PieceLayout = [];
|
|
for (let i = 0; i < this.layout.length; i++) {
|
|
const col = this.layout.length - 1 - i;
|
|
for (let j = 0; j < this.layout[i].length; j++) {
|
|
const row = j;
|
|
if (!newLayout[row]) {
|
|
newLayout[row] = [];
|
|
}
|
|
newLayout[row][col] = this.layout[i][j];
|
|
}
|
|
}
|
|
|
|
this.layout = newLayout;
|
|
}
|
|
|
|
public rotateRight() {
|
|
if (this.state !== 'in-play') {
|
|
return;
|
|
}
|
|
|
|
const newLayout: PieceLayout = [];
|
|
for (let i = 0; i < this.layout.length; i++) {
|
|
const col = i;
|
|
for (let j = 0; j < this.layout[i].length; j++) {
|
|
const row = this.layout[i].length - 1 - j;
|
|
if (!newLayout[row]) {
|
|
newLayout[row] = [];
|
|
}
|
|
newLayout[row][col] = this.layout[i][j];
|
|
}
|
|
}
|
|
|
|
this.layout = newLayout;
|
|
}
|
|
|
|
public setFixed() {
|
|
this.state = 'fixed';
|
|
}
|
|
|
|
public getBlockWidth(): number {
|
|
return this.layout
|
|
.map(arr => arr.filter(value => value === 1).length)
|
|
.reduce((max, len) => Math.max(max, len), 0);
|
|
}
|
|
|
|
public getBlockHeight(): number {
|
|
const cols: PieceCell[][] = [];
|
|
for (let i = 0; i < this.layout.length; i++) {
|
|
for (let j = 0; j < this.layout[i].length; j++) {
|
|
if (!cols[j]) {
|
|
cols[j] = [];
|
|
}
|
|
cols[j].push(this.layout[i][j]);
|
|
}
|
|
}
|
|
|
|
return cols
|
|
.map(arr => arr.filter(value => value === 1).length)
|
|
.reduce((max, len) => Math.max(max, len), 0);
|
|
}
|
|
|
|
public clone(): TetrissimoPiece {
|
|
return new TetrissimoPiece(this.name, this.layout, this.color);
|
|
}
|
|
|
|
public render(context: CanvasRenderingContext2D, options: UIElementRenderOptions) {
|
|
this.layout.forEach((columns, rowNum) => {
|
|
columns.forEach((value, colNum) => {
|
|
switch (value) {
|
|
case 0:
|
|
break;
|
|
case 1:
|
|
context.fillStyle = this.color;
|
|
const length = TetrissimoPiece.blockLength;
|
|
const offsetX = this.offset.x;
|
|
const offsetY = this.offset.y;
|
|
|
|
const x = options.x + offsetX + (colNum * length);
|
|
const y = options.y + offsetY + (rowNum * length);
|
|
const actualLength = getSize(length, options);
|
|
context.fillRect(getSize(x, options), getSize(y, options), actualLength, actualLength);
|
|
break;
|
|
default:
|
|
nope(value);
|
|
throw new Error(`Invalid piece cell value "${value}"`);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------
|
|
|
|
const hud = new TetrissimoHUD();
|
|
const playArea = new TetrissimoPlayArea();
|
|
|
|
const canvas = document.getElementsByTagName('canvas')[0];
|
|
if (!canvas) {
|
|
throw new Error('no canvas');
|
|
}
|
|
|
|
const ui = new TetrissimoUI(canvas, playArea, hud);
|
|
const game = new TetrissimoGame(ui);
|
|
|