From 50c869536d634973a296a568ca4a1a63913ed2e8 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Sun, 21 Dec 2025 19:45:20 +0100 Subject: [PATCH] highscore fixes --- src/app/TetrisApp.cpp | 91 ++++++++++++++++++--- src/core/application/ApplicationManager.cpp | 24 ++++-- src/gameplay/coop/CoopGame.cpp | 7 ++ src/graphics/renderers/GameRenderer.cpp | 28 +++++++ src/persistence/Scores.cpp | 10 ++- src/persistence/Scores.h | 6 +- src/states/MenuState.cpp | 18 ++-- 7 files changed, 155 insertions(+), 29 deletions(-) diff --git a/src/app/TetrisApp.cpp b/src/app/TetrisApp.cpp index 62da545..cbb0e90 100644 --- a/src/app/TetrisApp.cpp +++ b/src/app/TetrisApp.cpp @@ -172,6 +172,8 @@ struct TetrisApp::Impl { int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings bool isNewHighScore = false; std::string playerName; + std::string player2Name; + int highScoreEntryIndex = 0; // 0 = entering player1, 1 = entering player2 bool helpOverlayPausedGame = false; SDL_Window* window = nullptr; @@ -866,22 +868,54 @@ void TetrisApp::Impl::runLoop() } if (!showHelpOverlay && state == AppState::GameOver && isNewHighScore && e.type == SDL_EVENT_TEXT_INPUT) { - if (playerName.length() < 12) { - playerName += e.text.text; + // Support single-player and coop two-name entry + if (game && game->getMode() == GameMode::Cooperate && coopGame) { + if (highScoreEntryIndex == 0) { + if (playerName.length() < 12) playerName += e.text.text; + } else { + if (player2Name.length() < 12) player2Name += e.text.text; + } + } else { + if (playerName.length() < 12) playerName += e.text.text; } } if (!showHelpOverlay && state == AppState::GameOver && e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { if (isNewHighScore) { - if (e.key.scancode == SDL_SCANCODE_BACKSPACE && !playerName.empty()) { - playerName.pop_back(); - } else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) { - if (playerName.empty()) playerName = "PLAYER"; - ensureScoresLoaded(); - scores.submit(game->score(), game->lines(), game->level(), game->elapsed(), playerName); - Settings::instance().setPlayerName(playerName); - isNewHighScore = false; - SDL_StopTextInput(window); + if (game && game->getMode() == GameMode::Cooperate && coopGame) { + // Two-name entry flow + if (e.key.scancode == SDL_SCANCODE_BACKSPACE) { + if (highScoreEntryIndex == 0 && !playerName.empty()) playerName.pop_back(); + else if (highScoreEntryIndex == 1 && !player2Name.empty()) player2Name.pop_back(); + } else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) { + if (highScoreEntryIndex == 0) { + if (playerName.empty()) playerName = "P1"; + highScoreEntryIndex = 1; // move to second name + } else { + if (player2Name.empty()) player2Name = "P2"; + // Submit combined name + std::string combined = playerName + " & " + player2Name; + int leftScore = coopGame->score(CoopGame::PlayerSide::Left); + int rightScore = coopGame->score(CoopGame::PlayerSide::Right); + int combinedScore = leftScore + rightScore; + ensureScoresLoaded(); + scores.submit(combinedScore, coopGame->lines(), coopGame->level(), coopGame->elapsed(), combined); + Settings::instance().setPlayerName(playerName); + isNewHighScore = false; + SDL_StopTextInput(window); + } + } + } else { + if (e.key.scancode == SDL_SCANCODE_BACKSPACE && !playerName.empty()) { + playerName.pop_back(); + } else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) { + if (playerName.empty()) playerName = "PLAYER"; + ensureScoresLoaded(); + scores.submit(game->score(), game->lines(), game->level(), game->elapsed(), playerName); + Settings::instance().setPlayerName(playerName); + isNewHighScore = false; + SDL_StopTextInput(window); + } } } else { if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) { @@ -1255,6 +1289,21 @@ void TetrisApp::Impl::runLoop() } if (coopGame->isGameOver()) { + // Compute combined coop stats for Game Over + int leftScore = coopGame->score(CoopGame::PlayerSide::Left); + int rightScore = coopGame->score(CoopGame::PlayerSide::Right); + int combinedScore = leftScore + rightScore; + if (combinedScore > 0) { + isNewHighScore = true; + playerName.clear(); + player2Name.clear(); + highScoreEntryIndex = 0; + SDL_StartTextInput(window); + } else { + isNewHighScore = false; + ensureScoresLoaded(); + scores.submit(combinedScore, coopGame->lines(), coopGame->level(), coopGame->elapsed()); + } state = AppState::GameOver; stateMgr->setState(state); } @@ -1974,13 +2023,29 @@ void TetrisApp::Impl::runLoop() SDL_RenderFillRect(renderer, &boxRect); ensureScoresLoaded(); - bool realHighScore = scores.isHighScore(game->score()); + // Choose display values based on mode (single-player vs coop) + int displayScore = 0; + int displayLines = 0; + int displayLevel = 0; + if (game && game->getMode() == GameMode::Cooperate && coopGame) { + int leftScore = coopGame->score(CoopGame::PlayerSide::Left); + int rightScore = coopGame->score(CoopGame::PlayerSide::Right); + displayScore = leftScore + rightScore; + displayLines = coopGame->lines(); + displayLevel = coopGame->level(); + } else if (game) { + displayScore = game->score(); + displayLines = game->lines(); + displayLevel = game->level(); + } + + bool realHighScore = scores.isHighScore(displayScore); const char* title = realHighScore ? "NEW HIGH SCORE!" : "GAME OVER"; int tW=0, tH=0; pixelFont.measure(title, 2.0f, tW, tH); pixelFont.draw(renderer, boxX + (boxW - tW) * 0.5f + contentOffsetX, boxY + 40 + contentOffsetY, title, 2.0f, realHighScore ? SDL_Color{255, 220, 0, 255} : SDL_Color{255, 60, 60, 255}); char scoreStr[64]; - snprintf(scoreStr, sizeof(scoreStr), "SCORE: %d", game->score()); + snprintf(scoreStr, sizeof(scoreStr), "SCORE: %d", displayScore); int sW=0, sH=0; pixelFont.measure(scoreStr, 1.2f, sW, sH); pixelFont.draw(renderer, boxX + (boxW - sW) * 0.5f + contentOffsetX, boxY + 100 + contentOffsetY, scoreStr, 1.2f, {255, 255, 255, 255}); diff --git a/src/core/application/ApplicationManager.cpp b/src/core/application/ApplicationManager.cpp index 9f2c2e6..5a3c766 100644 --- a/src/core/application/ApplicationManager.cpp +++ b/src/core/application/ApplicationManager.cpp @@ -1232,13 +1232,25 @@ void ApplicationManager::setupStateHandlers() { // "GAME OVER" title font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 120, 140, "GAME OVER", 3.0f, {255, 80, 60, 255}); - // Game stats + // Game stats (single-player or coop combined) char buf[128]; - std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d", - m_stateContext.game->score(), - m_stateContext.game->lines(), - m_stateContext.game->level()); - font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 180, 220, buf, 1.2f, {220, 220, 230, 255}); + if (m_stateContext.game && m_stateContext.game->getMode() == GameMode::Cooperate && m_stateContext.coopGame) { + int leftScore = m_stateContext.coopGame->score(::CoopGame::PlayerSide::Left); + int rightScore = m_stateContext.coopGame->score(::CoopGame::PlayerSide::Right); + int total = leftScore + rightScore; + std::snprintf(buf, sizeof(buf), "SCORE %d + %d = %d LINES %d LEVEL %d", + leftScore, + rightScore, + total, + m_stateContext.coopGame->lines(), + m_stateContext.coopGame->level()); + } else { + std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d", + m_stateContext.game ? m_stateContext.game->score() : 0, + m_stateContext.game ? m_stateContext.game->lines() : 0, + m_stateContext.game ? m_stateContext.game->level() : 0); + } + font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 220, 220, buf, 1.2f, {220, 220, 230, 255}); // Instructions font.draw(renderer.getSDLRenderer(), LOGICAL_W * 0.5f - 120, 270, "PRESS ENTER / SPACE", 1.2f, {200, 200, 220, 255}); diff --git a/src/gameplay/coop/CoopGame.cpp b/src/gameplay/coop/CoopGame.cpp index 8b29b4c..bcaf8b6 100644 --- a/src/gameplay/coop/CoopGame.cpp +++ b/src/gameplay/coop/CoopGame.cpp @@ -408,6 +408,13 @@ void CoopGame::applyLineClearRewards(PlayerState& creditPlayer, int cleared) { _score += base * (_level + 1); creditPlayer.score += base * (creditPlayer.level + 1); + // Also award a trivial per-line bonus to both players so clears benefit + // both participants equally (as requested). + if (cleared > 0) { + left.score += cleared; + right.score += cleared; + } + _lines += cleared; creditPlayer.lines += cleared; diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index c845404..eb6b732 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -2668,6 +2668,34 @@ void GameRenderer::renderCoopPlayingState( drawPlayerScoreboard(CoopGame::PlayerSide::Left, leftColumnLeftX, leftColumnRightX, "PLAYER 1"); drawPlayerScoreboard(CoopGame::PlayerSide::Right, rightColumnLeftX, rightColumnRightX, "PLAYER 2"); + + // Combined score summary centered under the grid + { + int leftScore = game->score(CoopGame::PlayerSide::Left); + int rightScore = game->score(CoopGame::PlayerSide::Right); + int sumScore = leftScore + rightScore; + char sumLabel[64]; + char sumValue[64]; + std::snprintf(sumLabel, sizeof(sumLabel), "SCORE %d + SCORE %d =", leftScore, rightScore); + std::snprintf(sumValue, sizeof(sumValue), "%d", sumScore); + + // Draw label smaller and value larger + float labelScale = 0.9f; + float valueScale = 1.6f; + SDL_Color labelColor = {200, 220, 235, 220}; + SDL_Color valueColor = {255, 230, 130, 255}; + + // Position: centered beneath the grid + float centerX = gridX + GRID_W * 0.5f; + int lw=0, lh=0; pixelFont->measure(sumLabel, labelScale, lw, lh); + int vw=0, vh=0; pixelFont->measure(sumValue, valueScale, vw, vh); + float labelX = centerX - static_cast(lw) * 0.5f; + float valueX = centerX - static_cast(vw) * 0.5f; + float belowY = gridY + GRID_H + 14.0f; // small gap below grid + + pixelFont->draw(renderer, labelX, belowY, sumLabel, labelScale, labelColor); + pixelFont->draw(renderer, valueX, belowY + 22.0f, sumValue, valueScale, valueColor); + } } void GameRenderer::renderExitPopup( diff --git a/src/persistence/Scores.cpp b/src/persistence/Scores.cpp index c744b8e..3232ad4 100644 --- a/src/persistence/Scores.cpp +++ b/src/persistence/Scores.cpp @@ -42,6 +42,7 @@ void ScoreManager::load() { if (value.contains("level")) e.level = value["level"]; if (value.contains("timeSec")) e.timeSec = value["timeSec"]; if (value.contains("name")) e.name = value["name"]; + if (value.contains("game_type")) e.gameType = value["game_type"].get(); scores.push_back(e); } } @@ -54,6 +55,7 @@ void ScoreManager::load() { if (value.contains("level")) e.level = value["level"]; if (value.contains("timeSec")) e.timeSec = value["timeSec"]; if (value.contains("name")) e.name = value["name"]; + if (value.contains("game_type")) e.gameType = value["game_type"].get(); scores.push_back(e); } } @@ -92,6 +94,8 @@ void ScoreManager::load() { if (!remaining.empty() && remaining[0] == ' ') { e.name = remaining.substr(1); // Remove leading space } + // For backward compatibility local files may not include gameType; default is 'classic' + e.gameType = "classic"; scores.push_back(e); } if (scores.size() >= maxEntries) break; @@ -108,11 +112,12 @@ void ScoreManager::load() { void ScoreManager::save() const { std::ofstream f(filePath(), std::ios::trunc); for (auto &e : scores) { - f << e.score << ' ' << e.lines << ' ' << e.level << ' ' << e.timeSec << ' ' << e.name << '\n'; + // Save gameType as trailing token so future loads can preserve it + f << e.score << ' ' << e.lines << ' ' << e.level << ' ' << e.timeSec << ' ' << e.name << ' ' << e.gameType << '\n'; } } -void ScoreManager::submit(int score, int lines, int level, double timeSec, const std::string& name) { +void ScoreManager::submit(int score, int lines, int level, double timeSec, const std::string& name, const std::string& gameType) { // Add to local list scores.push_back(ScoreEntry{score,lines,level,timeSec, name}); std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;}); @@ -131,6 +136,7 @@ void ScoreManager::submit(int score, int lines, int level, double timeSec, const j["timeSec"] = timeSec; j["name"] = name; j["timestamp"] = std::time(nullptr); // Add timestamp + j["game_type"] = gameType; // Fire and forget (async) would be better, but for now let's just try to send // We can use std::thread to make it async diff --git a/src/persistence/Scores.h b/src/persistence/Scores.h index 1fede86..1daa7cc 100644 --- a/src/persistence/Scores.h +++ b/src/persistence/Scores.h @@ -3,14 +3,16 @@ #include #include -struct ScoreEntry { int score{}; int lines{}; int level{}; double timeSec{}; std::string name{"PLAYER"}; }; +struct ScoreEntry { int score{}; int lines{}; int level{}; double timeSec{}; std::string name{"PLAYER"}; std::string gameType{"classic"}; }; class ScoreManager { public: explicit ScoreManager(size_t maxScores = 12); void load(); void save() const; - void submit(int score, int lines, int level, double timeSec, const std::string& name = "PLAYER"); + // New optional `gameType` parameter will be sent to Firebase as `game_type`. + // Allowed values: "classic", "versus", "cooperate", "challenge". + void submit(int score, int lines, int level, double timeSec, const std::string& name = "PLAYER", const std::string& gameType = "classic"); bool isHighScore(int score) const; const std::vector& all() const { return scores; } private: diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index a4c8e43..1a96e49 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -813,7 +813,13 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi } static const std::vector EMPTY_SCORES; const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES; - size_t maxDisplay = std::min(hs.size(), size_t(10)); // display only top 10 + // Filter highscores to show only classic gameplay entries on the main menu + std::vector filtered; + filtered.reserve(hs.size()); + for (const auto &e : hs) { + if (e.gameType == "classic") filtered.push_back(e); + } + size_t maxDisplay = std::min(filtered.size(), size_t(10)); // display only top 10 // Draw highscores as an inline HUD-like panel (no opaque box), matching Options/Level/Exit style if (useFont) { @@ -899,18 +905,18 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi char rankStr[8]; std::snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1); useFont->draw(renderer, rankX, y + wave + entryOffset, rankStr, curRowScale, rowColor); - useFont->draw(renderer, nameXAdj, y + wave + entryOffset, hs[i].name, curRowScale, rowColor); + useFont->draw(renderer, nameXAdj, y + wave + entryOffset, filtered[i].name, curRowScale, rowColor); - char scoreStr[16]; std::snprintf(scoreStr, sizeof(scoreStr), "%d", hs[i].score); + char scoreStr[16]; std::snprintf(scoreStr, sizeof(scoreStr), "%d", filtered[i].score); useFont->draw(renderer, scoreX, y + wave + entryOffset, scoreStr, curRowScale, rowColor); - char linesStr[8]; std::snprintf(linesStr, sizeof(linesStr), "%d", hs[i].lines); + char linesStr[8]; std::snprintf(linesStr, sizeof(linesStr), "%d", filtered[i].lines); useFont->draw(renderer, linesX, y + wave + entryOffset, linesStr, curRowScale, rowColor); - char levelStr[8]; std::snprintf(levelStr, sizeof(levelStr), "%d", hs[i].level); + char levelStr[8]; std::snprintf(levelStr, sizeof(levelStr), "%d", filtered[i].level); useFont->draw(renderer, levelX, y + wave + entryOffset, levelStr, curRowScale, rowColor); - char timeStr[16]; int mins = int(hs[i].timeSec) / 60; int secs = int(hs[i].timeSec) % 60; + char timeStr[16]; int mins = int(filtered[i].timeSec) / 60; int secs = int(filtered[i].timeSec) % 60; std::snprintf(timeStr, sizeof(timeStr), "%d:%02d", mins, secs); useFont->draw(renderer, timeX, y + wave + entryOffset, timeStr, curRowScale, rowColor); }