Skip to content

Commit

Permalink
Save run history and add game stats screen (#673)
Browse files Browse the repository at this point in the history
  • Loading branch information
zenzombie authored May 4, 2024
1 parent fe26d96 commit 4a58caf
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 1 deletion.
1 change: 1 addition & 0 deletions changes/game-stats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Players can now view summary statistics of games played by selecting the "Game Stats" option from the "View" menu. Statistics are collected only for games played since the menu option became available.
218 changes: 217 additions & 1 deletion src/brogue/MainMenu.c
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,10 @@ static void initializeFlyoutMenu(buttonState *menu, screenDisplayBuffer *shadowB

} else if (rogue.nextGame == NG_FLYOUT_VIEW) {

buttonCount = 2;
buttonCount = 3;
initializeMainMenuButton(&(buttons[0]), " View %sR%secording ", 'r', 'R', NG_VIEW_RECORDING);
initializeMainMenuButton(&(buttons[1]), " %sH%sigh Scores ", 'h', 'H', NG_HIGH_SCORES);
initializeMainMenuButton(&(buttons[2]), " %sG%same Stats ", 'g', 'G', NG_GAME_STATS);

} else {
return;
Expand Down Expand Up @@ -858,6 +859,217 @@ boolean dialogChooseFile(char *path, const char *suffix, const char *prompt) {
}
}

typedef struct gameStats {
int games;
int escaped;
int mastered;
int won;
float winRate;
int deepestLevel;
int cumulativeLevels;
int highestScore;
unsigned long cumulativeScore;
int mostGold;
unsigned long cumulativeGold;
int mostLumenstones;
int cumulativeLumenstones;
int fewestTurnsWin; // zero means never won
unsigned long cumulativeTurns;
int longestWinStreak;
int longestMasteryStreak;
int currentWinStreak;
int currentMasteryStreak;
} gameStats;

/// @brief Updates the given stats to include a run
/// @param run The run to add
/// @param stats The stats to update
static void addRuntoGameStats(rogueRun *run, gameStats *stats) {

stats->games++;
stats->cumulativeScore += run->score;
stats->cumulativeGold += run->gold;
stats->cumulativeLumenstones += run->lumenstones;
stats->cumulativeLevels += run->deepestLevel;
stats->cumulativeTurns += run->turns;

stats->highestScore = (run->score > stats->highestScore) ? run->score : stats->highestScore;
stats->mostGold = (run->gold > stats->mostGold) ? run->gold : stats->mostGold;
stats->mostLumenstones = (run->lumenstones > stats->mostLumenstones) ? run->lumenstones : stats->mostLumenstones;
stats->deepestLevel = (run->deepestLevel > stats->deepestLevel) ? run->deepestLevel : stats->deepestLevel;

if (strcmp(run->result, "Escaped") == 0 || strcmp(run->result, "Mastered") == 0) {
if (stats->fewestTurnsWin == 0 || run->turns < stats->fewestTurnsWin) {
stats->fewestTurnsWin = run->turns;
}
stats->won++;
stats->currentWinStreak++;
if (strcmp(run->result, "Mastered") == 0) {
stats->currentMasteryStreak++;
stats->mastered++;
} else {
stats->currentMasteryStreak = 0;
stats->escaped++;
}
} else {
stats->currentWinStreak = stats->currentMasteryStreak = 0;
}

if (stats->currentWinStreak > stats->longestWinStreak) {
stats->longestWinStreak = stats->currentWinStreak;
}
if (stats->currentMasteryStreak > stats->longestMasteryStreak) {
stats->longestMasteryStreak = stats->currentMasteryStreak;
}

if (stats->games == 0) {
stats->winRate = 0.0;
} else {
stats->winRate = ((float)stats->won / stats->games) * 100.0;
}
}

/// @brief Display the game stats screen
/// Includes "All Time" stats and "Recent" stats. The player can reset their recent stats at any time.
static void viewGameStats(void) {

gameStats allTimeStats = {0};
gameStats recentStats = {0};

rogueRun *runHistory = loadRunHistory();
rogueRun *run = runHistory;

// calculate stats
while (run != NULL) {
if (run->seed != 0) {
addRuntoGameStats(run, &allTimeStats);
addRuntoGameStats(run, &recentStats);
} else { // when seed == 0 the run entry means the player reset their recent stats at this point
memset(&recentStats, 0, sizeof(gameStats));
}
run = run->nextRun;
}

// free run history
run = runHistory;
rogueRun *next;
while (run != NULL) {
next = run->nextRun;
free(run);
run = next;
}

const SavedDisplayBuffer rbuf = saveDisplayBuffer();
blackOutScreen();

screenDisplayBuffer dbuf;
clearDisplayBuffer(&dbuf);

char buf[COLS*3];
int i = 4;
int offset = 21;
char whiteColorEscape[5] = "", yellowColorEscape[5] = "";
encodeMessageColor(whiteColorEscape, 0, &white);
encodeMessageColor(yellowColorEscape, 0, &itemMessageColor);

color titleColor = black;
applyColorAverage(&titleColor, &itemMessageColor, 100);
printString("-- GAME STATS --", (COLS - 17 + 1) / 2, 0, &titleColor, &black, &dbuf);

sprintf(buf,"%-30s%16s%16s","", "All Time", "Recent");
printString(buf, offset, i, &itemMessageColor, &black, &dbuf);

sprintf(buf,"%-30s%s%16i%16i","Games Played", whiteColorEscape, allTimeStats.games, recentStats.games);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);
i++;
sprintf(buf,"%-30s%s%16i%16i","Won", whiteColorEscape, allTimeStats.won, recentStats.won);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);

