sudoku leader board fixed

This commit is contained in:
2026-05-15 23:21:31 +08:00
parent f1f906be55
commit 5567bf572f
4 changed files with 109 additions and 17 deletions

View File

@@ -7,6 +7,7 @@ export interface ISudokuRecord extends Document {
username: string;
fullname: string;
difficulty: SudokuDifficulty;
gameMode: 'standard' | 'irregular';
timeSeconds: number;
won: boolean;
createdAt: Date;
@@ -17,6 +18,7 @@ export interface ISudokuLeaderboardRecord {
userId: mongoose.Types.ObjectId;
username: string;
fullname: string;
gameMode: 'standard' | 'irregular';
bestTime: number;
totalGames: number;
wonGames: number;
@@ -43,6 +45,12 @@ const sudokuRecordSchema = new Schema<ISudokuRecord>({
enum: ['easy', 'medium', 'hard'],
required: true
},
gameMode: {
type: String,
enum: ['standard', 'irregular'],
required: true,
default: 'standard'
},
timeSeconds: {
type: Number,
required: true,
@@ -56,25 +64,41 @@ const sudokuRecordSchema = new Schema<ISudokuRecord>({
timestamps: true
});
sudokuRecordSchema.index({ userId: 1, difficulty: 1 });
sudokuRecordSchema.index({ difficulty: 1, won: 1, timeSeconds: 1 });
sudokuRecordSchema.index({ userId: 1, difficulty: 1, gameMode: 1 });
sudokuRecordSchema.index({ difficulty: 1, won: 1, gameMode: 1, timeSeconds: 1 });
sudokuRecordSchema.statics.getLeaderboard = function(
difficulty: SudokuDifficulty,
gameMode: 'standard' | 'irregular' | 'all',
skipCount: number,
pageSize: number
): mongoose.Aggregate<ISudokuLeaderboardRecord[]> {
const match: any = {
difficulty
};
if (gameMode === 'standard') {
match.$or = [
{ gameMode: 'standard' },
{ gameMode: { $exists: false } }
];
} else if (gameMode === 'irregular') {
match.gameMode = 'irregular';
}
return this.aggregate([
{
$match: {
difficulty
}
$match: match
},
{
$group: {
_id: '$userId',
_id: {
userId: '$userId',
gameMode: { $ifNull: ['$gameMode', 'standard'] }
},
username: { $first: '$username' },
fullname: { $first: '$fullname' },
gameMode: { $first: { $ifNull: ['$gameMode', 'standard'] } },
bestTime: {
$min: {
$cond: [{ $eq: ['$won', true] }, '$timeSeconds', null]
@@ -92,7 +116,7 @@ sudokuRecordSchema.statics.getLeaderboard = function(
},
{
$addFields: {
userId: '$_id',
userId: '$_id.userId',
winRate: {
$multiply: [
{ $cond: [{ $eq: ['$totalGames', 0] }, 0, { $divide: ['$wonGames', '$totalGames'] }] },

View File

@@ -51,34 +51,71 @@ router.post('/record', auth, async (req: Request, res: Response) => {
router.get('/leaderboard/:difficulty', async (req: Request, res: Response) => {
try {
const { difficulty } = req.params;
const { page = '1', limit = '10' } = req.query;
const { page = '1', limit = '10', mode = 'all' } = req.query;
const validDifficulties: SudokuDifficulty[] = ['easy', 'medium', 'hard'];
const validModes = ['all', 'standard', 'irregular'];
if (!validDifficulties.includes(difficulty as SudokuDifficulty)) {
return res.status(400).json({ error: '无效的难度级别' });
}
if (!validModes.includes(mode as string)) {
return res.status(400).json({ error: '无效的游戏模式' });
}
const pageNum = parseInt(page as string, 10);
const pageSize = parseInt(limit as string, 10);
const skipCount = (pageNum - 1) * pageSize;
const gameMode = mode as 'standard' | 'irregular' | 'all';
const records = await SudokuRecord.getLeaderboard(
difficulty as SudokuDifficulty,
gameMode,
skipCount,
pageSize
);
const totalUsers = await SudokuRecord.distinct('userId', {
const totalMatch: any = {
difficulty,
won: true
});
};
if (gameMode === 'standard') {
totalMatch.$or = [
{ gameMode: 'standard' },
{ gameMode: { $exists: false } }
];
} else if (gameMode === 'irregular') {
totalMatch.gameMode = 'irregular';
}
const totalCountResult = await SudokuRecord.aggregate([
{
$match: totalMatch
},
{
$group: {
_id: {
userId: '$userId',
gameMode: { $ifNull: ['$gameMode', 'standard'] }
}
}
},
{
$count: 'count'
}
]);
const total = totalCountResult[0]?.count || 0;
res.json({
records,
total: totalUsers.length,
total,
currentPage: pageNum,
totalPages: Math.ceil(totalUsers.length / pageSize),
difficulty
totalPages: Math.ceil(total / pageSize),
difficulty,
mode: gameMode
});
} catch (error) {
console.error('获取数独排行榜失败:', error);

View File

@@ -158,6 +158,7 @@ const SudokuGame: React.FC = () => {
},
body: JSON.stringify({
difficulty,
gameMode,
timeSeconds,
won
})

View File

@@ -19,11 +19,13 @@ import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
import { API_BASE_URL } from '../config';
type Difficulty = 'easy' | 'medium' | 'hard';
type GameMode = 'all' | 'standard' | 'irregular';
interface LeaderboardRecord {
userId: string;
username: string;
fullname: string;
gameMode: 'standard' | 'irregular';
bestTime: number;
totalGames: number;
wonGames: number;
@@ -47,6 +49,7 @@ const DIFFICULTY_LABELS: Record<Difficulty, string> = {
const SudokuLeaderboard: React.FC = () => {
const [difficulty, setDifficulty] = useState<Difficulty>('hard');
const [gameModeFilter, setGameModeFilter] = useState<GameMode>('all');
const [records, setRecords] = useState<LeaderboardRecord[]>([]);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
@@ -55,14 +58,14 @@ const SudokuLeaderboard: React.FC = () => {
useEffect(() => {
fetchLeaderboard();
}, [difficulty, page]);
}, [difficulty, page, gameModeFilter]);
const fetchLeaderboard = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(
`${API_BASE_URL}/api/sudoku/leaderboard/${difficulty}?page=${page}&limit=10`
`${API_BASE_URL}/api/sudoku/leaderboard/${difficulty}?page=${page}&limit=10&mode=${gameModeFilter}`
);
if (!response.ok) {
@@ -85,6 +88,11 @@ const SudokuLeaderboard: React.FC = () => {
setPage(1);
};
const handleGameModeChange = (_event: React.SyntheticEvent, newValue: GameMode) => {
setGameModeFilter(newValue);
setPage(1);
};
const handlePageChange = (_event: React.ChangeEvent<unknown>, value: number) => {
setPage(value);
};
@@ -139,12 +147,27 @@ const SudokuLeaderboard: React.FC = () => {
</Tabs>
</Box>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs
value={gameModeFilter}
onChange={handleGameModeChange}
centered
textColor="primary"
indicatorColor="primary"
>
<Tab value="all" label="全部" />
<Tab value="standard" label="标准" />
<Tab value="irregular" label="不规则" />
</Tabs>
</Box>
<TableContainer component={Paper} sx={{ mb: 3 }}>
<Table>
<TableHead>
<TableRow>
<TableCell align="center" width="80px"></TableCell>
<TableCell></TableCell>
<TableCell align="center"></TableCell>
<TableCell align="center"></TableCell>
<TableCell align="center"></TableCell>
<TableCell align="center"></TableCell>
@@ -155,7 +178,7 @@ const SudokuLeaderboard: React.FC = () => {
<TableBody>
{records.length === 0 ? (
<TableRow>
<TableCell colSpan={7} align="center">
<TableCell colSpan={8} align="center">
<Typography variant="body2" color="textSecondary" py={3}>
</Typography>
@@ -167,7 +190,7 @@ const SudokuLeaderboard: React.FC = () => {
const badge = page === 1 ? getRankBadge(rank) : null;
return (
<TableRow
key={record.userId}
key={`${record.userId}_${record.gameMode}`}
sx={badge ? { backgroundColor: 'rgba(255, 215, 0, 0.1)' } : {}}
>
<TableCell align="center">
@@ -181,6 +204,13 @@ const SudokuLeaderboard: React.FC = () => {
{record.fullname || record.username}
</Typography>
</TableCell>
<TableCell align="center">
<Chip
label={record.gameMode === 'standard' ? '标准' : '不规则'}
color={record.gameMode === 'standard' ? 'primary' : 'secondary'}
size="small"
/>
</TableCell>
<TableCell align="center">
<Chip
label={formatTime(record.bestTime)}