#include "Game.hpp" #include #include #include // 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 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(mActiveType) << std::endl; mState = GameState::GAME_OVER; flagGameOverSFX = true; } else { std::cout << "[DEBUG] Spawned piece type " << static_cast(mActiveType) << " successfully at (" << mActiveX << ", " << mActiveY << ")" << std::endl; } } std::vector Game::getActiveCells() const { std::vector cells; if (mActiveType == PieceType::NONE) return cells; int typeIdx = static_cast(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 Game::getGhostCells() const { std::vector 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& 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(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 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 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 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 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 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 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 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; } }