sprintf(buf,"%-30s%s%16.1f%16.1f","Win Rate (%)", whiteColorEscape, allTimeStats.winRate, recentStats.winRate);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);

sprintf(buf,"%-30s%s%16i%16i","Escaped", whiteColorEscape, allTimeStats.escaped, recentStats.escaped);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);

sprintf(buf,"%-30s%s%16i%16i","Mastered", whiteColorEscape, allTimeStats.mastered, recentStats.mastered);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);
i++;
sprintf(buf,"%-30s%s%16i%16i","High Score", whiteColorEscape, allTimeStats.highestScore, recentStats.highestScore);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);

sprintf(buf,"%-30s%s%16i%16i","Most Gold", whiteColorEscape, allTimeStats.mostGold, recentStats.mostGold);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);

sprintf(buf,"%-30s%s%16i%16i","Most Lumenstones", whiteColorEscape, allTimeStats.mostLumenstones, recentStats.mostLumenstones);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);
i++;
sprintf(buf,"%-30s%s%16i%16i","Deepest Level", whiteColorEscape, allTimeStats.deepestLevel, recentStats.deepestLevel);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);

char allTimeFewestTurns[20] = "-";
char recentFewestTurns[20] = "-";
if (allTimeStats.fewestTurnsWin > 0) {
sprintf(allTimeFewestTurns, "%i", allTimeStats.fewestTurnsWin);
}
if (recentStats.fewestTurnsWin > 0) {
sprintf(recentFewestTurns, "%i", recentStats.fewestTurnsWin);
}
sprintf(buf,"%-30s%s%16s%16s","Shortest Win (Turns)", whiteColorEscape, allTimeFewestTurns, recentFewestTurns);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);
i++;
sprintf(buf,"%-30s%s%16i%16i","Longest Win Streak", whiteColorEscape, allTimeStats.longestWinStreak, recentStats.longestWinStreak);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);

sprintf(buf,"%-30s%s%16i%16i","Longest Mastery Streak", whiteColorEscape, allTimeStats.longestMasteryStreak, recentStats.longestMasteryStreak);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);
i++;
sprintf(buf,"%-30s%s%32i","Current Win Streak", whiteColorEscape, recentStats.currentWinStreak);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);

sprintf(buf,"%-30s%s%32i","Current Mastery Streak", whiteColorEscape, recentStats.currentMasteryStreak);
printString(buf, offset, ++i, &itemMessageColor, &black, &dbuf);

// Set the dbuf opacity.
for (int i=0; i<COLS; i++) {
for (int j=0; j<ROWS; j++) {
dbuf.cells[i][j].opacity = INTERFACE_OPACITY;
}
}

