CyberMatris port for Miyoo Mini Plus
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

450 lines
12 KiB

#include "Game.hpp"
#include <algorithm>
#include <chrono>
#include <iostream>
// SRS Wall-Kick translation vectors (x, y) for 3x3 shapes (T, J, L, S, Z)
// Direction is either Clockwise (CW) or Counter-Clockwise (CCW).
// Format: KICKS_3x3[CW/CCW][StartRot][TestIndex]
static const Point KICKS_3x3_CW[4][5] = {
{ {0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2} }, // 0 -> 1
{ {0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2} }, // 1 -> 2
{ {0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2} }, // 2 -> 3
{ {0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2} } // 3 -> 0
};
static const Point KICKS_3x3_CCW[4][5] = {
{ {0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2} }, // 0 -> 3
{ {0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2} }, // 1 -> 0
{ {0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2} }, // 2 -> 1
{ {0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2} } // 3 -> 2
};
// SRS Wall-Kick translation vectors (x, y) for I piece (4x4)
static const Point KICKS_I_CW[4][5] = {
{ {0, 0}, {-2, 0}, {1, 0}, {-2, 1}, {1, -2} }, // 0 -> 1
{ {0, 0}, {-1, 0}, {2, 0}, {-1, -2}, {2, 1} }, // 1 -> 2
{ {0, 0}, {2, 0}, {-1, 0}, {2, -1}, {-1, 2} }, // 2 -> 3
{ {0, 0}, {1, 0}, {-2, 0}, {1, 2}, {-2, -1} } // 3 -> 0
};
static const Point KICKS_I_CCW[4][5] = {
{ {0, 0}, {-1, 0}, {2, 0}, {-1, -2}, {2, 1} }, // 0 -> 3
{ {0, 0}, {2, 0}, {-1, 0}, {2, -1}, {-1, 2} }, // 1 -> 0
{ {0, 0}, {1, 0}, {-2, 0}, {1, 2}, {-2, -1} }, // 2 -> 1
{ {0, 0}, {-2, 0}, {1, 0}, {-2, 1}, {1, -2} } // 3 -> 2
};
Game::Game() {
unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
mRng.seed(seed);
init();
}
void Game::init() {
mState = GameState::START;
reset();
}
void Game::reset() {
// Clear grid
for (int y = 0; y < BOARD_HEIGHT; ++y) {
for (int x = 0; x < BOARD_WIDTH; ++x) {
mBoard[y][x] = PieceType::NONE;
}
}
mScore = 0;
mLinesCleared = 0;
mLevel = 1;
mCombo = -1;
mHoldType = PieceType::NONE;
mCanHold = true;
mFallTimer = 0.0f;
mLockTimer = 0.0f;
mIsLocking = false;
mShakeIntensity = 0.0f;
mBag.clear();
mNextQueue.clear();
// Populate the queue with initial pieces
fillBag();
for (int i = 0; i < 4; ++i) {
mNextQueue.push_back(popNextPiece());
}
mActiveType = PieceType::NONE;
spawnPiece();
}
void Game::fillBag() {
std::vector<PieceType> pieces = {
PieceType::I, PieceType::O, PieceType::T, PieceType::S, PieceType::Z, PieceType::J, PieceType::L
};
std::shuffle(pieces.begin(), pieces.end(), mRng);
mBag.insert(mBag.end(), pieces.begin(), pieces.end());
}
PieceType Game::popNextPiece() {
if (mBag.empty()) {
fillBag();
}
PieceType next = mBag.back();
mBag.pop_back();
return next;
}
void Game::spawnPiece() {
mActiveType = mNextQueue.front();
mNextQueue.erase(mNextQueue.begin());
mNextQueue.push_back(popNextPiece());
mActiveRot = 0;
// Spawn at top center
mActiveX = BOARD_WIDTH / 2 - 1;
mActiveY = 0;
mCanHold = true;
mIsLocking = false;
mLockTimer = 0.0f;
// Check game over right at spawn
if (fits(mActiveType, mActiveRot, mActiveX, mActiveY)) {
std::cout << "[DEBUG] Game Over triggered on spawn of piece type " << static_cast<int>(mActiveType) << std::endl;
mState = GameState::GAME_OVER;
flagGameOverSFX = true;
} else {
std::cout << "[DEBUG] Spawned piece type " << static_cast<int>(mActiveType) << " successfully at (" << mActiveX << ", " << mActiveY << ")" << std::endl;
}
}
std::vector<Point> Game::getActiveCells() const {
std::vector<Point> cells;
if (mActiveType == PieceType::NONE) return cells;
int typeIdx = static_cast<int>(mActiveType);
for (int i = 0; i < 4; ++i) {
Point p = TETROMINO_CELLS[typeIdx][mActiveRot][i];
cells.push_back({mActiveX + p.x, mActiveY + p.y});
}
return cells;
}
std::vector<Point> Game::getGhostCells() const {
std::vector<Point> cells = getActiveCells();
if (cells.empty()) return cells;
int dy = 0;
while (!checkCollision(cells, 0, dy + 1)) {
dy++;
}
for (auto& p : cells) {
p.y += dy;
}
return cells;
}
bool Game::checkCollision(const std::vector<Point>& cells, int dx, int dy) const {
for (const auto& p : cells) {
int nx = p.x + dx;
int ny = p.y + dy;
// Bounds check
if (nx < 0 || nx >= BOARD_WIDTH || ny >= BOARD_HEIGHT) {
return true;
}
// Top cap check (allow pieces to scroll off top grid temporarily)
if (ny < 0) continue;
if (mBoard[ny][nx] != PieceType::NONE) {
return true;
}
}
return false;
}
bool Game::fits(PieceType type, int rot, int cx, int cy) const {
int typeIdx = static_cast<int>(type);
for (int i = 0; i < 4; ++i) {
Point p = TETROMINO_CELLS[typeIdx][rot][i];
int nx = cx + p.x;
int ny = cy + p.y;
if (nx < 0 || nx >= BOARD_WIDTH || ny >= BOARD_HEIGHT) {
return true;
}
if (ny < 0) continue;
if (mBoard[ny][nx] != PieceType::NONE) {
return true;
}
}
return false;
}
bool Game::moveLeft() {
if (mState != GameState::PLAYING) return false;
std::vector<Point> cells = getActiveCells();
if (!checkCollision(cells, -1, 0)) {
mActiveX--;
flagMoveSFX = true;
// Slide kick resets lock timer
if (mIsLocking) {
mLockTimer = 0.0f;
}
return true;
}
return false;
}
bool Game::moveRight() {
if (mState != GameState::PLAYING) return false;
std::vector<Point> cells = getActiveCells();
if (!checkCollision(cells, 1, 0)) {
mActiveX++;
flagMoveSFX = true;
// Slide kick resets lock timer
if (mIsLocking) {
mLockTimer = 0.0f;
}
return true;
}
return false;
}
bool Game::rotate(int dir) {
if (mState != GameState::PLAYING || mActiveType == PieceType::NONE) return false;
if (mActiveType == PieceType::O) return false; // O piece does not rotate
int nextRot = (mActiveRot + dir + 4) % 4;
// Get wall kick offset trials
const Point(*kicks)[5] = nullptr;
if (mActiveType == PieceType::I) {
kicks = (dir == 1) ? KICKS_I_CW : KICKS_I_CCW;
} else {
kicks = (dir == 1) ? KICKS_3x3_CW : KICKS_3x3_CCW;
}
// Try all 5 Wall-kick tests
for (int t = 0; t < 5; ++t) {
int dx = kicks[mActiveRot][t].x;
int dy = kicks[mActiveRot][t].y;
if (!fits(mActiveType, nextRot, mActiveX + dx, mActiveY + dy)) {
// Apply rotation and kick offset
mActiveRot = nextRot;
mActiveX += dx;
mActiveY += dy;
flagRotateSFX = true;
// Reset lock delay on successful rotation
if (mIsLocking) {
mLockTimer = 0.0f;
}
return true;
}
}
return false;
}
void Game::softDrop() {
if (mState != GameState::PLAYING) return;
std::vector<Point> cells = getActiveCells();
if (!checkCollision(cells, 0, 1)) {
mActiveY++;
mScore += 1; // Soft drop score
mFallTimer = 0.0f;
}
}
bool Game::hardDrop() {
if (mState != GameState::PLAYING) return false;
std::vector<Point> cells = getActiveCells();
int dy = 0;
while (!checkCollision(cells, 0, dy + 1)) {
dy++;
}
mActiveY += dy;
mScore += dy * 2; // Hard drop score
// Force instant lock
mShakeIntensity = 0.15f + (dy * 0.015f); // hard drop screenshake!
lockPiece();
return true;
}
void Game::holdPiece() {
if (mState != GameState::PLAYING || !mCanHold) return;
flagRotateSFX = true; // cool swoosh
PieceType temp = mHoldType;
mHoldType = mActiveType;
if (temp == PieceType::NONE) {
spawnPiece();
} else {
mActiveType = temp;
mActiveRot = 0;
mActiveX = BOARD_WIDTH / 2 - 1;
mActiveY = 0;
mIsLocking = false;
mLockTimer = 0.0f;
}
mCanHold = false;
}
float Game::getFallDelay() const {
// Standard progressive fall timing (seconds per grid step)
switch (mLevel) {
case 1: return 0.85f;
case 2: return 0.72f;
case 3: return 0.60f;
case 4: return 0.48f;
case 5: return 0.38f;
case 6: return 0.30f;
case 7: return 0.22f;
case 8: return 0.16f;
case 9: return 0.11f;
case 10: return 0.08f;
default: return 0.06f; // level 11+
}
}
bool Game::update(float dt) {
if (mState != GameState::PLAYING) return false;
if (mShakeIntensity > 0.0f) {
mShakeIntensity -= dt * 0.8f; // Decay over time
if (mShakeIntensity < 0.0f) mShakeIntensity = 0.0f;
}
mClearedLinesThisTick.clear();
std::vector<Point> cells = getActiveCells();
bool onGround = checkCollision(cells, 0, 1);
if (onGround) {
if (!mIsLocking) {
mIsLocking = true;
mLockTimer = 0.0f;
}
mLockTimer += dt;
// Lock delay threshold of 0.45 seconds
if (mLockTimer >= 0.45f) {
lockPiece();
return true;
}
} else {
mIsLocking = false;
mFallTimer += dt;
float fallDelay = getFallDelay();
if (mFallTimer >= fallDelay) {
mFallTimer -= fallDelay;
mActiveY++;
}
}
return false;
}
void Game::lockPiece() {
std::vector<Point> cells = getActiveCells();
if (cells.empty()) return;
mLastLockedCells = cells;
for (const auto& p : cells) {
if (p.y >= 0 && p.y < BOARD_HEIGHT && p.x >= 0 && p.x < BOARD_WIDTH) {
mBoard[p.y][p.x] = mActiveType;
}
}
flagLandSFX = true;
clearLines();
spawnPiece();
}
void Game::clearLines() {
std::vector<int> fullLines;
for (int y = 0; y < BOARD_HEIGHT; ++y) {
bool full = true;
for (int x = 0; x < BOARD_WIDTH; ++x) {
if (mBoard[y][x] == PieceType::NONE) {
full = false;
break;
}
}
if (full) {
fullLines.push_back(y);
}
}
if (fullLines.empty()) {
mCombo = -1; // reset combo
return;
}
mClearedLinesThisTick = fullLines; // save for particle animations
mCombo++;
// Scoring guideline
int baseScore = 0;
int cleared = fullLines.size();
if (cleared == 1) {
baseScore = 100;
flagLineClearSFX = true;
mShakeIntensity = std::max(mShakeIntensity, 0.08f);
} else if (cleared == 2) {
baseScore = 300;
flagLineClearSFX = true;
mShakeIntensity = std::max(mShakeIntensity, 0.15f);
} else if (cleared == 3) {
baseScore = 500;
flagLineClearSFX = true;
mShakeIntensity = std::max(mShakeIntensity, 0.22f);
} else if (cleared >= 4) {
baseScore = 800;
flagTetrisClearSFX = true; // Boom! Tetris!
mShakeIntensity = std::max(mShakeIntensity, 0.40f); // Screen Rumble!
}
mScore += baseScore * mLevel;
if (mCombo > 0) {
mScore += 50 * mCombo * mLevel;
}
// Erase cleared lines and shift elements down
for (int lineY : fullLines) {
for (int currY = lineY; currY > 0; --currY) {
for (int x = 0; x < BOARD_WIDTH; ++x) {
mBoard[currY][x] = mBoard[currY - 1][x];
}
}
// Top line cleared
for (int x = 0; x < BOARD_WIDTH; ++x) {
mBoard[0][x] = PieceType::NONE;
}
}
mLinesCleared += cleared;
// Level up logic (every 10 lines)
int nextLevel = (mLinesCleared / 10) + 1;
if (nextLevel > mLevel) {
mLevel = nextLevel;
flagLevelUpSFX = true;
}
}