1 Star 0 Fork 0

binave/divtetris

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
base.js 22.75 KB
一键复制 编辑 原始数据 按行查看 历史
binave 提交于 2024-02-07 22:59 +08:00 . fix project name
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
/*
* Copyright (c) 2024 bin jin.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @param max {number}
* @returns {number}
*/
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
/**
* 多个洗牌算法相连,且相邻两个数字不重复
* @param {number} size
* @returns {Generator<number, void, unknown>}
*/
function* Shuffle(size) {
let head;
while (true) {
let poker = Array.from({ length: size }, (v, i) => i * 1);
for (let i = size - 1; i >= 0; i--) {
let ri;
do {
ri = Math.floor(Math.random() * (i + 1));
} while (i == size - 1 && head == poker[ri])
yield poker[ri];
poker[ri] = poker[i];
if (i == 0) { head = poker[0]; }
}
}
}
class Block {
/** @type {number} */ x;
/** @type {number} */ y;
/** @type {number} */ style;
/**
* @param x {number}
* @param y {number}
* @param style {number}
*/
constructor(x, y, style) {
this.init(x, y, style);
}
/**
* @param x {number}
* @param y {number}
* @param style {number}
* @returns {Block}
*/
init(x, y, style) {
this.y = y;
this.x = x;
this.style = style;
}
toString() {
return `{x:${this.x}, y:${this.y}, s:${this.style}}`
}
}
/**
* 每个形状
*/
class Tetromino {
/** @type {Array<Block>} */ blocks;
/** @type {Array<boolean>} [up, right, down, lefft] */ banMoves;
constructor() {
this.blocks = Array.from({ length: 4 }, () => new Block()); // 初始化4个block
}
static #LTZO = [
[0, 0, 1, 0], // L
[0, 0, 2, 0], // T
[1, 0, 2, 0], // L
[0, 0, 0, 1], // O
[0, 0, 2, 1], // Z
[2, 0, 0, 1] // Z
];
/**
* 随机 6 种形状和若干颜色。并指定生成 I 型。
*
* 除了 I 型以外,其他六种形状都会占用三行两列。
*
* 将“用 2 个点将 3x2 矩阵分割成两半”的情况排除掉,剩下的排列就可以组成 L T Z O 四种方块之一。
* 其中 O 概率上会多一倍,强制降低即可。
*
* O O . O . O . O O O . . O . . . . O . . O + + + . X . . X . . X .
* . . . . . . . . . O . . . . O O . . . . O + . + X . . . X . . . X
*
* . . . . . . . . . O . . . . O O . . . . O + . + X . . . X . . . X
* O O . O . O . O O O . . O . . . . O . . O + + + . X . . X . . X .
*
* @param {number} style
* @param {boolean} isLine
*/
init(style, isLine) {
this.banMoves = [false, true, false, true]; // up right down lefft
if (isLine) {
for (let x = 0; x < this.blocks.length; x++) {
this.blocks[x].init(x, 0, style);
}
return;
}
/** @type {Array<number>} [ 0:x1, 1:y1, 2:x2, 3:y2 ] */
let non = Tetromino.#LTZO[getRandomInt(Tetromino.#LTZO.length)];
// let non;
// do {
// non = [getRandomInt(3), getRandomInt(2), getRandomInt(3), getRandomInt(2)];
//
// } while (
// (non[0] == non[2] && non[1] == non[3]) ||
// (non[1] != non[3] && (
// Math.abs(non[0] - non[2]) == 1 ||
// (non[0] == non[2] && non[0] != 2)
// ))
// )
let i = 0;
for (let x = 0; x < 3; x++) {
for (let y = 0; y < 2; y++) {
if (!((x === non[0] && y === non[1]) || (x === non[2] && y === non[3]))) {
this.blocks[i++].init(x, y, style);
}
}
}
// let non_row = 0, non_colume = 0, times = 0;
// let chirality = getRandomInt(2) === 0 ? 0 : 2; // 决定方块朝向
//
// do {
// if (++times > 2) { // 当重复次数超过三次生成 I 型
// for (let colume = 0; colume < this.blocks.length; colume++) {
// this.blocks[colume].init(0, colume, style);
// }
// return;
// }
// non_row = getRandomInt(2), non_colume = getRandomInt(3); // 确定 row = colume = 0 以外的另一个需要排除的点。
//
// } while (
// (non_row === 1 && non_colume === 1) ||
// (non_row === 0 && non_colume === chirality)
// );
//
// let i = 0;
// for (let row = 0; row < 2; row++) { // 刨去排除的点,产生正反 Z L O T 之一
// for (let colume = 0; colume < 3; colume++) {
// if (!(row === non_row && colume === non_colume || row === 0 && colume === chirality)) {
// this.blocks[i++].init(row, colume, style);
// }
// }
// }
//
// return this; // bug: [new Tetromino().init(), new Tetromino().init()]: random undefined
}
/**
* @param x {number}
* @param y {number}
*/
moveBy(x, y) {
this.blocks.forEach(block => {
block.x += x;
block.y += y;
});
}
/**
* 旋转
* 3 x 3 的矩阵
* 顺时针旋转 90 度:交换 y 与 x 的坐标并上下翻转。
* 逆时针旋转 90 度:交换 y 与 x 的坐标并左右翻转。
* 在交换过程中需要减去与 0,0 的间距,
* 在 0,0 点交换 y 与 x 会得到与目标图样颠倒的图样,
* 然后用 2 减去 x 或 y,再将间距加回来。
* 再移动回原来的位置即可实现旋转。
*
* @param counterclockwise {boolean}
*/
rotate(counterclockwise) {
// console.log(`rotate: before ${counterclockwise ? 1 : 0} ${this.blocks}`);
let min_x = this.blocks[0].x, min_y = this.blocks[0].y;
let line_x = true, line_y = true; // 用于判断直线
for (let i = 1; i < this.blocks.length; i++) {
min_x = Math.min(this.blocks[i].x, min_x);
min_y = Math.min(this.blocks[i].y, min_y);
line_x = line_x && this.blocks[0].x == this.blocks[i].x;
line_y = line_y && this.blocks[0].y == this.blocks[i].y;
}
if (line_x || line_y) { counterclockwise = false; }
let empty_line = 0;
this.blocks.forEach(block => {
let old_block_x = block.x;
block.x = (counterclockwise ? block.y - min_y : 2 - (block.y - min_y)) + min_x;
block.y = (!counterclockwise ? old_block_x - min_x : 2 - (old_block_x - min_x)) + min_y;
empty_line += counterclockwise ? (min_y != block.y ? 1 : 0) : (min_x != block.x ? 1 : 0);
});
if (empty_line == this.blocks.length) { this.moveBy(counterclockwise ? 0 : -1, counterclockwise ? -1 : 0); }
// console.log(`rotate: after ${counterclockwise ? 1 : 0} ${this.blocks}`);
}
toString() {
return `{blocks:[${this.blocks}], banMoves:[${this.banMoves}]}`
}
}
/**
* |-- width: 4 --|
* 0,0 5,0 _
* 0,1 5,1 |
* 0,2 O 5,2 |
* 0,3 O O O 5,3 | height: 8
* 0,4 5,4 |
* 0,5 5,5 |
* 0,6 5,6 |
* 0,7 5,7 -
* 0,8 1,8 2,8 3,8 4,8 5,8
*/
export class Background {
static WIDTH = 10; static HEIGHT = 20;
static WALL = -1; static EMPTY = -3;
static #BG_WIDTH_OFFSET = 1;
/** @type {number} */ #bg_auto_drop_cycle;
#autoDropSum = 0;
/**
* [-x+, -y+, exchangeTetromino, pause/game_over] 按键按下状态每步累计的按键累加值。
*/
#key_board_code_holds = Array.from({ length: 5 }, () => 0);
/** 1: pause, 2: gameover */
#gameStatus = 0; #sP = 0; #subLineSum = 0;
#generateTetrisLine = false;
/** @type {Array<number>} */ #TetrisLineListCache = new Array();
/** @type {Generator<number, void, unknown>} 随机不重复数字 */ #loopRan;
/** @type {Array<Tetromino>} */ #dual_tetromino;
/** @type {number} */ #tetIdx = 0;
/** @type {Array<Array<number>>} */ #current_blocksStyle;
/** @type {Snapshot} */ #snapshot;
/**
* @param {number} styleCount
* @param {number} skipCycle
*/
constructor(styleCount, skipCycle) {
this.#loopRan = Shuffle(styleCount);
this.#bg_auto_drop_cycle = skipCycle;
this.#dual_tetromino = [new Tetromino(), new Tetromino()];
this.#dual_tetromino[0].init(this.#loopRan.next().value, false);
this.#dual_tetromino[1].init(this.#loopRan.next().value, false);
const blocksStyle = new Array();
for (let y = 0; y < Background.HEIGHT + Background.#BG_WIDTH_OFFSET; y++) {
blocksStyle[y] = new Array();
if (y == Background.HEIGHT + Background.#BG_WIDTH_OFFSET - 1) {
for (let x = 0; x <= Background.WIDTH + Background.#BG_WIDTH_OFFSET; x++) { blocksStyle[y][x] = Background.WALL; }
} else {
for (const x of [0, Background.WIDTH + Background.#BG_WIDTH_OFFSET]) { blocksStyle[y][x] = Background.WALL; }
for (let x = 1; x <= Background.WIDTH; x++) { blocksStyle[y][x] = Background.EMPTY; }
}
}
this.#current_blocksStyle = blocksStyle;
this.#dual_tetromino[this.#tetIdx].moveBy(5, -2);
this.#snapshot = new Snapshot();
}
init() {
this.#exchangeTetromino();
this.#autoDropSum = 0;
this.#sP = 0;
this.#subLineSum = 0;
this.#gameStatus = 0;
this.#snapshot.init();
for (let y = 0; y < Background.HEIGHT; y++) {
for (let x = 1; x <= Background.WIDTH; x++) {
this.#current_blocksStyle[y][x] = Background.EMPTY;
}
}
}
#exchangeTetromino() {
// console.log(`exchange`);
this.#dual_tetromino[this.#tetIdx].init(this.#loopRan.next().value, this.#generateTetrisLine);
this.#generateTetrisLine = false;
this.#tetIdx = 1 - this.#tetIdx;
this.#dual_tetromino[this.#tetIdx].moveBy(5, -2);
}
#borderAABB() {
const tetris = this.#dual_tetromino[this.#tetIdx], cur_style = this.#current_blocksStyle;
tetris.banMoves.fill(false);
for (const block of tetris.blocks) {
if (block.y < 0) {
tetris.banMoves[1] = true;
tetris.banMoves[3] = true;
}
tetris.banMoves = [
tetris.banMoves[0] /*|| cur_style[block.y - 1] == undefined ||
cur_style[block.y - 1][block.x] > -2 */,
tetris.banMoves[1] || cur_style[block.y] == undefined ||
cur_style[block.y][block.x + 1] > -2,
tetris.banMoves[2] || (cur_style[block.y + 1] != undefined &&
cur_style[block.y + 1][block.x] > -2), // moveBy(5, -2)
tetris.banMoves[3] || cur_style[block.y] == undefined ||
cur_style[block.y][block.x - 1] > -2
]
}
}
/**
* @returns {boolean} hit
*/
#overAABB() {
for (const block of this.#dual_tetromino[this.#tetIdx].blocks) {
if (this.#current_blocksStyle[block.y] == undefined ||
this.#current_blocksStyle[block.y][block.x] > -2) {
return true;
}
}
return false;
}
#subLine() {
let sub_sum = 0;
let y = Background.HEIGHT - Background.#BG_WIDTH_OFFSET;
for (; y >= 0; y--) {
let block_sum = 0;
for (let x = 1; x <= Background.WIDTH; x++) {
if (this.#current_blocksStyle[y][x] >= 0) { block_sum++; }
}
if (block_sum == Background.WIDTH) {
sub_sum++;
let [styleArr] = this.#current_blocksStyle.splice(y++, 1);
styleArr.fill(Background.EMPTY);
styleArr[0] = Background.WALL;
styleArr[styleArr.length - 1] = Background.WALL;
this.#current_blocksStyle.unshift(styleArr);
}
if (block_sum == 0) { break; }
}
/**
* find line space
*/
if (y <= Background.HEIGHT - 5 && !this.#generateTetrisLine) {
const bs = this.#current_blocksStyle;
for (let x = 0; x < Background.WIDTH; x++) {
for (let _y = y + 1; _y < Background.HEIGHT - 2; _y++) {
// TODO if (this.#TetrisLineListCache = new Array()[0] == ) { continue; }
if (bs[_y][x + 1] != Background.EMPTY) { break; }
if (
bs[_y + 0][x] != Background.EMPTY && bs[_y + 0][x + 1] == Background.EMPTY && bs[_y + 0][x + 2] != Background.EMPTY &&
bs[_y + 1][x] != Background.EMPTY && bs[_y + 1][x + 1] == Background.EMPTY && bs[_y + 1][x + 2] != Background.EMPTY &&
bs[_y + 2][x] != Background.EMPTY && bs[_y + 2][x + 1] == Background.EMPTY && bs[_y + 2][x + 2] != Background.EMPTY
) {
// console.log(`find: x,y:${x + 1},${_y}-${_y + 2}`);
this.#generateTetrisLine = getRandomInt(3) == 0;
break;
}
}
if (this.#generateTetrisLine) { break; }
}
}
switch (sub_sum) {
case 1: break;
case 2: this.#sP += 1; break;
case 3: this.#sP += 3; break;
case 4: this.#sP += 6; break;
default: break;
}
this.#sP = Math.min(this.#sP, 30);
this.#subLineSum += sub_sum;
}
#hitBottom() {
const tetris = this.#dual_tetromino[this.#tetIdx];
// console.log(`save to background: ${tetris.blocks}`);
tetris.blocks.forEach(block => {
if (block.y >= 0) {
this.#current_blocksStyle[block.y][block.x] = block.style;
} else { this.#gameStatus = 2; console.log(`game over, ${this.#subLineSum}`); return; }
});
if (this.#gameStatus != 2) {
this.#subLine();
this.#exchangeTetromino();
tetris.banMoves[1] = true;
tetris.banMoves[3] = true;
}
}
/**
* @param {Array<number>} key_board_codes [-x+, -y+, exchangeTetromino, pause/game_over]
* @returns {Array<Array<number>>} [oldBg, newBg, exBg, ready, [gameStatus, sp, subLineSum]]
*/
run1Step(key_board_codes) {
for (const i in key_board_codes) {
key_board_codes[i] == 0 ? this.#key_board_code_holds[i] = 0 : this.#key_board_code_holds[i] += key_board_codes[i];
}
if (this.#key_board_code_holds[3] == 1) {
if (this.#gameStatus == 0) {
this.#gameStatus = 1;
return [[], [], [], [], [1, this.#sP, this.#subLineSum]];
} else if (this.#gameStatus == 1) {
this.#gameStatus = 0;
} else if (this.#gameStatus == 2) {
this.#gameStatus = 0;
this.init();
this.#dual_tetromino[this.#tetIdx].banMoves[2] = false;
return [[], [], [], [], [3, this.#sP, this.#subLineSum]];
}
}
if (this.#gameStatus != 0) {
return [[], [], [], [], [this.#gameStatus == 2 ? 2 : 1, this.#sP, this.#subLineSum]];
}
// console.log(`key_board_codes:${key_board_codes}, key_board_code_holds:${this.#key_board_code_holds}`);
const tetris = this.#dual_tetromino[this.#tetIdx];
if (this.#key_board_code_holds[0] > 0 && this.#key_board_code_holds[0] != 2 && this.#key_board_code_holds[0] != 3) {
this.#borderAABB();
if (!tetris.banMoves[1]) { tetris.moveBy(1, 0); }
} else if (this.#key_board_code_holds[0] < 0 && this.#key_board_code_holds[0] != -2 && this.#key_board_code_holds[0] != -3) {
this.#borderAABB();
if (!tetris.banMoves[3]) { tetris.moveBy(-1, 0); }
}
if (this.#key_board_code_holds[2] == 1) {
if (this.#sP > 0) {
this.#sP --;
this.#exchangeTetromino();
}
}
if (this.#key_board_code_holds[1] > 0) {
if (this.#key_board_code_holds[1] == 1) {
tetris.rotate()
if (this.#overAABB()) {
// tetris.rotate(); tetris.rotate(); tetris.rotate();
tetris.rotate(true);
}
}
} else if (this.#key_board_code_holds[1] < 0) {
for (let y = 0; y < this.#key_board_code_holds[1] * -1 / 2; y++) {
this.#borderAABB();
if (tetris.banMoves[2]) { this.#key_board_code_holds[1] = 2; this.#hitBottom(); } else { tetris.moveBy(0, 1); }
}
}
if (this.#autoDropSum++ >= this.#bg_auto_drop_cycle) {
this.#autoDropSum = 0;
if (this.#key_board_code_holds[1] >= 0) {
this.#borderAABB();
if (tetris.banMoves[2]) { this.#hitBottom(); } else { tetris.moveBy(0, 1); }
}
}
return this.#BgDiff();
}
/**
* @returns {Array<Array<number>>} [oldBg, newBg, exBg, ready, [gameStatus, sp, subLineSum]]
*/
#BgDiff() {
const oldBg = new Array(0), newBg = new Array(0), exBg = new Array(0), oldMap = new Map();
// tetromino diff
const old_tetris = this.#snapshot.log_dual_tetris[0], cur_tetris = this.#dual_tetromino[this.#tetIdx].blocks;
for (let i = 0; i < cur_tetris.length; i++) {
// auto down
if (old_tetris[i].x != cur_tetris[i].x - 1 || old_tetris[i].y != cur_tetris[i].y || old_tetris[i].style != cur_tetris[i].style) {
if (old_tetris[i].x == undefined) {
newBg.push(cur_tetris[i].x - 1, cur_tetris[i].y, cur_tetris[i].style);
} else if (old_tetris[i].x == cur_tetris[i].x - 1 && old_tetris[i].y == cur_tetris[i].y) {
exBg.push(cur_tetris[i].x - 1, cur_tetris[i].y, cur_tetris[i].style);
} else {
oldBg.push(old_tetris[i].x, old_tetris[i].y, old_tetris[i].style);
oldMap.set(`${old_tetris[i].x},${old_tetris[i].y}`, 1);
newBg.push(cur_tetris[i].x - 1, cur_tetris[i].y, cur_tetris[i].style);
}
old_tetris[i].init(cur_tetris[i].x - 1, cur_tetris[i].y, cur_tetris[i].style);
}
}
// background diff
const log_blocksStyle = this.#snapshot.log_blocksStyle, cur_blocksStyle = this.#current_blocksStyle;
for (let y = log_blocksStyle.length - 1; y >= 0; y--) {
let blocksStyleSum = 0;
for (let x = 0; x < log_blocksStyle[y].length; x++) {
if (log_blocksStyle[y][x] >= 0 || cur_blocksStyle[y][x + 1] >= 0) { blocksStyleSum++; }
if (log_blocksStyle[y][x] != cur_blocksStyle[y][x + 1]) {
if (log_blocksStyle[y][x] != Background.EMPTY && cur_blocksStyle[y][x + 1] > 0) {
exBg.push(x, y, cur_blocksStyle[y][x + 1]);
log_blocksStyle[y][x] = cur_blocksStyle[y][x + 1];
} else if (log_blocksStyle[y][x] != Background.EMPTY) {
oldBg.push(x, y, Background.EMPTY);
oldMap.set(`${x},${y}`, 1);
log_blocksStyle[y][x] = Background.EMPTY;
} else {
newBg.push(x, y, cur_blocksStyle[y][x + 1]);
log_blocksStyle[y][x] = cur_blocksStyle[y][x + 1];
}
}
}
if (blocksStyleSum == 0) { break; }
}
// trim block
let ni = 0;
while (newBg[ni] != undefined) {
if (oldMap.has(`${newBg[ni]},${newBg[ni + 1]}`)) {
oldMap.delete(`${newBg[ni]},${newBg[ni + 1]}`);
let oi = 0;
for (; oi < oldBg.length; oi += 3) { if (oldBg[oi] == newBg[ni] && oldBg[oi + 1] == newBg[ni + 1]) { break; } }
if (oldBg[oi + 2] != newBg[ni + 2]) { exBg.push(newBg[ni], newBg[ni + 1], newBg[ni + 2]); }
oldBg.splice(oi, 3);
newBg.splice(ni, 3);
}
ni += 3;
}
// ready tetromino
const ready = new Array(0), old_ready_tetris = this.#snapshot.log_dual_tetris[1], cur_ready_tetris = this.#dual_tetromino[1 - this.#tetIdx].blocks;
for (let i = 0; i < cur_ready_tetris.length; i++) {
if (old_ready_tetris[i].x != cur_ready_tetris[i].x || old_ready_tetris[i].y != cur_ready_tetris[i].y || old_ready_tetris[i].style != cur_ready_tetris[i].style) {
ready.push(cur_ready_tetris[i].x, cur_ready_tetris[i].y, cur_ready_tetris[i].style);
old_ready_tetris[i].init(cur_ready_tetris[i].x, cur_ready_tetris[i].y, cur_ready_tetris[i].style);
}
}
return [oldBg, newBg, exBg, ready, [0, this.#sP, this.#subLineSum]];
}
}
class Snapshot {
/** @type {Array<Array<Block>>} */ log_dual_tetris;
/** @type {Array<Array<number>>} */ log_blocksStyle;
constructor() {
this.log_blocksStyle = Array.from(
{ length: Background.HEIGHT },
() => new Array(Background.WIDTH).fill(Background.EMPTY)
);
// this.log_dual_tetris = Array.from({ length: 2 }, () => new Array(4).fill(new Block())); // bug: Array().fill(): same obj
this.log_dual_tetris = Array.from({ length: 2 }, () => Array.from({ length: 4 }, () => new Block()));
}
init() {
for (const cs of this.log_blocksStyle) { cs.fill(Background.EMPTY); }
for (const tetris of this.log_dual_tetris) {
for (const block of tetris) {
block.init(undefined, undefined, undefined);
}
}
}
}
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
JavaScript
1
https://gitee.com/binave/divtetris.git
git@gitee.com:binave/divtetris.git
binave
divtetris
divtetris
develop

搜索帮助