tmemory
Version:
A terminal-based Memory card game built with React Ink. Features multiple grid sizes, AI opponent, and high scores.
150 lines (149 loc) • 5.74 kB
JavaScript
import Conf from 'conf';
import React, { createContext, useContext } from 'react';
import { getDeviceId } from '../../utils/device.js';
// Initialize Conf with schema validation
const config = new Conf({
projectName: 'tmemory',
schema: {
scores: {
type: 'object',
additionalProperties: {
type: 'array',
items: {
type: 'object',
properties: {
time: { type: 'number', minimum: 0 },
rows: { type: 'number', minimum: 1, maximum: 12 },
cols: { type: 'number', minimum: 1, maximum: 12 },
gameMode: {
type: 'string',
enum: ['single', 'vs-ai', 'vs-player'],
},
date: { type: 'string', format: 'date-time' },
playerName: { type: 'string', maxLength: 12 },
deviceId: { type: 'string' },
isOnline: { type: 'boolean' },
},
required: ['time', 'rows', 'cols', 'gameMode', 'date'],
additionalProperties: false,
},
default: [],
},
default: {},
},
playerName: {
type: 'string',
default: '',
},
onlineEnabled: {
type: 'boolean',
default: false,
},
},
clearInvalidConfig: true, // This will clear any invalid config data
});
const getHighScoreKey = (grid, mode) => {
return `${grid.rows}x${grid.cols}-${mode}`;
};
const HighScoreContext = createContext(undefined);
export const HighScoreProvider = ({ children, }) => {
const [onlineEnabled, setOnlineEnabled] = React.useState(config.get('onlineEnabled') || false);
const getAllHighScores = () => {
const scores = config.get('scores');
// Handle migration from old format (single score) to new format (array of scores)
const migratedScores = {};
for (const key in scores) {
if (Array.isArray(scores[key])) {
migratedScores[key] = scores[key];
}
else if (scores[key]) {
// Convert single score to array
migratedScores[key] = [scores[key]];
}
else {
migratedScores[key] = [];
}
}
return migratedScores;
};
const getHighScore = (mode, grid) => {
const scores = getAllHighScores();
const key = getHighScoreKey(grid, mode);
if (!scores[key] || scores[key].length === 0) {
return null;
}
// Return the best score (lowest time)
return scores[key].reduce((best, current) => (!best || current.time < best.time ? current : best), null);
};
const saveHighScore = (score) => {
// Ensure the score has a deviceId and playerName
const scoreWithDetails = {
...score,
deviceId: score.deviceId || getDeviceId(),
playerName: score.playerName || getPlayerName() || 'Anonymous',
isOnline: score.isOnline || false,
};
const scores = getAllHighScores();
const key = getHighScoreKey({ rows: score.rows, cols: score.cols }, score.gameMode);
// Initialize array if it doesn't exist
if (!scores[key]) {
scores[key] = [];
}
// Add new score to the array
scores[key].push(scoreWithDetails);
// Sort by time (ascending) and keep only top 10
scores[key] = scores[key].sort((a, b) => a.time - b.time).slice(0, 10);
config.set('scores', scores);
};
const isNewHighScore = (time, grid, mode) => {
const scores = getAllHighScores();
const key = getHighScoreKey(grid, mode);
// If we have fewer than 10 scores, it's a new high score
if (!scores[key] || scores[key].length < 10) {
return true;
}
// Check if this time beats the worst time in the top 10
const worstScore = [...scores[key]].sort((a, b) => a.time - b.time)[9];
return worstScore ? time < worstScore.time : true;
};
// Get local leaderboard for a specific mode and grid size
const getLocalLeaderboard = (mode, grid) => {
const key = getHighScoreKey(grid, mode);
const scores = getAllHighScores();
// Return the array of scores sorted by time
return scores[key] ? [...scores[key]].sort((a, b) => a.time - b.time) : [];
};
// Get the player's name
const getPlayerName = () => {
return config.get('playerName');
};
// Set the player's name
const setPlayerName = (name) => {
config.set('playerName', name);
};
const value = {
getHighScore,
saveHighScore,
isNewHighScore,
// TODO: This really doesn't make sense here but it's where we have our persistent data store...
onlineEnabled,
setOnlineEnabled: (enabled) => {
// Update the config value and the local state
config.set('onlineEnabled', enabled);
setOnlineEnabled(enabled);
},
getAllHighScores,
getLocalLeaderboard,
getPlayerName,
setPlayerName,
getDeviceId,
};
return (React.createElement(HighScoreContext.Provider, { value: value }, children));
};
export const useHighScores = () => {
const context = useContext(HighScoreContext);
if (context === undefined) {
throw new Error('useHighScores must be used within a HighScoreProvider');
}
return context;
};