// Display.
overlayDisplayBuffer(&dbuf);
color continueColor = black;
applyColorAverage(&continueColor, &goodMessageColor, 100);

printString(KEYBOARD_LABELS ? "Press space or click to continue." : "Touch anywhere to continue.",
(COLS - strLenWithoutEscapes(KEYBOARD_LABELS ? "Press space or click to continue." : "Touch anywhere to continue.")) / 2,
ROWS - 1, &continueColor, &black, 0);

commitDraws();

if (recentStats.games > 0) {
brogueButton buttons[1];
initializeButton(&(buttons[0]));
if (KEYBOARD_LABELS) {
sprintf(buttons[0].text, " %sR%seset ", yellowColorEscape, whiteColorEscape);
} else {
strcpy(buttons[0].text, " Reset ");
}
buttons[0].hotkey[0] = 'R';
buttons[0].hotkey[1] = 'r';
buttons[0].x = 74;
buttons[0].y = 25;

if (buttonInputLoop(buttons, 1, 74, 25, 10, 3, NULL) == 0 && confirm("Reset recent stats?",false)) {
saveResetRun();
}
} else {
waitForKeystrokeOrMouseClick();
}

restoreDisplayBuffer(&rbuf);
}

// This is the basic program loop.
// When the program launches, or when a game ends, you end up here.
// If the player has already said what he wants to do next
Expand Down Expand Up @@ -1041,6 +1253,10 @@ void mainBrogueJunction() {
rogue.nextGame = NG_NOTHING;
printHighScores(false);
break;
case NG_GAME_STATS:
rogue.nextGame = NG_NOTHING;
viewGameStats();
break;
case NG_QUIT:
// No need to do anything.
break;
Expand Down
17 changes: 17 additions & 0 deletions src/brogue/Rogue.h
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,19 @@ typedef struct rogueHighScoresEntry {
char description[DCOLS];
} rogueHighScoresEntry;

typedef struct rogueRun {
uint64_t seed;
long dateNumber;
char result[DCOLS];
char killedBy[DCOLS];
int gold;
int lumenstones;
int score;
int turns;
int deepestLevel;
struct rogueRun *nextRun;
} rogueRun;

typedef struct fileEntry {
char *path;
struct tm date;
Expand Down Expand Up @@ -2319,6 +2332,7 @@ enum NGCommands {
NG_OPEN_GAME,
NG_VIEW_RECORDING,
NG_HIGH_SCORES,
NG_GAME_STATS,
NG_QUIT,
};

Expand Down Expand Up @@ -2929,6 +2943,9 @@ extern "C" {
boolean shiftKeyIsDown(void);
short getHighScoresList(rogueHighScoresEntry returnList[HIGH_SCORES_COUNT]);
boolean saveHighScore(rogueHighScoresEntry theEntry);
void saveRunHistory(char *result, char *killedBy, int score, int lumenstones);
void saveResetRun(void);
rogueRun *loadRunHistory(void);
fileEntry *listFiles(short *fileCount, char **dynamicMemoryBuffer);
void initializeLaunchArguments(enum NGCommands *command, char *path, uint64_t *seed);

Expand Down
8 changes: 8 additions & 0 deletions src/brogue/RogueMain.c
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,10 @@ void gameOver(char *killedBy, boolean useCustomPhrasing) {
notifyEvent(GAMEOVER_RECORDING, 0, 0, "recording ended", "none");
}

if (!rogue.playbackMode && !rogue.easyMode && !rogue.wizard) {
saveRunHistory(rogue.quit ? "Quit" : "Died", rogue.quit ? "-" : killedBy, (int) theEntry.score, numGems);
}

rogue.gameHasEnded = true;
rogue.gameExitStatusCode = EXIT_STATUS_SUCCESS;
}
Expand Down Expand Up @@ -1348,6 +1352,10 @@ void victory(boolean superVictory) {
notifyEvent(GAMEOVER_RECORDING, 0, 0, "recording ended", "none");
}

if (!rogue.playbackMode && !rogue.easyMode && !rogue.wizard) {
saveRunHistory(victoryVerb, "-", (int) theEntry.score, gemCount);
}

rogue.gameHasEnded = true;
rogue.gameExitStatusCode = EXIT_STATUS_SUCCESS;
}
Expand Down
82 changes: 82 additions & 0 deletions src/platform/platformdependent.c
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,88 @@ boolean saveHighScore(rogueHighScoresEntry theEntry) {
return true;
}

/// @brief Sets the name of the run history file based on the variant
/// @param buffer The filename
/// @param bufferMaxLength The maximum filename length
static void setRunHistoryFilename(char *buffer, int bufferMaxLength) {
strncpy(buffer, gameConst->variantName, bufferMaxLength);
strncat(buffer, "RunHistory.txt", bufferMaxLength);
buffer[0] = toupper(buffer[0]);
}

/// @brief Saves the run to the history file at the end of a game
/// @param result The game result (Escaped, Mastered, Died, Quit)
/// @param killedBy How the player died (monster name, etc.)
/// @param score The total score
/// @param lumenstones The number of lumenstones collected
void saveRunHistory(char *result, char *killedBy, int score, int lumenstones) {
FILE *runHistoryFile;
char runHistoryFilename[BROGUE_FILENAME_MAX];

setRunHistoryFilename(runHistoryFilename, BROGUE_FILENAME_MAX);
runHistoryFile = fopen(runHistoryFilename, "a"); // append. create if not found.

fprintf(runHistoryFile, "%llu\t%li\t%s\t%s\t%i\t%i\t%i\t%i\t%i\n", rogue.seed, (long) time(NULL), result, killedBy,
score, (int) rogue.gold, lumenstones, (int) rogue.deepestLevel, (int) rogue.playerTurnNumber);
fclose(runHistoryFile);
}
/// @brief Saves a "reset" run to the history file. This serves to reset the player's recent stats to zero.
void saveResetRun(void) {
FILE *runHistoryFile;
char runHistoryFilename[BROGUE_FILENAME_MAX];

setRunHistoryFilename(runHistoryFilename, BROGUE_FILENAME_MAX);
runHistoryFile = fopen(runHistoryFilename, "a"); // append. create if not found.

fprintf(runHistoryFile, "%i\t%li\t%s\t%s\t%i\t%i\t%i\t%i\t%i\n", 0, (long) time(NULL), "Reset", "-", 0, 0, 0, 0, 0);
fclose(runHistoryFile);
}

/// @brief Loads the run history file
/// @return Linked list of runs
rogueRun* loadRunHistory(void) {
FILE *runHistoryFile;
char runHistoryFilename[BROGUE_FILENAME_MAX];

setRunHistoryFilename(runHistoryFilename, BROGUE_FILENAME_MAX);
runHistoryFile = fopen(runHistoryFilename, "r"); // read

if (runHistoryFile == NULL) {
runHistoryFile = fopen(runHistoryFilename, "w"); // create if not found
fclose(runHistoryFile);
runHistoryFile = fopen(runHistoryFilename, "r");
}

rogueRun *runHistory = NULL;
rogueRun *current = NULL;
char line[1024]; // maximum line length
while (fgets(line, sizeof(line), runHistoryFile) != NULL) {
rogueRun *run = (rogueRun *)malloc(sizeof(rogueRun));
memset(run, '\0', sizeof(rogueRun));
run->nextRun = NULL;

int vals = sscanf(line, "%llu\t%li\t%s\t%[^\t]\t%i\t%i\t%i\t%i\t%i\n", &run->seed, &run->dateNumber,
run->result, run->killedBy, &run->score, &run->gold, &run->lumenstones,
&run->deepestLevel, &run->turns);

if ( vals == 9) {
if (runHistory == NULL) {
runHistory = run;
current = run;
} else {
current->nextRun = run;
current = run;
}
} else {
fprintf(stderr, "Error parsing line: %s\n", line);
free(run);
}
}
fclose(runHistoryFile);

return runHistory;
}

// start of file listing

struct filelist {
Expand Down

0 comments on commit 4a58caf

Please sign in to comment.