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
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; |
|
} |
|
}
|
|
|