@haelp/teto
Version:
A typescript-based controllable TETR.IO client.
1,215 lines (1,214 loc) • 49.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "Engine", {
enumerable: true,
get: function() {
return Engine;
}
});
const _events = require("../utils/events");
const _board = _export_star(require("./board"), exports);
const _constants = require("./constants");
const _garbage = _export_star(require("./garbage"), exports);
const _multiplayer = _export_star(require("./multiplayer"), exports);
const _queue = _export_star(require("./queue"), exports);
const _utils = _export_star(require("./utils"), exports);
const _damageCalc = require("./utils/damageCalc");
const _kicks = require("./utils/kicks");
const _data = require("./utils/kicks/data");
const _tetromino = require("./utils/tetromino");
const _chalk = /*#__PURE__*/ _interop_require_default(require("chalk"));
_export_star(require("./types"), exports);
function _export_star(from, to) {
Object.keys(from).forEach(function(k) {
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
Object.defineProperty(to, k, {
enumerable: true,
get: function() {
return from[k];
}
});
}
});
return from;
}
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
class Engine {
queue;
held;
holdLocked;
falling;
_kickTable;
board;
connectedBoard;
lastSpin;
lastWasClear;
stats;
gameOptions;
garbageQueue;
frame;
subframe;
initializer;
handling;
input;
pc;
b2b;
dynamic;
glock;
multiplayer;
igeHandler;
misc;
state;
currentSpike;
events = new _events.EventEmitter();
resCache;
constructor(options){
this.initializer = (0, _utils.deepCopy)(options, [
{
type: Date,
copy: (d)=>new Date(d)
}
]);
this.init();
}
init() {
const options = this.initializer;
this.queue = new _queue.Queue(options.queue);
this.queue.onRepopulate(this.#onQueueRepopulate.bind(this));
this._kickTable = options.kickTable;
this.board = new _board.Board(options.board);
this.connectedBoard = new _board.ConnectedBoard(options.board);
this.garbageQueue = new ((options.misc.date ?? new Date()) > new Date("2025-05-06T15:00:00-04:00") ? _garbage.GarbageQueue : _garbage.LegacyGarbageQueue)(options.garbage);
this.igeHandler = new _multiplayer.IGEHandler(options.multiplayer?.opponents || []);
if (options.multiplayer) this.multiplayer = {
options: options.multiplayer,
targets: [],
passthrough: {
network: [
"consistent",
"zero"
].includes(options.multiplayer.passthrough),
replay: options.multiplayer.passthrough !== "full",
travel: [
"zero",
"limited"
].includes(options.multiplayer.passthrough)
}
};
this.held = null;
this.holdLocked = false;
this.lastSpin = null;
this.lastWasClear = false;
this.stats = {
combo: -1,
b2b: -1,
pieces: 0,
lines: 0,
garbage: {
sent: 0,
attack: 0,
receive: 0,
cleared: 0
}
};
this.pc = options.pc;
this.b2b = {
chaining: options.b2b.chaining,
charging: options.b2b.charging
};
this.dynamic = {
gravity: new _utils.IncreaseTracker(options.gravity.value, options.gravity.increase, options.gravity.marginTime),
garbageMultiplier: new _utils.IncreaseTracker(options.garbage.multiplier.value, options.garbage.multiplier.increase, options.garbage.multiplier.marginTime),
garbageCap: new _utils.IncreaseTracker(options.garbage.cap.value, options.garbage.cap.increase, options.garbage.cap.marginTime)
};
this.glock = 0;
this.misc = options.misc;
this.gameOptions = options.options;
this.handling = options.handling;
this.input = {
lShift: {
held: false,
arr: 0,
das: 0,
dir: -1
},
rShift: {
held: false,
arr: 0,
das: 0,
dir: 1
},
lastShift: -1,
firstInputTime: -1,
time: {
start: 0,
zero: true,
locked: false,
prev: 0,
frameoffset: 0
},
lastPieceTime: 0,
keys: {
softDrop: false,
hold: false,
rotateCW: false,
rotateCCW: false,
rotate180: false
}
};
this.frame = 0;
this.subframe = 0;
this.state = 0;
this.currentSpike = 0;
this.flushRes();
this.nextPiece();
this.bindAll();
}
flushRes() {
const res = this.resCache ? (0, _utils.deepCopy)(this.resCache) : null;
this.resCache = {
pieces: 0,
garbage: {
sent: [],
received: []
},
keys: [],
lastLock: res?.lastLock ?? 0
};
return res;
}
reset() {
this.init();
}
bindAll() {
this.moveRight = this.moveRight.bind(this);
this.moveLeft = this.moveLeft.bind(this);
this.dasRight = this.dasRight.bind(this);
this.dasLeft = this.dasLeft.bind(this);
this.softDrop = this.softDrop.bind(this);
this.hardDrop = this.hardDrop.bind(this);
this.hold = this.hold.bind(this);
this.rotateCW = this.rotateCW.bind(this);
this.rotateCCW = this.rotateCCW.bind(this);
this.rotate180 = this.rotate180.bind(this);
this.snapshot = this.snapshot.bind(this);
this.fromSnapshot = this.fromSnapshot.bind(this);
this.nextPiece = this.nextPiece.bind(this);
}
#onQueueRepopulate(pieces) {
this.events.emit("queue.add", pieces);
}
snapshot() {
return {
board: (0, _utils.deepCopy)(this.board.state),
connectedBoard: (0, _utils.deepCopy)(this.connectedBoard.state),
falling: this.falling.snapshot(),
frame: this.frame,
garbage: this.garbageQueue.snapshot(),
hold: this.held,
holdLocked: this.holdLocked,
lastSpin: (0, _utils.deepCopy)(this.lastSpin),
lastWasClear: this.lastWasClear,
queue: this.queue.index,
input: (0, _utils.deepCopy)(this.input),
subframe: this.subframe,
targets: this.multiplayer?.targets,
stats: (0, _utils.deepCopy)(this.stats),
glock: this.glock,
ige: this.igeHandler.snapshot(),
state: this.state,
currentSpike: this.currentSpike,
resCache: (0, _utils.deepCopy)(this.resCache)
};
}
fromSnapshot(snapshot) {
this.board.state = (0, _utils.deepCopy)(snapshot.board);
this.connectedBoard.state = (0, _utils.deepCopy)(snapshot.connectedBoard);
this.falling = new _tetromino.Tetromino({
boardHeight: this.board.height,
boardWidth: this.board.width,
initialRotation: this.kickTable.spawn_rotation[snapshot.falling.symbol.toLowerCase()] ?? 0,
symbol: snapshot.falling.symbol,
from: snapshot.falling
});
for (const key of Object.keys(snapshot.falling)){
// @ts-expect-error
this.falling[key] = (0, _utils.deepCopy)(snapshot.falling[key]);
}
this.frame = snapshot.frame;
this.subframe = snapshot.subframe;
this.garbageQueue.fromSnapshot(snapshot.garbage);
this.held = snapshot.hold;
this.holdLocked = snapshot.holdLocked;
this.lastSpin = (0, _utils.deepCopy)(snapshot.lastSpin);
this.lastWasClear = snapshot.lastWasClear;
this.queue = new _queue.Queue(this.initializer.queue);
for(let i = 0; i < snapshot.queue; i++)this.queue.shift();
this.queue.onRepopulate(this.#onQueueRepopulate.bind(this));
this.dynamic = {
gravity: new _utils.IncreaseTracker(this.initializer.gravity.value, this.initializer.gravity.increase, this.initializer.gravity.marginTime),
garbageMultiplier: new _utils.IncreaseTracker(this.initializer.garbage.multiplier.value, this.initializer.garbage.multiplier.increase, this.initializer.garbage.multiplier.marginTime),
garbageCap: new _utils.IncreaseTracker(this.initializer.garbage.cap.value, this.initializer.garbage.cap.increase, this.initializer.garbage.cap.marginTime)
};
for(let i = 0; i < this.frame; i++)for (const key of Object.keys(this.dynamic))this.dynamic[key].tick();
this.input = (0, _utils.deepCopy)(snapshot.input);
if (this.multiplayer && snapshot.targets) this.multiplayer.targets = [
...snapshot.targets
];
this.stats = (0, _utils.deepCopy)(snapshot.stats);
this.glock = snapshot.glock;
this.state = snapshot.state;
this.currentSpike = snapshot.currentSpike;
this.igeHandler.fromSnapshot(snapshot.ige);
this.resCache = (0, _utils.deepCopy)(snapshot.resCache);
}
get kickTable() {
return _data.kicks[this._kickTable];
}
get kickTableName() {
return this._kickTable;
}
set kickTable(value) {
this._kickTable = value;
}
get dynamicStats() {
return {
apm: this.stats.garbage.attack / (this.frame / 60 / 60) || 0,
pps: this.stats.pieces / (this.frame / 60) || 0,
vs: (this.stats.garbage.attack + this.stats.garbage.cleared) / (this.frame / 60) * 100 || 0,
surgePower: this.b2b.charging ? Math.floor(this.stats.b2b - this.b2b.charging.at + this.b2b.charging.base + 1) : 0
};
}
#getEffectiveGravity() {
return this.glock <= 0 ? this.dynamic.gravity.get() : this.glock <= 180 ? (1 - this.glock / 180) ** 2 * this.dynamic.gravity.get() : 0;
}
#hasHitWall() {
return !!(this.state & _constants.constants.flags.STATE_WALL);
}
// @ts-expect-error unused
#hasRotated() {
return !!(this.state & (_constants.constants.flags.ROTATION_LEFT | _constants.constants.flags.ROTATION_RIGHT));
}
// @ts-expect-error unused
#hasRotated180() {
return !!(this.state & _constants.constants.flags.ROTATION_180);
}
// @ts-expect-error unused
#isSpin() {
return !!(this.state & _constants.constants.flags.ROTATION_SPIN);
}
// @ts-expect-error unused
#isSpinMini() {
return !(~this.state & (_constants.constants.flags.ROTATION_SPIN | _constants.constants.flags.ROTATION_MINI));
}
// @ts-expect-error unused
#isSpinAll() {
return !!(this.state & _constants.constants.flags.ROTATION_SPIN_ALL);
}
#isSleep() {
return !!(this.state & _constants.constants.flags.STATE_SLEEP);
}
// @ts-expect-error unused
#isFloored() {
return !!(this.state & _constants.constants.flags.STATE_FLOOR);
}
// @ts-expect-error unused
#isVisible() {
return !(this.state & _constants.constants.flags.STATE_NODRAW);
}
// @ts-expect-error unused
#isSoftDropped() {
return !!(this.state & _constants.constants.flags.ACTION_SOFTDROP);
}
#isForcedToLock() {
return !!(this.state & _constants.constants.flags.ACTION_FORCELOCK);
}
#is20G() {
const is20G = this.dynamic.gravity.get() > this.board.height;
const mode20G = this.misc.movement.may20G;
if (this.input.keys.softDrop) {
const preferSoftDrop = this.handling.may20g || is20G && mode20G;
return (this.handling.sdf === 41 || this.dynamic.gravity.get() * this.handling.sdf > this.board.height) && preferSoftDrop;
}
return is20G && mode20G;
}
#shouldLock() {
return !this.misc.movement.infinite && this.falling.lockResets >= this.misc.movement.lockResets;
}
#shouldFallFaster() {
return this.misc.movement.infinite ? false : this.falling.rotResets > this.misc.movement.lockResets + 15;
}
#clearFlags(e) {
this.state &= ~e;
}
#__internal_lock(subframe = 1 - this.subframe) {
this.falling.locking += subframe;
return this.falling.locking > this.misc.movement.lockTime || !!this.#isForcedToLock() || this.#shouldLock();
}
#__internal_fall(value) {
let y1 = Math.round(1e6 * (this.falling.location[1] - value)) / 1e6, y2 = this.falling.location[1] - 1;
if (y1 % 1 === 0) y1 -= 1e-6;
if (y2 % 1 === 0) y2 += 2e-6;
if (!(0, _kicks.legal)(this.falling.absoluteAt({
y: y1
}), this.board.state) || !(0, _kicks.legal)(this.falling.absoluteAt({
y: y2
}), this.board.state)) return false;
const { highestY } = this.falling;
if (highestY > y1) this.falling.highestY = Math.floor(y1);
this.falling.location[1] = y1;
if (this.gameOptions.spinBonuses !== "stupid") this.lastSpin = null;
this.state &= ~_constants.constants.flags.STATE_FLOOR;
if (y1 < highestY || this.misc.movement.infinite) {
this.falling.lockResets = 0;
this.falling.rotResets = 0;
}
return true;
}
// @ts-expect-error unused
#__internal_lockout() {
// TODO: implement
// if (this.options.nolockout) return;
// if (!e || false === this.options.clutch)
// return this.self.stm.LoseStockOrGameOver(
// this.options.topoutisclear ? "topout_clear" : "topout",
// );
}
#__internal_dcd() {
if (!this.#hasHitWall() || !this.handling.dcd) return;
this.input.lShift.das = Math.min(this.input.lShift.das, this.handling.das - this.handling.dcd);
this.input.lShift.arr = this.handling.arr;
this.input.rShift.das = Math.min(this.input.rShift.das, this.handling.das - this.handling.dcd);
this.input.rShift.arr = this.handling.arr;
}
#fall(subframe = 1 - this.subframe) {
if (this.falling.safeLock > 0) this.falling.safeLock--;
// TODO: if pause, return: if ((this.piece.safelock > 0 && this.piece.safelock--, this.S.pause)) return;
if (this.#isSleep()) return;
let fall = this.#getEffectiveGravity() * subframe;
if (this.glock > 0) this.glock -= subframe;
if (this.glock < 0) this.glock = 0;
if (this.input.keys.softDrop) {
if (this.handling.sdf === 41) fall = 400 * subframe;
else {
fall *= this.handling.sdf;
fall = Math.max(fall, 0.05 * this.handling.sdf);
}
}
if (this.#shouldLock() && !(0, _kicks.legal)(this.falling.absoluteAt({
y: this.falling.location[1] - 1
}), this.board.state)) {
fall = 20;
this.state |= _constants.constants.flags.ACTION_FORCELOCK;
}
if (this.#shouldFallFaster()) {
fall += 0.5 * subframe * (this.falling.rotResets - (this.misc.movement.lockResets + 15));
}
for(let dropFactor = fall; dropFactor > 0; dropFactor -= Math.min(1, dropFactor)){
const y = this.falling.location[1];
if (!this.#__internal_fall(Math.min(1, dropFactor))) {
if (this.#__internal_lock(subframe)) {
if (this.handling.safelock) this.falling.safeLock = 7;
this.#lock(false);
}
return;
}
if (Math.floor(y) !== Math.floor(this.falling.location[1])) {
this.state &= ~_constants.constants.flags.ROTATION_ALL;
}
}
}
#clampRotation(amount) {
return ((this.falling.rotation + amount) % 4 + 4) % 4;
}
#__internal_rotate(newX, newY, newRotation, rotationDirection, kick, _isIRS = false) {
// Handle rotation flags based on direction
if (rotationDirection >= 2) {
// 180 degree rotation
rotationDirection = newRotation > this.falling.rotation ? 1 : -1;
this.state |= _constants.constants.flags.ROTATION_180;
}
// Update this.falling properties
this.falling.x = newX;
this.falling.y = newY;
this.falling.rotation = newRotation;
// Set rotation direction flags
this.state |= rotationDirection === 1 ? _constants.constants.flags.ROTATION_RIGHT : _constants.constants.flags.ROTATION_LEFT;
this.state &= ~(_constants.constants.flags.ROTATION_SPIN | _constants.constants.flags.ROTATION_MINI | _constants.constants.flags.ROTATION_SPIN_ALL);
// Update lock and rotation resets with caps
if (this.falling.lockResets < 31) this.falling.lockResets++;
if (this.falling.rotResets < 63) this.falling.rotResets++;
// Check for T-Spin
const spin = this.#detectSpin(this.#isTSpinKick(kick));
this.lastSpin = {
piece: this.falling.symbol,
type: spin
};
if (spin) {
this.state |= _constants.constants.flags.ROTATION_SPIN;
if (spin === "mini") {
this.state |= _constants.constants.flags.ROTATION_MINI;
}
}
this.falling.totalRotations++;
// Handle post-rotation actions
this.#__internal_dcd();
// Reset locking if not going to lock
if (!this.#shouldLock()) {
this.falling.locking = 0;
}
return true;
}
#kick(to) {
const kick = (0, _kicks.performKick)(this.kickTableName, this.falling.symbol, this.falling.location, [
this.falling.aox,
this.falling.aoy
], !this.misc.movement.infinite && this.falling.totalRotations > this.misc.movement.lockResets + 15, this.falling.states[to], this.falling.rotation, to, this.board.state);
if (typeof kick === "object") return kick;
else return kick === false ? false : {
kick: [
0,
0
],
newLocation: [
this.falling.location[0],
this.falling.location[1]
],
id: "00",
index: 0
};
}
#rotate(amount, isIRS = false) {
if (this.#isSleep()) {
if (this.handling.irs === "tap") this.falling.irs = ((this.falling.irs + amount) % 4 + 4) % 4;
return false;
}
const to = this.#clampRotation(amount);
this.state |= _constants.constants.flags.ACTION_MOVE;
this.state |= _constants.constants.flags.ACTION_ROTATE;
const kick = this.#kick(to);
return kick ? this.#__internal_rotate(kick.newLocation[0], kick.newLocation[1], to, amount, kick, isIRS) : false;
}
nextPiece(ignoreBlockout = false, isHold = false) {
const newTetromino = this.queue.shift();
this.initiatePiece(newTetromino, ignoreBlockout, isHold);
}
initiatePiece(piece, ignoreBlockout = false, isHold = false) {
if (this.handling.irs === "hold" && this.falling) {
let rotationState = 0;
// Calculate rotation based on input
if (this.input.keys.rotateCCW) rotationState -= 1;
if (this.input.keys.rotateCW) rotationState += 1;
if (this.input.keys.rotate180) rotationState += 2;
// Ensure rotation state is in 0-3 range
this.falling.irs = (rotationState % 4 + 4) % 4;
}
if (this.handling.ihs === "hold" && this.input.keys.hold && !isHold) {
this.state |= _constants.constants.flags.ACTION_IHS;
}
this.#__internal_dcd();
this.state &= ~(_constants.constants.flags.ROTATION_ALL | _constants.constants.flags.STATE_ALL | _constants.constants.flags.ACTION_FORCELOCK | _constants.constants.flags.ACTION_SOFTDROP | _constants.constants.flags.ACTION_MOVE | _constants.constants.flags.ACTION_ROTATE);
this.input.firstInputTime = -1;
if (!isHold) this.holdLocked = false;
this.falling = new _tetromino.Tetromino({
boardHeight: this.board.height,
boardWidth: this.board.width,
initialRotation: this.kickTable.spawn_rotation[piece.toLowerCase()] ?? 0,
symbol: piece,
from: this.falling
});
if (!ignoreBlockout && this.#considerBlockout(isHold)) {
// Lose Stock or Game Over
this.state ^= _constants.constants.flags.ACTION_IHS;
this.falling.irs = 0;
} else {
if (this.state & _constants.constants.flags.ACTION_IHS) {
this.state ^= _constants.constants.flags.ACTION_IHS;
this.hold(true, ignoreBlockout);
} else {
if (this.falling.irs !== 0) {
this.#rotate(this.falling.irs, true);
this.falling.irs = 0;
}
if (this.#considerBlockout(!ignoreBlockout || isHold)) {
// lose stock or game over
} else {
if (this.#is20G()) {
this.#slamToFloor();
}
}
}
}
}
#considerBlockout(isSilent = false) {
if ((0, _kicks.legal)(this.falling.absoluteBlocks, this.board.state)) {
// TODO: clutch count
// if (!isSilent) {
// this.S.clutchCount = 0;
// }
return false;
}
let clutched = false;
if (this.lastWasClear && this.gameOptions.clutch !== false) {
const originalY = this.falling.location[1];
const originalHy = this.falling.highestY;
while(this.falling.y < this.board.fullHeight){
this.falling.location[1]++;
this.falling.highestY++;
if ((0, _kicks.legal)(this.falling.absoluteBlocks, this.board.state)) {
clutched = true;
break;
}
}
if (!clutched) {
this.falling.location[1] = originalY;
this.falling.highestY = originalHy;
}
}
if (!clutched) return true;
if (!isSilent) {
// TODO: update clutch count +1
}
return false;
}
hold(_ihs = false, ignoreBlockout = false) {
if (this.#isSleep()) {
if (this.handling.ihs === "tap") this.state |= _constants.constants.flags.ACTION_IHS;
return false;
}
if (this.holdLocked || !this.misc.allowed.hold) return false;
this.holdLocked = !this.misc.infiniteHold;
if (this.held) {
const save = this.held;
this.held = this.falling.symbol;
this.initiatePiece(save, ignoreBlockout, true);
} else {
this.held = this.falling.symbol;
this.nextPiece(ignoreBlockout, true);
}
this.holdLocked = !this.misc.infiniteHold;
return true;
}
#slamToFloor() {
while(this.#__internal_fall(1)){}
}
get toppedOut() {
try {
for (const block of this.falling.blocks){
if (this.board.state[-block[1] + this.falling.y][block[0] + this.falling.location[0]] !== null) return true;
}
return false;
} catch {
return true;
}
}
#isTSpinKick(kick) {
if (typeof kick === "object") {
return((kick.id === "23" || kick.id === "03") && kick.kick[0] === 1 && kick.kick[1] === -2 || // fin ccw and tst cw
(kick.id === "21" || kick.id === "01") && kick.kick[0] === -1 && kick.kick[1] === -2);
}
return false;
}
rotateCW() {
return this.#processRotate(1);
}
rotateCCW() {
return this.#processRotate(-1);
}
rotate180() {
return this.#processRotate(2);
}
moveRight() {
const res = this.falling.moveRight(this.board.state);
if (res && this.gameOptions.spinBonuses !== "stupid") this.lastSpin = null;
return res;
}
moveLeft() {
const res = this.falling.moveLeft(this.board.state);
if (res && this.gameOptions.spinBonuses !== "stupid") this.lastSpin = null;
return res;
}
dasRight() {
const res = this.falling.dasRight(this.board.state);
if (res && this.gameOptions.spinBonuses !== "stupid") this.lastSpin = null;
return res;
}
dasLeft() {
const res = this.falling.dasLeft(this.board.state);
if (res && this.gameOptions.spinBonuses !== "stupid") this.lastSpin = null;
return res;
}
softDrop() {
const res = this.falling.softDrop(this.board.state);
if (res && this.gameOptions.spinBonuses !== "stupid") this.lastSpin = null;
return res;
}
#maxSpin(...spins) {
const score = (spin)=>[
"none",
"mini",
"normal"
].indexOf(spin);
return spins.reduce((a, b)=>{
return score(a) > score(b) ? a : b;
});
}
#detectSpin(finOrTst) {
if (this.gameOptions.spinBonuses === "none") return "none";
const tSpin = [
"all",
"all-mini",
"all-mini+",
"all+",
"T-spins",
"T-spins+"
].includes(this.gameOptions.spinBonuses) && this.falling.symbol === "t" ? this.#detectSpinFromCorners(finOrTst) : false;
const allSpin = this.falling.isAllSpinPosition(this.board.state);
switch(this.gameOptions.spinBonuses){
case "stupid":
return this.falling.isStupidSpinPosition(this.board.state) ? "normal" : "none";
case "T-spins":
return tSpin || "none";
case "T-spins+":
return this.#maxSpin(tSpin || "none", allSpin ? this.falling.symbol === "t" ? "mini" : "none" : "none");
case "all":
return tSpin || (allSpin ? "normal" : "none");
case "all-mini":
return tSpin || (allSpin ? "mini" : "none");
case "all+":
return this.#maxSpin(tSpin || "none", allSpin ? this.falling.symbol === "t" ? "mini" : "normal" : "none");
case "all-mini+":
return this.#maxSpin(tSpin || "none", allSpin ? "mini" : "none");
case "mini-only":
return tSpin === "normal" ? "mini" : this.#maxSpin(tSpin || "none", allSpin ? "mini" : "none");
case "handheld":
return this.#detectSpinFromCorners(finOrTst);
}
}
#spinbonuses(piece) {
const p = _tetromino.tetrominoes[piece];
const rules = _data.spinbonusRules[this.gameOptions.spinBonuses];
// @ts-expect-error
return p?.spinbonus_override ? p.spinbonus_override?.mini : !!rules?.types_mini?.includes(piece);
}
#detectSpinFromCorners(finOrTst) {
if ((0, _kicks.legal)(this.falling.blocks.map((block)=>[
block[0] + this.falling.location[0],
-block[1] + this.falling.y - 1
]), this.board.state)) return "none";
let corners = 0;
let frontCorners = 0;
for(let i = 0; i < 4; i++){
const table = _data.cornerTable[this.falling.symbol]?.[this.falling.rotation];
if (!table) break;
if (this.board.occupied(this.falling.x + table[i][0] + 1, this.falling.y - table[i][1] - 1)) {
corners++;
if (this.falling.rotation === table[i][2] || this.falling.rotation === table[i][3]) {
frontCorners++;
}
}
}
if (corners < 3) return "none";
let spin = "normal";
if (this.#spinbonuses(this.falling.symbol) && frontCorners !== 2) spin = "mini";
if (finOrTst) spin = "normal";
return spin;
}
/** */ hardDrop() {
while(this.#__internal_fall(1));
return this.#lock(true);
}
#lock(hard) {
this.holdLocked = false;
// TODO: ARE (line clear, garbage)
const placed = this.falling.blocks.map((block)=>[
this.falling.symbol,
this.falling.location[0] + block[0],
this.falling.y - block[1]
]);
this.board.add(...placed);
this.connectedBoard.add(...this.connect(placed.map(([_, x, y])=>[
x,
-y
])).map(([x, y, s])=>[
{
mino: this.falling.symbol,
connection: s
},
x,
-y
]));
this.connectedBoard.clearBombsAndLines(placed.map((b)=>[
b[1],
b[2]
]));
const { lines, garbageCleared } = this.board.clearBombsAndLines(placed.map((b)=>[
b[1],
b[2]
]));
const pc = this.board.perfectClear;
this.stats.garbage.cleared += garbageCleared;
let brokeB2B = this.stats.b2b;
if (lines > 0) {
this.stats.combo++;
if ((this.lastSpin && this.lastSpin.type !== "none" || lines >= 4) && !(pc && this.pc && this.pc.b2b)) {
this.stats.b2b++;
brokeB2B = false;
}
if (pc && this.pc && this.pc.b2b) {
this.stats.b2b += this.pc.b2b;
brokeB2B = false;
}
if (brokeB2B !== false) {
this.stats.b2b = -1;
}
} else {
this.stats.combo = -1;
brokeB2B = false;
}
const gSpecialBonus = this.initializer.garbage.specialBonus && garbageCleared > 0 && (this.lastSpin && this.lastSpin.type !== "none" || lines >= 4) ? 1 : 0;
const garbage = (0, _damageCalc.garbageCalcV2)({
b2b: Math.max(this.stats.b2b, 0),
combo: Math.max(this.stats.combo, 0),
enemies: 0,
lines,
piece: this.falling.symbol,
spin: this.lastSpin ? this.lastSpin.type : "none"
}, {
...this.gameOptions,
b2b: {
chaining: this.b2b.chaining,
charging: !!this.b2b.charging
}
});
const gEvents = garbage.garbage > 0 || gSpecialBonus > 0 ? [
this.garbageQueue.round(garbage.garbage * this.dynamic.garbageMultiplier.get() + gSpecialBonus)
] : [];
let surged = 0;
if (brokeB2B !== false) {
let btb = brokeB2B;
if (this.b2b.charging !== false && btb + 1 > this.b2b.charging.at) {
surged = Math.floor((btb - this.b2b.charging.at + this.b2b.charging.base + 1) * this.dynamic.garbageMultiplier.get());
const garbages = [
Math.round(surged / 3),
Math.round(surged / 3),
surged - 2 * Math.round(surged / 3)
];
gEvents.splice(0, 0, ...garbages);
brokeB2B = false;
}
}
if (pc && this.pc) {
gEvents.push(this.garbageQueue.round(this.pc.garbage * this.dynamic.garbageMultiplier.get()));
}
const res = {
mino: this.falling.symbol,
garbageCleared,
lines,
spin: this.lastSpin ? this.lastSpin.type : "none",
garbage: gEvents.filter((g)=>g > 0),
rawGarbage: gEvents.filter((g)=>g > 0),
surge: surged,
stats: this.stats,
garbageAdded: false,
topout: false,
keysPresses: this.resCache.keys.splice(0),
pieceTime: Math.round((this.frame + this.subframe - this.resCache.lastLock) * 10) / 10
};
for (const gb of res.garbage)this.stats.garbage.attack += gb;
if (lines > 0) {
const cancelEvents = [];
this.lastWasClear = true;
while(res.garbage.length > 0){
if (res.garbage[0] === 0) {
res.garbage.shift();
continue;
}
const [r, cancelled] = this.garbageQueue.cancel(res.garbage[0], this.stats.pieces, {
openerPhase: (this.misc.date ?? new Date()) < new Date(2025, 1, 16)
});
cancelEvents.push(...cancelled.map((c)=>({
iid: c.cid,
amount: c.amount,
size: c.size
})));
if (r === 0) res.garbage.shift();
else {
res.garbage[0] = r;
break;
}
}
cancelEvents.forEach((event)=>{
this.events.emit("garbage.cancel", event);
});
} else {
this.lastWasClear = false;
const garbages = this.garbageQueue.tank(this.frame, this.dynamic.garbageCap.get(), hard);
res.garbageAdded = garbages;
if (res.garbageAdded) {
const tankEvent = [];
garbages.forEach((garbage, idx)=>{
this.board.insertGarbage({
...garbage,
bombs: this.garbageQueue.options.bombs
});
this.connectedBoard.insertGarbage({
...garbage,
bombs: this.garbageQueue.options.bombs,
isBeginning: idx === 0 || garbages[idx - 1].id !== garbage.id,
isEnd: idx === garbages.length - 1 || garbages[idx + 1].id !== garbage.id
});
if (tankEvent.length === 0 || tankEvent[tankEvent.length - 1].iid !== garbage.id) {
tankEvent.push({
iid: garbage.id,
column: garbage.column,
amount: garbage.amount,
size: garbage.size
});
} else {
tankEvent[tankEvent.length - 1].amount += garbage.amount;
}
});
tankEvent.forEach((event)=>{
this.events.emit("garbage.tank", event);
});
}
}
this.nextPiece();
this.lastSpin = null;
try {
if (!(0, _kicks.legal)(this.falling.absoluteBlocks, this.board.state)) res.topout = true;
} catch {
res.topout = true;
}
if (res.garbage.length > 0) {
if (this.multiplayer) this.multiplayer.targets.forEach((target)=>res.garbage.forEach((g)=>this.igeHandler.send({
amount: g,
playerID: target
})));
}
const sent = res.garbage.reduce((a, b)=>a + b, 0);
this.stats.garbage.sent += sent;
if (this.stats.combo >= 0) this.currentSpike += sent;
else this.currentSpike = 0;
this.resCache.pieces++;
this.resCache.garbage.sent.push(...res.garbage);
this.resCache.garbage.received.push(...res.garbageAdded || []);
this.resCache.lastLock = this.frame + this.subframe;
this.stats.pieces++;
this.stats.lines += lines;
this.events.emit("falling.lock", (0, _utils.deepCopy)(res));
return res;
}
press(key) {
if (key in this) return this[key]();
else throw new Error("invalid key: " + key);
}
#keydown({ data: event }) {
this.#processSubframe(event.subframe);
this.resCache.keys.push(event.key);
// TODO: inversion?
// if (this.misc.inverted)
// switch (e.key) {
// case "moveLeft":
// e.key = "moveRight";
// break;
// case "moveRight":
// e.key = "moveLeft";
// }
switch(event.key){
case "moveLeft":
this.falling.keys++;
if (this.input.firstInputTime == -1) this.input.firstInputTime = this.frame + this.subframe;
this.#activateShift("lShift", !!event.hoisted);
this.#__internal_shift();
return;
case "moveRight":
this.falling.keys++;
if (this.input.firstInputTime == -1) this.input.firstInputTime = this.frame + this.subframe;
this.#activateShift("rShift", !!event.hoisted);
this.#__internal_shift();
return;
case "softDrop":
this.input.keys.softDrop = true;
if (this.input.firstInputTime == -1) this.input.firstInputTime = this.frame + this.subframe;
return;
case "rotateCCW":
this.input.keys.rotateCCW = true;
break;
case "rotateCW":
this.input.keys.rotateCW = true;
break;
case "rotate180":
this.input.keys.rotate180 = true;
break;
case "hold":
this.input.keys.hold = true;
break;
}
// todo: all in if (!this.pause)
switch(event.key){
case "rotateCCW":
if (this.input.firstInputTime == -1) this.input.firstInputTime = this.frame + this.subframe;
this.#processRotate(-1);
break;
case "rotateCW":
if (this.input.firstInputTime == -1) this.input.firstInputTime = this.frame + this.subframe;
this.#processRotate(1);
break;
case "rotate180":
if (!this.misc.allowed.spin180) return;
if (this.input.firstInputTime == -1) this.input.firstInputTime = this.frame + this.subframe;
this.#processRotate(2);
break;
case "hardDrop":
if (this.misc.allowed.hardDrop === false || this.falling.safeLock !== 0) return;
this.hardDrop();
return;
case "hold":
this.hold();
return;
}
}
#keyup({ data: event }) {
this.#processSubframe(event.subframe);
// TODO: inversion?
// if (this.misc.inverted)
// switch (e.key) {
// case "moveLeft":
// e.key = "moveRight";
// break;
// case "moveRight":
// e.key = "moveLeft";
// }
switch(event.key){
case "moveLeft":
this.input.lShift.held = false;
this.input.lShift.das = 0;
this.input.lastShift = this.input.rShift.held ? this.input.rShift.dir : this.input.lastShift;
if (this.handling.cancel) {
this.input.rShift.arr = this.handling.arr;
this.input.rShift.das = 0;
}
break;
case "moveRight":
this.input.rShift.held = false;
this.input.rShift.das = 0;
this.input.lastShift = this.input.lShift.held ? this.input.lShift.dir : this.input.lastShift;
if (this.handling.cancel) {
this.input.lShift.arr = this.handling.arr;
this.input.lShift.das = 0;
}
break;
case "softDrop":
this.state |= _constants.constants.flags.ACTION_SOFTDROP;
this.input.keys.softDrop = false;
break;
case "rotateCCW":
this.input.keys.rotateCCW = false;
break;
case "rotateCW":
this.input.keys.rotateCW = false;
break;
case "rotate180":
this.input.keys.rotate180 = false;
break;
case "hold":
this.input.keys.hold = false;
break;
}
}
#activateShift(shift, hoisted) {
this.input[shift].held = true;
this.input[shift].das = hoisted ? this.handling.das - this.handling.dcd : 0;
this.input[shift].arr = this.handling.arr;
this.input.lastShift = this.input[shift].dir;
}
#processRotate(rotation) {
this.falling.keys += rotation >= 2 ? 2 : 1;
return this.#rotate(rotation);
}
#processSubframe(subframe) {
if (subframe <= this.subframe) return;
const delta = subframe - this.subframe;
this.#processAllShift(delta);
this.#fall(delta);
this.subframe = subframe;
}
#__internal_shift() {
if (!this.#isSleep()) {
// TODO: missing: && !this.pause
this.state |= _constants.constants.flags.ACTION_MOVE;
if ((0, _kicks.legal)(this.falling.absoluteAt({
x: this.falling.location[0] + this.input.lastShift
}), this.board.state)) {
this.falling.location[0] += this.input.lastShift;
if (this.falling.lockResets < 31) this.falling.lockResets++;
this.#clearFlags(_constants.constants.flags.ROTATION_ALL | _constants.constants.flags.STATE_WALL);
// fallingdirty = true;
if (this.#is20G()) this.#slamToFloor();
if (!this.#shouldLock()) this.falling.locking = 0;
return true;
} else {
this.state |= _constants.constants.flags.STATE_WALL;
return false;
}
}
}
#processShift(shift, delta) {
if (!this.input[shift].held || this.input.lastShift !== this.input[shift].dir) return;
const arrDelta = Math.max(0, delta - Math.max(0, this.handling.das - this.input[shift].das));
this.input[shift].das = Math.min(this.input[shift].das + delta, this.handling.das);
if (this.input[shift].das < this.handling.das) return;
if (this.#isSleep()) return; // TODO: missing: && !this.pause
this.input[shift].arr += arrDelta;
if (this.input[shift].arr < this.handling.arr) return;
const arrMultiplier = this.handling.arr === 0 ? this.board.width : Math.floor(this.input[shift].arr / this.handling.arr);
this.input[shift].arr -= this.handling.arr * arrMultiplier;
for(let i = 0; i < arrMultiplier; i++)this.#__internal_shift();
}
#processAllShift(subFrameDiff = 1 - this.subframe) {
for (const shift of [
"lShift",
"rShift"
])this.#processShift(shift, subFrameDiff);
}
#run(...frames) {
frames.forEach((frame)=>{
switch(frame.type){
case "keydown":
this.#keydown(frame);
break;
case "keyup":
this.#keyup(frame);
break;
case "ige":
if (frame.data.type === "interaction") {
if (frame.data.data.type === "garbage") {
const original = frame.data.data.amt;
const amount = this.multiplayer?.passthrough?.network ? this.igeHandler.receive({
playerID: frame.data.data.gameid,
ackiid: frame.data.data.ackiid,
amount: frame.data.data.amt,
iid: frame.data.data.iid
}) : frame.data.data.amt;
this.receiveGarbage({
frame: Number.MAX_SAFE_INTEGER - this.garbageQueue.options.garbage.speed,
amount,
size: frame.data.data.size,
cid: frame.data.data.iid,
gameid: frame.data.data.gameid,
confirmed: false
});
this.stats.garbage.receive += amount;
this.events.emit("garbage.receive", {
iid: frame.data.data.iid,
amount,
originalAmount: original
});
}
} else if (frame.data.type === "interaction_confirm") {
if (frame.data.data.type === "garbage") {
this.garbageQueue.confirm(frame.data.data.iid, frame.data.data.gameid, frame.frame);
this.events.emit("garbage.confirm", {
iid: frame.data.data.iid,
gameid: frame.data.data.gameid,
frame: frame.frame
});
}
} else if (frame.data.type === "target" && this.multiplayer) {
this.multiplayer.targets = frame.data.data.targets;
}
break;
}
});
}
tick(frames) {
this.subframe = 0;
this.#run(...frames);
this.frame++;
this.#processAllShift();
this.#fall();
// TODO: execute waiting frames
// TODO: process garbage are
Object.keys(this.dynamic).forEach((key)=>this.dynamic[key].tick());
return {
...this.flushRes()
};
}
receiveGarbage(...garbage) {
this.garbageQueue.receive(...garbage);
}
getPreview(piece) {
return _tetromino.tetrominoes[piece.toLowerCase()].preview;
}
connect(blocks) {
const exists = (x, y)=>!!blocks.find(([a, b])=>a === x && y === b);
return blocks.map(([x, y])=>{
let state = 0;
if (!exists(x, y - 1)) state |= 0b1000;
if (!exists(x + 1, y)) state |= 0b0100;
if (!exists(x, y + 1)) state |= 0b0010;
if (!exists(x - 1, y)) state |= 0b0001;
if (state === 0b1001 && !exists(x + 1, y + 1) || state === 0b1100 && !exists(x - 1, y + 1) || state === 0b0011 && !exists(x + 1, y - 1) || state === 0b0110 && !exists(x - 1, y - 1) || state === 0b0010 && !exists(x + 1, y - 1) && !exists(x - 1, y - 1) || state === 0b0001 && !exists(x + 1, y - 1) && !exists(x + 1, y + 1) || state === 0b1000 && !exists(x - 1, y + 1) && !exists(x + 1, y + 1) || state === 0b0100 && !exists(x - 1, y - 1) && !exists(x - 1, y + 1)) {
state |= 0b1_0000;
}
return [
x,
y,
state
];
});
}
getConnectedPreview(piece) {
const data = _tetromino.tetrominoes[piece.toLowerCase()].preview;
return {
...data,
data: this.connect(data.data)
};
}
/** @deprecated Engine.onQueuePieces is deprecated and no longer functional. Switch to Engine.events.on("queue.add", (pieces) => {}) instead. */ onQueuePieces(_listener) {
console.log(`${_chalk.default.redBright("[Triangle.js]")} Engine.onQueuePieces is deprecated and no longer functional. Switch to Engine.events.on("queue.add", (pieces) => {}) instead.`);
}
static colorMap = {
i: _chalk.default.bgCyan,
j: _chalk.default.bgBlue,
l: _chalk.default.bgYellow,
o: _chalk.default.bgWhite,
s: _chalk.default.bgGreenBright,
t: _chalk.default.bgMagentaBright,
z: _chalk.default.bgRedBright,
gb: _chalk.default.bgBlackBright,
bomb: _chalk.default.bgHex("#FFA500")
};
get text() {
const boardTop = this.board.state.findIndex((row)=>row.every((block)=>block === null));
const height = Math.max(this.garbageQueue.size, boardTop, 0);
const output = [];
for(let i = 0; i < height; i++){
let str = i % 2 === 0 ? "|" : " ";
if (i < this.garbageQueue.size) str += " " + _chalk.default.bgRed(" ") + " ";
else str += " ";
if (i > boardTop) {
output.push(str + " ".repeat(this.board.width) + " " + (i % 2 === 0 ? "|" : " "));
continue;
}
for(let j = 0; j < this.board.width; j++){
const block = this.board.state[i][j];
str += block ? Engine.colorMap[block](" ") : " ";
}
output.push(str + " " + (i % 2 === 0 ? "|" : " "));
}
return output.reverse().join("\n");
}
/** Return an engine with the same config. Does not preserve state. */ clone() {
return new Engine(this.initializer);
}
}
//# sourceMappingURL=index.js.map