如何将 x->n y->n 对象顺时针旋转 90°?
How to rotate an x->n y->n object clockwise by 90°?
我正在实现一个简单的俄罗斯方块游戏,其中有多个形状:
- S 形
- 一行
- 一个正方形
- 等
在典型的游戏中,当您在键盘上按UP时,下落的形状会顺时针旋转。
我在 JavaScript 实现中将形状定义为点坐标数组。比如S形是:
[
{x:0, y:2},
{x:0, y:1},
{x:1, y:1},
{x:1, y:0}
]
这将转换为以下形状:
往上按,应该把上面的数组转换成下面的数组:
[
{x:0, y:0},
{x:1, y:0},
{x:1, y:1},
{x:2, y:1}
]
又名,这个形状:
为了实现这一点,我...无耻地将坐标硬编码到 shapeRotationMap
对象中:
let shapeRotationMap = {
"line0": [
{x:0, y:0},
{x:0, y:1},
{x:0, y:2},
{x:0, y:3},
],
"line1": [
{x:0, y:0},
{x:1, y:0},
{x:2, y:0},
{x:3, y:0},
],
"leftS0": [
{x:0, y:0},
{x:1, y:0},
{x:1, y:1},
{x:2, y:1},
],
"leftS1": [
{x:0, y:1},
{x:0, y:2},
{x:1, y:0},
{x:1, y:1},
],
"rightS0": [
{x:0, y:1},
{x:1, y:1},
{x:1, y:0},
{x:2, y:0},
],
"rightS1": [
{x:0, y:0},
{x:0, y:1},
{x:1, y:1},
{x:1, y:2},
],
"podium0": [
{x:0, y:1},
{x:1, y:0},
{x:1, y:1},
{x:1, y:2},
],
"podium1": [
{x:0, y:0},
{x:1, y:0},
{x:1, y:1},
{x:2, y:0},
],
"podium2": [
{x:0, y:0},
{x:0, y:1},
{x:0, y:2},
{x:1, y:1},
],
"podium3": [
{x:0, y:1},
{x:1, y:0},
{x:1, y:1},
{x:2, y:1},
]
}
shapeRotationMap["line2"] = shapeRotationMap["line0"];
shapeRotationMap["line3"] = shapeRotationMap["line1"];
shapeRotationMap["leftS2"] = shapeRotationMap["leftS0"];
shapeRotationMap["leftS3"] = shapeRotationMap["leftS1"];
shapeRotationMap["rightS2"] = shapeRotationMap["rightS0"];
shapeRotationMap["rightS3"] = shapeRotationMap["rightS1"];
["square0", "square1", "square2","square3"].forEach(function(key){
shapeRotationMap[key] = [
{x:0, y:0},
{x:0, y:1},
{x:1, y:0},
{x:1, y:1},
];
});
我有一个形状字符串 ("line"
) 和一个旋转 (0
、1
、2
或 3
),这就是我知道的要拿哪个对象。
但是,这会使代码复杂化,很难添加新形状,只是将其张贴在这里我觉得我在侮辱一些程序员。
但是我找不到旋转这样一个对象的算法。
我找到了这个算法:How to rotate a matrix in an array in javascript。但是这里OP有一个二维数组(矩阵),在我的例子中我有一个坐标对象。
有谁知道如何将我的对象旋转 90°?
如果不是,我想我会切换我的逻辑并改用矩阵。
对于简单的形状,只需交换 x 和 y 坐标即可:
function rotateShape(s) {
return s.map(function (p) {
return { x: p.y, y: p.x };
});
}
//TEST
var shape = [
{ x: 0, y: 1 },
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: -1 }
];
var tileSize = 20;
var canvas = document.body.appendChild(document.createElement("canvas"));
canvas.width = canvas.height = tileSize * 6;
var ctx = canvas.getContext("2d");
ctx.strokeStyle = "red";
ctx.fillStyle = "green";
function draw(s, offset) {
if (offset === void 0) { offset = { x: 2, y: 2 }; }
ctx.clearRect(0, 0, canvas.width, canvas.height);
s.forEach(function (p) {
ctx.fillRect((p.x + offset.x) * tileSize, (p.y + offset.y) * tileSize, tileSize, tileSize);
ctx.strokeRect((p.x + offset.x) * tileSize, (p.y + offset.y) * tileSize, tileSize, tileSize);
});
}
setInterval(function () {
shape = rotateShape(shape);
draw(shape);
}, 1000);
以上代码段使用坐标 x:0,y:0
作为轴心点。偏移形状坐标将有效地 "move" 枢轴点。
感谢@NinaScholz in ,我成功地实现了我的俄罗斯方块游戏,并适当地旋转了我的方块
let _game;
var gameLoopHandle;
let possibleShapes = ["square","line","leftS","rightS","podium","lShapeR","lShapeL","zShapeL","zShapeR","fourSquares"];
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.rotateCW = function (c) {
// x' = x cos phi + y sin phi \ formula with pivot at (0, 0)
// y' = -x sin phi + y cos phi /
// phi = 90° insert phi
// cos 90° = 0 sin 90° = 1 calculate cos and sin
// x' = y \ formula with pivot at (0, 0)
// y' = -x /
// x' = (cy - y) + cx \ formula with different pivot needs correction
// y' = -(cx - x) + cy /
// y' = -cx + x + cy /
return new Point(
c.x + c.y - this.y,
c.y - c.x + this.x
);
}
Point.prototype.rotateCCW = function (c) {
// source: https://en.wikipedia.org/wiki/Rotation_(mathematics)#Two_dimensions
// x' = x cos phi + y sin phi \ formula with pivot at (0, 0)
// y' = -x sin phi + y cos phi /
// phi = -90°
// cos -90° = 0 sin -90° = -1
// x' = -y \ formula with pivot at (0, 0)
// y' = x /
// x' = -(cy - y) + cx \ formula with different pivot needs correction
// x' = -cy + y + cx /
// y' = (cx - x) + cy /
return new Point(
c.x - c.y + this.y,
c.y + c.x - this.x
);
}
let Shape = function(shapeStr){
this.shapeStr = shapeStr;
switch(shapeStr){
case "lShapeR":
this.body = [
new Point(0,0),
new Point(1,0),
new Point(2,0),
new Point(0,1)
];
this.pivotPointIndex = 0;
this.canRotate = true;
this.color = "pink";
break;
case "lShapeL":
this.body = [
new Point(0,0),
new Point(1,0),
new Point(2,0),
new Point(2,1)
];
this.pivotPointIndex = 3;
this.canRotate = true;
this.color = "gray";
break;
case "zShapeR":
this.body = [
new Point(0,0),
new Point(1,0),
new Point(1,1),
new Point(1,2),
new Point(2,2)
];
this.pivotPointIndex = 2;
this.canRotate = true;
this.color = "#73C6B6";
break;
case "fourSquares":
this.body = [
new Point(0,0),
new Point(2,0),
new Point(2,2),
new Point(0,2),
];
this.pivotPointIndex = 2;
this.canRotate = false;
this.color = "#E67E22";
break;
case "line":
this.body = [
new Point(0,0),
new Point(0,1),
new Point(0,2),
new Point(0,3)
];
this.pivotPointIndex = 1;
this.canRotate = true;
this.color = "green";
break;
case "leftS":
this.body = [
new Point(0,2),
new Point(0,1),
new Point(1,1),
new Point(1,0)
];
this.pivotPointIndex = 2;
this.canRotate = true;
this.color = "lime";
break;
case "rightS":
this.body = [
new Point(1,2),
new Point(1,1),
new Point(0,1),
new Point(0,0)
];
this.pivotPointIndex = 2;
this.canRotate = true;
this.color = "yellow";
break;
case "podium":
this.body = [
new Point(0,0),
new Point(1,0),
new Point(2,0),
new Point(1,1)
];
this.pivotPointIndex = 1;
this.canRotate = true;
this.color = "brown";
break;
case "square":
default:
this.body = [
new Point(0,0),
new Point(0,1),
new Point(1,1),
new Point(1,0)
];
this.canRotate = false;
this.color = "red";
break;
}
this.rotate = function(){
if (!this.canRotate) return;
for(let i=0; i<this.body.length; i++){
this.body[i] = this.body[i].rotateCW(this.body[this.pivotPointIndex])
}
}
}
let TetrisBlock = function(shapeStr){
this.stepsDown = 0;
this.stepsRight = 0;
this.shape = new Shape(shapeStr);
this.getLowestSquare = function(){
let lowestSquare;
for (let i=0; i<this.shape.body.length;i++){
let thisSquare = this.shape.body[i];
if (!lowestSquare) {
lowestSquare = thisSquare;
} else {
if (thisSquare.y > lowestSquare.y){
lowestSquare = thisSquare;
}
}
}
return lowestSquare;
}
this.containsPoint = function(point){
for (let i = 0; i<this.shape.body.length; i++){
if (this.shape.body[i].x === point.x && this.shape.body[i].y === point.y) return true;
}
return false;
}
this.cloneShape = function(){
let clonedShape = new Shape(this.shape.shapeStr);
let nonPoints = JSON.parse(JSON.stringify(this.shape.body));
let actualPoints = [];
for (let i = 0; i<nonPoints.length; i++){
actualPoints.push(new Point(nonPoints[i].x, nonPoints[i].y));
}
clonedShape.body = actualPoints;
return clonedShape;
}
this.rotate = function(){
this.shape.rotate();
}
this.moveDown = function(){
for (let i=0; i<this.shape.body.length;i++){
this.shape.body[i].y++;
}
this.stepsDown++;
}
this.moveLeft = function(){
for (let i=0; i<this.shape.body.length;i++){
this.shape.body[i].x--;
}
this.stepsRight--;
}
this.moveRight = function(){
for (let i=0; i<this.shape.body.length;i++){
this.shape.body[i].x++;
}
this.stepsRight++;
}
}
let getLeftMostSquare = function(body){
let leftMostsquare;
for (let i=0; i<body.length;i++){
let thisSquare = body[i];
if (!leftMostsquare) {
leftMostsquare = thisSquare;
} else {
if (leftMostsquare.x > thisSquare.x){
leftMostsquare = thisSquare;
}
}
}
return leftMostsquare;
}
let getRightMostSquare = function(body){
let rightMostSquare;
for (let i=0; i<body.length;i++){
let thisSquare = body[i];
if (!rightMostSquare) {
rightMostSquare = thisSquare;
} else {
if (rightMostSquare.x < thisSquare.x){
rightMostSquare = thisSquare;
}
}
}
return rightMostSquare;
}
let TetrisGame = function(){
this.board = [];
this.score = 0;
this.speed = 1000;
this.ended = false;
this.boardSizeY = 40;
this.boardSizeX = 20;
this.manager = {};
this.fallingBlock = new TetrisBlock(possibleShapes[getRandomInt(0,possibleShapes.length-1)]);
this.blocksFallen = [];
}
TetrisGame.prototype.init = function(options){
options = options || {};
this.boardSizeY = options.boardSizeY || 20;
this.boardSizeX = options.boardSizeX || 20;
}
TetrisGame.prototype.generateBoard = function(){
this.board = [];
for (let i=0;i<this.boardSizeY;i++){
let boardRow=[];
for (let j = 0; j < this.boardSizeX; j++) {
let hadBlock = false;
for (let k = 0; k<this.fallingBlock.shape.body.length;k++){
if(this.fallingBlock.shape.body[k].y == i &&
this.fallingBlock.shape.body[k].x == j) {
boardRow.push(this.fallingBlock.shape.color);
hadBlock = true;
}
}
for (let l = 0; l<this.blocksFallen.length;l++){
for (let k = 0; k<this.blocksFallen[l].shape.body.length;k++){
if(this.blocksFallen[l].shape.body[k].y == i &&
this.blocksFallen[l].shape.body[k].x == j) {
boardRow.push(this.blocksFallen[l].shape.color);
hadBlock = true;
}
}
}
if (!hadBlock)
boardRow.push(0);
}
this.board.push(boardRow);
}
}
TetrisGame.prototype.setSpeed = function(speed){
this.speed = speed;
}
TetrisGame.prototype.setScore = function(score){
this.score = score;
}
function outOfBoundsDownOrIsBlock(game,tetrisBody){
game.generateBoard();
let gameBoard = game.board;
for (let i = 0; i<tetrisBody.length;i++){
let tetrisBlock = tetrisBody[i];
for(let j = 0; j<gameBoard.length;j++){
for(let k = 0; k<gameBoard[j].length;k++){
let gameBlock = gameBoard[j][k];
if (gameBlock) {
let blockPoint = {x:k,y:j}
if(!game.fallingBlock.containsPoint(blockPoint)){
if (tetrisBlock.x == blockPoint.x && tetrisBlock.y == blockPoint.y) return true;
}
}
}
}
};
return game.fallingBlock.getLowestSquare().y == game.boardSizeY - 1;
}
function haveCollision(game,tetrisBody){
game.generateBoard();
let fallenBlocks = game.blocksFallen;
// left edge
if (getLeftMostSquare(tetrisBody).x < 0) return true;
// right edge
if (getRightMostSquare(tetrisBody).x == game.boardSizeX) return true;
// fallenBlocks
for (let i = 0; i<tetrisBody.length;i++){
let tetrisBlock = tetrisBody[i];
for(let j = 0; j<fallenBlocks.length;j++){
let thisFallenBlock = fallenBlocks[j];
for (let k=0;k<thisFallenBlock.shape.body.length;k++){
let fallenBlockPoint = thisFallenBlock.shape.body[k];
if (tetrisBlock.x == fallenBlockPoint.x && tetrisBlock.y == fallenBlockPoint.y) return true;
}
}
}
return false;
}
TetrisGame.prototype.rotateIfCan = function(){
if (!this.fallingBlock.shape.canRotate) return;
let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
rawBlock.rotate();
if (!haveCollision(this,rawBlock.shape.body)) this.fallingBlock.rotate();
}
TetrisGame.prototype.moveLeftIfCan = function(){
let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
rawBlock.moveLeft();
if (!haveCollision(this,rawBlock.shape.body)) this.fallingBlock.moveLeft();
}
TetrisGame.prototype.generateNewBlockOrGameOver = function(){
this.fallingBlock = new TetrisBlock(possibleShapes[getRandomInt(0,possibleShapes.length-1)]);
if (haveCollision(this,this.fallingBlock.shape.body)) {
this.ended = true;
} else {
this.score++;
this.generateBoard();
let lineCount = 0;
for (let i=0; i<this.board.length;i++){
let boardLine = this.board[i];
let pointCountPerLine = 0;
for (let j=0;j<boardLine.length;j++) {
let blockPoint = boardLine[j];
if (blockPoint !== 0 && typeof blockPoint === "string") pointCountPerLine++;
}
if (pointCountPerLine === boardLine.length) {
lineCount++;
for (let k=0;k<this.blocksFallen.length;k++){
let thisFallenBlock = this.blocksFallen[k];
for (let p = 0; p<thisFallenBlock.shape.body.length;p++){
let thisFallenBlockPoint = thisFallenBlock.shape.body[p];
if(thisFallenBlockPoint.y === i) {
thisFallenBlock.shape.body.splice(p,1,{dummy:true});
}
}
}
for (let k=0;k<this.blocksFallen.length;k++){
let thisFallenBlock = this.blocksFallen[k];
for (let p = 0; p<thisFallenBlock.shape.body.length;p++){
let thisFallenBlockPoint = thisFallenBlock.shape.body[p];
if(thisFallenBlockPoint.y < i) {
thisFallenBlockPoint.y++;
}
}
}
}
}
if (lineCount) this.score += lineCount * 10;
if (lineCount == 4) this.score += 40;
this.generateBoard();
}
}
TetrisGame.prototype.teleportDown = function(){
let movedDown = this.moveDownOrNewBlock();
while (movedDown) {
movedDown = this.moveDownOrNewBlock();
}
}
TetrisGame.prototype.moveDownOrNewBlock = function(){
let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
rawBlock.moveDown();
if (outOfBoundsDownOrIsBlock(this,rawBlock.shape.body)) {
rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
this.blocksFallen.push(rawBlock);
return this.generateNewBlockOrGameOver();
}
if (!haveCollision(this,rawBlock.shape.body)) {
this.fallingBlock.moveDown();
return true;
}
}
TetrisGame.prototype.moveRightIfCan = function(){
let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
rawBlock.moveRight();
if (!haveCollision(this,rawBlock.shape.body)) this.fallingBlock.moveRight();
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
let genericDiv = function(color){
let returnDiv = document.createElement("div");
returnDiv.style.height = "10px";
returnDiv.style.width = "10px";
returnDiv.style.background = color;
return returnDiv;
}
let emptyDiv = function(){
return genericDiv("black");
}
function updateDOM(game) {
var el = document.getElementById("gameboard");
el.innerHTML = "";
el.style.position = "relative";
var scoreEl = document.getElementById("score");
scoreEl.innerText = game.score;
for (let i =0;i<game.board.length;i++){
let rowDiv = document.createElement("div");
//snakeRowDiv.style.position = "absolute";
for (let j=0;j<game.board[i].length;j++){
if (game.board[i][j]){
whichDiv = genericDiv(game.board[i][j]);
} else {
whichDiv = emptyDiv();
}
whichDiv.style.position = "absolute";
whichDiv.style.left = j * (parseInt(whichDiv.style.width)) + "px";
whichDiv.style.top = (i * (parseInt(whichDiv.style.height)) + 100) + "px";
rowDiv.appendChild(whichDiv);
}
el.appendChild(rowDiv);
}
}
function generateDomListener(game){
return function(event){
switch (event.key) {
case "ArrowUp":
game.rotateIfCan();
game.generateBoard();
updateDOM(game);
break;
case "ArrowDown":
game.teleportDown();
game.generateBoard();
updateDOM(game);
break;
case "ArrowLeft":
game.moveLeftIfCan();
game.generateBoard();
updateDOM(game);
break;
case "ArrowRight":
game.moveRightIfCan();
game.generateBoard();
updateDOM(game);
break;
}
}
}
function refreshInterval(game){
clearInterval(gameLoopHandle);
gameLoopHandle = setInterval(gameLoop(game), game.speed);
}
function decreaseDifficulty(game){
if (game.speed <= 900) {
game.speed += 50;
}
clearInterval(gameLoopHandle);
gameLoopHandle = setInterval(gameLoop(game), game.speed);
}
function restart(game){
game.ended = false;
game.genApple = true;
game.score = 0;
game.speed = 500;
game.apple = {x:null,y:null}
game.snake.body = [
{x:9,y:8},
{x:9,y:9},
{x:9,y:10},
{x:9,y:11},
]
game.snake.going = "RIGHT";
clearInterval(gameLoopHandle);
gameLoopHandle = setInterval(gameLoop(game), game.speed);
}
function increaseDifficulty(game){
if (game.speed >= 100) {
game.speed -= 50;
}
clearInterval(gameLoopHandle);
gameLoopHandle = setInterval(gameLoop(game), game.speed);
}
function gameLoop(game){
return function(){
if (!game.ended) {
game.moveDownOrNewBlock();
if (!game.ended) {
game.generateBoard();
updateDOM(game);
}
} else {
clearInterval(gameLoopHandle);
alert ("GAME OVER");
}
}
}
document.addEventListener("DOMContentLoaded", function(event) {
var game = new TetrisGame();
_game =game;
game.init();
game.generateBoard()
updateDOM(game);
document.addEventListener("keydown", generateDomListener(game));
gameLoopHandle = setInterval(gameLoop(game), game.speed);
})
<div id="gameboard"></div>
<div>
<h1>Score: <span id="score">0</span></h1>
<button onclick="increaseDifficulty(_game)">Increase difficulty</button>
<button onclick="decreaseDifficulty(_game)">Decrease difficulty</button>
</div>
我正在实现一个简单的俄罗斯方块游戏,其中有多个形状:
- S 形
- 一行
- 一个正方形
- 等
在典型的游戏中,当您在键盘上按UP时,下落的形状会顺时针旋转。
我在 JavaScript 实现中将形状定义为点坐标数组。比如S形是:
[
{x:0, y:2},
{x:0, y:1},
{x:1, y:1},
{x:1, y:0}
]
这将转换为以下形状:
往上按,应该把上面的数组转换成下面的数组:
[
{x:0, y:0},
{x:1, y:0},
{x:1, y:1},
{x:2, y:1}
]
又名,这个形状:
为了实现这一点,我...无耻地将坐标硬编码到 shapeRotationMap
对象中:
let shapeRotationMap = {
"line0": [
{x:0, y:0},
{x:0, y:1},
{x:0, y:2},
{x:0, y:3},
],
"line1": [
{x:0, y:0},
{x:1, y:0},
{x:2, y:0},
{x:3, y:0},
],
"leftS0": [
{x:0, y:0},
{x:1, y:0},
{x:1, y:1},
{x:2, y:1},
],
"leftS1": [
{x:0, y:1},
{x:0, y:2},
{x:1, y:0},
{x:1, y:1},
],
"rightS0": [
{x:0, y:1},
{x:1, y:1},
{x:1, y:0},
{x:2, y:0},
],
"rightS1": [
{x:0, y:0},
{x:0, y:1},
{x:1, y:1},
{x:1, y:2},
],
"podium0": [
{x:0, y:1},
{x:1, y:0},
{x:1, y:1},
{x:1, y:2},
],
"podium1": [
{x:0, y:0},
{x:1, y:0},
{x:1, y:1},
{x:2, y:0},
],
"podium2": [
{x:0, y:0},
{x:0, y:1},
{x:0, y:2},
{x:1, y:1},
],
"podium3": [
{x:0, y:1},
{x:1, y:0},
{x:1, y:1},
{x:2, y:1},
]
}
shapeRotationMap["line2"] = shapeRotationMap["line0"];
shapeRotationMap["line3"] = shapeRotationMap["line1"];
shapeRotationMap["leftS2"] = shapeRotationMap["leftS0"];
shapeRotationMap["leftS3"] = shapeRotationMap["leftS1"];
shapeRotationMap["rightS2"] = shapeRotationMap["rightS0"];
shapeRotationMap["rightS3"] = shapeRotationMap["rightS1"];
["square0", "square1", "square2","square3"].forEach(function(key){
shapeRotationMap[key] = [
{x:0, y:0},
{x:0, y:1},
{x:1, y:0},
{x:1, y:1},
];
});
我有一个形状字符串 ("line"
) 和一个旋转 (0
、1
、2
或 3
),这就是我知道的要拿哪个对象。
但是,这会使代码复杂化,很难添加新形状,只是将其张贴在这里我觉得我在侮辱一些程序员。
但是我找不到旋转这样一个对象的算法。
我找到了这个算法:How to rotate a matrix in an array in javascript。但是这里OP有一个二维数组(矩阵),在我的例子中我有一个坐标对象。
有谁知道如何将我的对象旋转 90°?
如果不是,我想我会切换我的逻辑并改用矩阵。
对于简单的形状,只需交换 x 和 y 坐标即可:
function rotateShape(s) {
return s.map(function (p) {
return { x: p.y, y: p.x };
});
}
//TEST
var shape = [
{ x: 0, y: 1 },
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: -1 }
];
var tileSize = 20;
var canvas = document.body.appendChild(document.createElement("canvas"));
canvas.width = canvas.height = tileSize * 6;
var ctx = canvas.getContext("2d");
ctx.strokeStyle = "red";
ctx.fillStyle = "green";
function draw(s, offset) {
if (offset === void 0) { offset = { x: 2, y: 2 }; }
ctx.clearRect(0, 0, canvas.width, canvas.height);
s.forEach(function (p) {
ctx.fillRect((p.x + offset.x) * tileSize, (p.y + offset.y) * tileSize, tileSize, tileSize);
ctx.strokeRect((p.x + offset.x) * tileSize, (p.y + offset.y) * tileSize, tileSize, tileSize);
});
}
setInterval(function () {
shape = rotateShape(shape);
draw(shape);
}, 1000);
以上代码段使用坐标 x:0,y:0
作为轴心点。偏移形状坐标将有效地 "move" 枢轴点。
感谢@NinaScholz
let _game;
var gameLoopHandle;
let possibleShapes = ["square","line","leftS","rightS","podium","lShapeR","lShapeL","zShapeL","zShapeR","fourSquares"];
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.rotateCW = function (c) {
// x' = x cos phi + y sin phi \ formula with pivot at (0, 0)
// y' = -x sin phi + y cos phi /
// phi = 90° insert phi
// cos 90° = 0 sin 90° = 1 calculate cos and sin
// x' = y \ formula with pivot at (0, 0)
// y' = -x /
// x' = (cy - y) + cx \ formula with different pivot needs correction
// y' = -(cx - x) + cy /
// y' = -cx + x + cy /
return new Point(
c.x + c.y - this.y,
c.y - c.x + this.x
);
}
Point.prototype.rotateCCW = function (c) {
// source: https://en.wikipedia.org/wiki/Rotation_(mathematics)#Two_dimensions
// x' = x cos phi + y sin phi \ formula with pivot at (0, 0)
// y' = -x sin phi + y cos phi /
// phi = -90°
// cos -90° = 0 sin -90° = -1
// x' = -y \ formula with pivot at (0, 0)
// y' = x /
// x' = -(cy - y) + cx \ formula with different pivot needs correction
// x' = -cy + y + cx /
// y' = (cx - x) + cy /
return new Point(
c.x - c.y + this.y,
c.y + c.x - this.x
);
}
let Shape = function(shapeStr){
this.shapeStr = shapeStr;
switch(shapeStr){
case "lShapeR":
this.body = [
new Point(0,0),
new Point(1,0),
new Point(2,0),
new Point(0,1)
];
this.pivotPointIndex = 0;
this.canRotate = true;
this.color = "pink";
break;
case "lShapeL":
this.body = [
new Point(0,0),
new Point(1,0),
new Point(2,0),
new Point(2,1)
];
this.pivotPointIndex = 3;
this.canRotate = true;
this.color = "gray";
break;
case "zShapeR":
this.body = [
new Point(0,0),
new Point(1,0),
new Point(1,1),
new Point(1,2),
new Point(2,2)
];
this.pivotPointIndex = 2;
this.canRotate = true;
this.color = "#73C6B6";
break;
case "fourSquares":
this.body = [
new Point(0,0),
new Point(2,0),
new Point(2,2),
new Point(0,2),
];
this.pivotPointIndex = 2;
this.canRotate = false;
this.color = "#E67E22";
break;
case "line":
this.body = [
new Point(0,0),
new Point(0,1),
new Point(0,2),
new Point(0,3)
];
this.pivotPointIndex = 1;
this.canRotate = true;
this.color = "green";
break;
case "leftS":
this.body = [
new Point(0,2),
new Point(0,1),
new Point(1,1),
new Point(1,0)
];
this.pivotPointIndex = 2;
this.canRotate = true;
this.color = "lime";
break;
case "rightS":
this.body = [
new Point(1,2),
new Point(1,1),
new Point(0,1),
new Point(0,0)
];
this.pivotPointIndex = 2;
this.canRotate = true;
this.color = "yellow";
break;
case "podium":
this.body = [
new Point(0,0),
new Point(1,0),
new Point(2,0),
new Point(1,1)
];
this.pivotPointIndex = 1;
this.canRotate = true;
this.color = "brown";
break;
case "square":
default:
this.body = [
new Point(0,0),
new Point(0,1),
new Point(1,1),
new Point(1,0)
];
this.canRotate = false;
this.color = "red";
break;
}
this.rotate = function(){
if (!this.canRotate) return;
for(let i=0; i<this.body.length; i++){
this.body[i] = this.body[i].rotateCW(this.body[this.pivotPointIndex])
}
}
}
let TetrisBlock = function(shapeStr){
this.stepsDown = 0;
this.stepsRight = 0;
this.shape = new Shape(shapeStr);
this.getLowestSquare = function(){
let lowestSquare;
for (let i=0; i<this.shape.body.length;i++){
let thisSquare = this.shape.body[i];
if (!lowestSquare) {
lowestSquare = thisSquare;
} else {
if (thisSquare.y > lowestSquare.y){
lowestSquare = thisSquare;
}
}
}
return lowestSquare;
}
this.containsPoint = function(point){
for (let i = 0; i<this.shape.body.length; i++){
if (this.shape.body[i].x === point.x && this.shape.body[i].y === point.y) return true;
}
return false;
}
this.cloneShape = function(){
let clonedShape = new Shape(this.shape.shapeStr);
let nonPoints = JSON.parse(JSON.stringify(this.shape.body));
let actualPoints = [];
for (let i = 0; i<nonPoints.length; i++){
actualPoints.push(new Point(nonPoints[i].x, nonPoints[i].y));
}
clonedShape.body = actualPoints;
return clonedShape;
}
this.rotate = function(){
this.shape.rotate();
}
this.moveDown = function(){
for (let i=0; i<this.shape.body.length;i++){
this.shape.body[i].y++;
}
this.stepsDown++;
}
this.moveLeft = function(){
for (let i=0; i<this.shape.body.length;i++){
this.shape.body[i].x--;
}
this.stepsRight--;
}
this.moveRight = function(){
for (let i=0; i<this.shape.body.length;i++){
this.shape.body[i].x++;
}
this.stepsRight++;
}
}
let getLeftMostSquare = function(body){
let leftMostsquare;
for (let i=0; i<body.length;i++){
let thisSquare = body[i];
if (!leftMostsquare) {
leftMostsquare = thisSquare;
} else {
if (leftMostsquare.x > thisSquare.x){
leftMostsquare = thisSquare;
}
}
}
return leftMostsquare;
}
let getRightMostSquare = function(body){
let rightMostSquare;
for (let i=0; i<body.length;i++){
let thisSquare = body[i];
if (!rightMostSquare) {
rightMostSquare = thisSquare;
} else {
if (rightMostSquare.x < thisSquare.x){
rightMostSquare = thisSquare;
}
}
}
return rightMostSquare;
}
let TetrisGame = function(){
this.board = [];
this.score = 0;
this.speed = 1000;
this.ended = false;
this.boardSizeY = 40;
this.boardSizeX = 20;
this.manager = {};
this.fallingBlock = new TetrisBlock(possibleShapes[getRandomInt(0,possibleShapes.length-1)]);
this.blocksFallen = [];
}
TetrisGame.prototype.init = function(options){
options = options || {};
this.boardSizeY = options.boardSizeY || 20;
this.boardSizeX = options.boardSizeX || 20;
}
TetrisGame.prototype.generateBoard = function(){
this.board = [];
for (let i=0;i<this.boardSizeY;i++){
let boardRow=[];
for (let j = 0; j < this.boardSizeX; j++) {
let hadBlock = false;
for (let k = 0; k<this.fallingBlock.shape.body.length;k++){
if(this.fallingBlock.shape.body[k].y == i &&
this.fallingBlock.shape.body[k].x == j) {
boardRow.push(this.fallingBlock.shape.color);
hadBlock = true;
}
}
for (let l = 0; l<this.blocksFallen.length;l++){
for (let k = 0; k<this.blocksFallen[l].shape.body.length;k++){
if(this.blocksFallen[l].shape.body[k].y == i &&
this.blocksFallen[l].shape.body[k].x == j) {
boardRow.push(this.blocksFallen[l].shape.color);
hadBlock = true;
}
}
}
if (!hadBlock)
boardRow.push(0);
}
this.board.push(boardRow);
}
}
TetrisGame.prototype.setSpeed = function(speed){
this.speed = speed;
}
TetrisGame.prototype.setScore = function(score){
this.score = score;
}
function outOfBoundsDownOrIsBlock(game,tetrisBody){
game.generateBoard();
let gameBoard = game.board;
for (let i = 0; i<tetrisBody.length;i++){
let tetrisBlock = tetrisBody[i];
for(let j = 0; j<gameBoard.length;j++){
for(let k = 0; k<gameBoard[j].length;k++){
let gameBlock = gameBoard[j][k];
if (gameBlock) {
let blockPoint = {x:k,y:j}
if(!game.fallingBlock.containsPoint(blockPoint)){
if (tetrisBlock.x == blockPoint.x && tetrisBlock.y == blockPoint.y) return true;
}
}
}
}
};
return game.fallingBlock.getLowestSquare().y == game.boardSizeY - 1;
}
function haveCollision(game,tetrisBody){
game.generateBoard();
let fallenBlocks = game.blocksFallen;
// left edge
if (getLeftMostSquare(tetrisBody).x < 0) return true;
// right edge
if (getRightMostSquare(tetrisBody).x == game.boardSizeX) return true;
// fallenBlocks
for (let i = 0; i<tetrisBody.length;i++){
let tetrisBlock = tetrisBody[i];
for(let j = 0; j<fallenBlocks.length;j++){
let thisFallenBlock = fallenBlocks[j];
for (let k=0;k<thisFallenBlock.shape.body.length;k++){
let fallenBlockPoint = thisFallenBlock.shape.body[k];
if (tetrisBlock.x == fallenBlockPoint.x && tetrisBlock.y == fallenBlockPoint.y) return true;
}
}
}
return false;
}
TetrisGame.prototype.rotateIfCan = function(){
if (!this.fallingBlock.shape.canRotate) return;
let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
rawBlock.rotate();
if (!haveCollision(this,rawBlock.shape.body)) this.fallingBlock.rotate();
}
TetrisGame.prototype.moveLeftIfCan = function(){
let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
rawBlock.moveLeft();
if (!haveCollision(this,rawBlock.shape.body)) this.fallingBlock.moveLeft();
}
TetrisGame.prototype.generateNewBlockOrGameOver = function(){
this.fallingBlock = new TetrisBlock(possibleShapes[getRandomInt(0,possibleShapes.length-1)]);
if (haveCollision(this,this.fallingBlock.shape.body)) {
this.ended = true;
} else {
this.score++;
this.generateBoard();
let lineCount = 0;
for (let i=0; i<this.board.length;i++){
let boardLine = this.board[i];
let pointCountPerLine = 0;
for (let j=0;j<boardLine.length;j++) {
let blockPoint = boardLine[j];
if (blockPoint !== 0 && typeof blockPoint === "string") pointCountPerLine++;
}
if (pointCountPerLine === boardLine.length) {
lineCount++;
for (let k=0;k<this.blocksFallen.length;k++){
let thisFallenBlock = this.blocksFallen[k];
for (let p = 0; p<thisFallenBlock.shape.body.length;p++){
let thisFallenBlockPoint = thisFallenBlock.shape.body[p];
if(thisFallenBlockPoint.y === i) {
thisFallenBlock.shape.body.splice(p,1,{dummy:true});
}
}
}
for (let k=0;k<this.blocksFallen.length;k++){
let thisFallenBlock = this.blocksFallen[k];
for (let p = 0; p<thisFallenBlock.shape.body.length;p++){
let thisFallenBlockPoint = thisFallenBlock.shape.body[p];
if(thisFallenBlockPoint.y < i) {
thisFallenBlockPoint.y++;
}
}
}
}
}
if (lineCount) this.score += lineCount * 10;
if (lineCount == 4) this.score += 40;
this.generateBoard();
}
}
TetrisGame.prototype.teleportDown = function(){
let movedDown = this.moveDownOrNewBlock();
while (movedDown) {
movedDown = this.moveDownOrNewBlock();
}
}
TetrisGame.prototype.moveDownOrNewBlock = function(){
let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
rawBlock.moveDown();
if (outOfBoundsDownOrIsBlock(this,rawBlock.shape.body)) {
rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
this.blocksFallen.push(rawBlock);
return this.generateNewBlockOrGameOver();
}
if (!haveCollision(this,rawBlock.shape.body)) {
this.fallingBlock.moveDown();
return true;
}
}
TetrisGame.prototype.moveRightIfCan = function(){
let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
rawBlock.moveRight();
if (!haveCollision(this,rawBlock.shape.body)) this.fallingBlock.moveRight();
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
let genericDiv = function(color){
let returnDiv = document.createElement("div");
returnDiv.style.height = "10px";
returnDiv.style.width = "10px";
returnDiv.style.background = color;
return returnDiv;
}
let emptyDiv = function(){
return genericDiv("black");
}
function updateDOM(game) {
var el = document.getElementById("gameboard");
el.innerHTML = "";
el.style.position = "relative";
var scoreEl = document.getElementById("score");
scoreEl.innerText = game.score;
for (let i =0;i<game.board.length;i++){
let rowDiv = document.createElement("div");
//snakeRowDiv.style.position = "absolute";
for (let j=0;j<game.board[i].length;j++){
if (game.board[i][j]){
whichDiv = genericDiv(game.board[i][j]);
} else {
whichDiv = emptyDiv();
}
whichDiv.style.position = "absolute";
whichDiv.style.left = j * (parseInt(whichDiv.style.width)) + "px";
whichDiv.style.top = (i * (parseInt(whichDiv.style.height)) + 100) + "px";
rowDiv.appendChild(whichDiv);
}
el.appendChild(rowDiv);
}
}
function generateDomListener(game){
return function(event){
switch (event.key) {
case "ArrowUp":
game.rotateIfCan();
game.generateBoard();
updateDOM(game);
break;
case "ArrowDown":
game.teleportDown();
game.generateBoard();
updateDOM(game);
break;
case "ArrowLeft":
game.moveLeftIfCan();
game.generateBoard();
updateDOM(game);
break;
case "ArrowRight":
game.moveRightIfCan();
game.generateBoard();
updateDOM(game);
break;
}
}
}
function refreshInterval(game){
clearInterval(gameLoopHandle);
gameLoopHandle = setInterval(gameLoop(game), game.speed);
}
function decreaseDifficulty(game){
if (game.speed <= 900) {
game.speed += 50;
}
clearInterval(gameLoopHandle);
gameLoopHandle = setInterval(gameLoop(game), game.speed);
}
function restart(game){
game.ended = false;
game.genApple = true;
game.score = 0;
game.speed = 500;
game.apple = {x:null,y:null}
game.snake.body = [
{x:9,y:8},
{x:9,y:9},
{x:9,y:10},
{x:9,y:11},
]
game.snake.going = "RIGHT";
clearInterval(gameLoopHandle);
gameLoopHandle = setInterval(gameLoop(game), game.speed);
}
function increaseDifficulty(game){
if (game.speed >= 100) {
game.speed -= 50;
}
clearInterval(gameLoopHandle);
gameLoopHandle = setInterval(gameLoop(game), game.speed);
}
function gameLoop(game){
return function(){
if (!game.ended) {
game.moveDownOrNewBlock();
if (!game.ended) {
game.generateBoard();
updateDOM(game);
}
} else {
clearInterval(gameLoopHandle);
alert ("GAME OVER");
}
}
}
document.addEventListener("DOMContentLoaded", function(event) {
var game = new TetrisGame();
_game =game;
game.init();
game.generateBoard()
updateDOM(game);
document.addEventListener("keydown", generateDomListener(game));
gameLoopHandle = setInterval(gameLoop(game), game.speed);
})
<div id="gameboard"></div>
<div>
<h1>Score: <span id="score">0</span></h1>
<button onclick="increaseDifficulty(_game)">Increase difficulty</button>
<button onclick="decreaseDifficulty(_game)">Decrease difficulty</button>
</div>