highscore fixes

This commit is contained in:
2025-12-21 19:45:20 +01:00
parent 0b99911f5d
commit 50c869536d
7 changed files with 155 additions and 29 deletions

View File

@ -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,13 +868,44 @@ 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 (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) {
@ -883,6 +916,7 @@ void TetrisApp::Impl::runLoop()
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) {
if (game->getMode() == GameMode::Challenge) {
@ -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});

View File

@ -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];
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->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});
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});

View File

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

View File

@ -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<float>(lw) * 0.5f;
float valueX = centerX - static_cast<float>(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(

View File

@ -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<std::string>();
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<std::string>();
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

View File

@ -3,14 +3,16 @@
#include <vector>
#include <string>
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<ScoreEntry>& all() const { return scores; }
private:

View File

@ -813,7 +813,13 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
}
static const std::vector<ScoreEntry> 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<ScoreEntry> 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);
}