tetrissimo/tetrissimo.js

555 lines
71 KiB
JavaScript
Raw Permalink Normal View History

2021-06-07 17:42:00 +00:00
const nope = (x) => { };
const getSize = (value, options) => value * options.pixelSize;
const drawBorderedRect = (position, context, options) => {
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 {
constructor(order, speed) {
this.order = order;
this.movementsPerSecond = speed;
}
}
TetrissimoLevel.level1 = new TetrissimoLevel(1, 0.5);
TetrissimoLevel.level2 = new TetrissimoLevel(2, 0.75);
TetrissimoLevel.level3 = new TetrissimoLevel(2, 1);
TetrissimoLevel.level4 = new TetrissimoLevel(2, 1.5);
TetrissimoLevel.level5 = new TetrissimoLevel(2, 2);
TetrissimoLevel.level6 = new TetrissimoLevel(2, 3);
TetrissimoLevel.level7 = new TetrissimoLevel(2, 4);
TetrissimoLevel.level8 = new TetrissimoLevel(2, 6);
TetrissimoLevel.level9 = new TetrissimoLevel(2, 8);
TetrissimoLevel.level10 = new TetrissimoLevel(2, 10);
class TetrissimoGame {
constructor(ui) {
this.startedAt = null;
this.pausedAt = null;
this.finishedAt = null;
this.state = 'not-started';
this.fps = 60;
this.lastFrameTimes = [];
this.pieces = [];
this.currentLevel = TetrissimoLevel.level1;
this.msUntilNextTick = 0;
this.lastTick = new Date();
this.lastUpdate = new Date();
this.ui = ui;
}
getState() {
return this.state;
}
setTargetFps(fps) {
this.fps = fps;
}
setLevel(level) {
this.currentLevel = level;
}
getCurrentFps() {
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}"`);
}
}
performPieceAction(action) {
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}"`);
}
}
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}"`);
}
}
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}"`);
}
}
update() {
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);
}
performUpdate() {
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();
}
moveIfValid(piece, xDelta, yDelta) {
const translation = {
xDelta,
yDelta,
};
if (this.ui.canMovePiece(piece, translation)) {
piece.move(translation);
return true;
}
return false;
}
;
tick() {
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 {
constructor(canvas, playArea,
// nextBox: TetrissimoNextBox,
hud) {
this.pieces = [];
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,
};
}
setOptions(options) {
Object.keys(options).forEach((key) => {
this.options[key] = options[key];
});
}
addPiece(piece) {
this.pieces.push(piece);
}
getPiecePosition(piece) {
const origin = this.getPieceOrigin(piece);
const offset = piece.getOffset();
return {
x: origin.x + offset.x,
y: origin.y + offset.y,
};
}
canMovePiece(piece, delta) {
// 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;
}
hasCollision(rect) {
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;
}
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, Object.assign(Object.assign({}, options), { x: totalWidth - padding - options.hud.width, y: padding }));
// draw play area
this.playArea.render(context, Object.assign(Object.assign({}, options), { x: padding, y: padding }));
this.pieces.forEach((piece, i) => {
piece.render(context, Object.assign(Object.assign({}, options), this.getPieceOrigin(piece)));
});
}
getPieceOrigin(piece) {
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 {
render(context, options) {
}
}
class TetrissimoHUD {
render(context, options) {
const position = {
x: options.x,
y: options.y,
width: options.hud.width,
height: options.hud.height,
};
drawBorderedRect(position, context, options);
}
}
class TetrissimoPlayArea {
render(context, options) {
const position = {
x: options.x,
y: options.y,
width: options.playArea.width,
height: options.playArea.height,
};
drawBorderedRect(position, context, options);
}
}
class TetrissimoPiece {
constructor(name, layout, color) {
this.offset = { x: 0, y: 0 };
this.state = 'in-play';
this.name = name;
this.originalLayout = layout.map(row => row.concat([]));
this.layout = layout.concat([]);
this.color = color;
}
getOffset() {
return this.offset;
}
getLayout() {
return this.layout;
}
move(translation) {
if (this.state !== 'in-play') {
return;
}
this.offset.x += (translation.xDelta * TetrissimoPiece.blockLength);
this.offset.y += (translation.yDelta * TetrissimoPiece.blockLength);
}
rotateLeft() {
if (this.state !== 'in-play') {
return;
}
// clockwise turn
const newLayout = [];
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;
}
rotateRight() {
if (this.state !== 'in-play') {
return;
}
const newLayout = [];
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;
}
setFixed() {
this.state = 'fixed';
}
getBlockWidth() {
return this.layout
.map(arr => arr.filter(value => value === 1).length)
.reduce((max, len) => Math.max(max, len), 0);
}
getBlockHeight() {
const cols = [];
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);
}
clone() {
return new TetrissimoPiece(this.name, this.layout, this.color);
}
render(context, options) {
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}"`);
}
});
});
}
}
TetrissimoPiece.blockLength = 4;
TetrissimoPiece.J = new TetrissimoPiece('J', [
[0, 1],
[0, 1],
[1, 1],
], 'black');
TetrissimoPiece.S = new TetrissimoPiece('S', [
[0, 1, 1],
[1, 1, 0],
], 'green');
TetrissimoPiece.T = new TetrissimoPiece('T', [
[1, 1, 1],
[0, 1, 0],
], 'blue');
TetrissimoPiece.Z = new TetrissimoPiece('Z', [
[1, 1, 0,],
[0, 1, 1,],
], 'yellow');
TetrissimoPiece.I = new TetrissimoPiece('I', [
[1, 1, 1, 1],
], 'magenta');
TetrissimoPiece.L = new TetrissimoPiece('L', [
[1],
[1],
[1, 1],
], 'cyan');
TetrissimoPiece.O = new TetrissimoPiece('O', [
[1, 1],
[1, 1],
], 'red');
// ---------------------------------------------
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);
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGV0cmlzc2ltby5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRldHJpc3NpbW8udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUEsTUFBTSxJQUFJLEdBQUcsQ0FBQyxDQUFRLEVBQVEsRUFBRSxHQUFFLENBQUMsQ0FBQztBQXFEcEMsTUFBTSxPQUFPLEdBQUcsQ0FBQyxLQUFhLEVBQUUsT0FBMkMsRUFBVSxFQUFFLENBQUMsS0FBSyxHQUFHLE9BQU8sQ0FBQyxTQUFTLENBQUM7QUFFbEgsTUFBTSxnQkFBZ0IsR0FBRyxDQUN4QixRQUE0QyxFQUM1QyxPQUFpQyxFQUNqQyxPQUErQixFQUN4QixFQUFFO0lBQ1QsTUFBTSxXQUFXLEdBQUcsT0FBTyxDQUFDLFdBQVcsQ0FBQztJQUN4QyxJQUFJLENBQUMsV0FBVyxFQUFFO1FBQ2pCLE9BQU87S0FDUDtJQUVELE1BQU0sZUFBZSxHQUFHLE9BQU8sQ0FBQyxXQUFXLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDdEQsT0FBTyxDQUFDLFNBQVMsR0FBRyxlQUFlLENBQUM7SUFDcEMsT0FBTyxDQUFDLFdBQVcsR0FBRyxPQUFPLENBQUM7SUFDOUIsT0FBTyxDQUFDLFVBQVUsQ0FDakIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsT0FBTyxDQUFDLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxlQUFlLEdBQUcsQ0FBQyxDQUFDLEVBQzlELE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLE9BQU8sQ0FBQyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsZUFBZSxHQUFHLENBQUMsQ0FBQyxFQUM5RCxPQUFPLENBQUMsUUFBUSxDQUFDLEtBQUssRUFBRSxPQUFPLENBQUMsR0FBRyxlQUFlLEVBQ2xELE9BQU8sQ0FBQyxRQUFRLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxHQUFHLGVBQWUsQ0FDbkQsQ0FBQztBQUNILENBQUMsQ0FBQztBQUVGLE1BQU0sZUFBZTtJQWVwQixZQUFtQixLQUFhLEVB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