A Deno-powered backend service for Plants vs. Zombies: MODDED. [Read-only GitHub mirror] docs.pvzm.net
express typescript expressjs plant deno jspvz pvzm game online backend plants-vs-zombies zombie javascript plants modded vs plantsvszombies openapi pvz noads
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update version badge to v0.3.0 and enhance featured sorting logic with diversity penalties

Clay 13fb8745 d994ca32

+65 -5
+1 -1
README.md
··· 1 - # PVZM Backend ![v0.2.2](https://img.shields.io/badge/version-v0.2.2-darklime) 1 + # PVZM Backend ![v0.3.0](https://img.shields.io/badge/version-v0.2.2-darklime) 2 2 3 3 > A Deno-powered backend service for [Plants vs. Zombies: MODDED](https://github.com/roblnet13/pvz). This service provides APIs for uploading, downloading, listing, favoriting, and reporting user-created _I, Zombie_ levels. 4 4
+64 -4
modules/routes/levels.ts
··· 293 293 const orderDirection = reversedOrder ? "ASC" : "DESC"; 294 294 295 295 let orderClause: string; 296 + let useDiversitySort = false; 296 297 if (sort === "featured") { 297 298 // Check if database has mature engagement data (any level with 100+ plays) 298 299 const maxPlaysResult = dbCtx.db.prepare("SELECT MAX(plays) as max_plays FROM levels").get() as { max_plays: number } | undefined; ··· 302 303 if (hasMatureData) { 303 304 // Balanced approach: recency + quality 304 305 // Recency weight: 1 point per day since epoch, quality: favorites * 100 + plays 305 - orderClause = `featured DESC, (created_at / 86400.0 + favorites * 100 + plays) DESC`; 306 + orderClause = `(created_at / 86400.0 + favorites * 100 + plays) DESC`; 306 307 } else { 307 308 // New database: heavily favor recency with minimal quality impact 308 309 // Recency weight: 1 point per day, quality: favorites * 10 + plays / 10 309 310 // This makes recency ~100x more important than in the mature formula 310 - orderClause = `featured DESC, (created_at / 86400.0 + favorites * 10 + plays / 10.0) DESC`; 311 + orderClause = `(created_at / 86400.0 + favorites * 10 + plays / 10.0) DESC`; 311 312 } 313 + useDiversitySort = true; 312 314 } else { 313 315 const orderColumn = sort === "recent" ? "created_at" : sort === "favorites" ? "favorites" : "plays"; 314 316 orderClause = `${orderColumn} ${orderDirection}, id ${orderDirection}`; ··· 338 340 params.push(version); 339 341 } 340 342 343 + // Featured sort only shows featured levels 344 + if (tokenLevelId === null && sort === "featured") { 345 + filters.push("featured = ?"); 346 + params.push(1); 347 + } 348 + 341 349 let query = `SELECT id, name, author, created_at, sun, is_water, favorites, plays, difficulty, version, featured, featured_at FROM levels`; 342 350 343 351 if (filters.length > 0) { 344 352 query += " WHERE " + filters.join(" AND "); 345 353 } 354 + 355 + // For featured sort with diversity, fetch more results to allow for re-ranking 356 + const shouldApplyDiversity = useDiversitySort && tokenLevelId === null; 357 + const fetchLimit = shouldApplyDiversity ? limit * 3 : limit; 358 + const fetchOffset = shouldApplyDiversity ? offset : offset; 346 359 347 360 query += ` ORDER BY ${orderClause} LIMIT ? OFFSET ?`; 348 - params.push(limit, offset); 361 + params.push(fetchLimit, fetchOffset); 362 + 363 + let levels = dbCtx.db.prepare(query).all(...params); 349 364 350 - const levels = dbCtx.db.prepare(query).all(...params); 365 + // Apply author diversity algorithm for featured sort 366 + if (shouldApplyDiversity && Array.isArray(levels) && levels.length > 0) { 367 + type LevelWithScore = { 368 + id: number; 369 + author: string; 370 + created_at: number; 371 + favorites: number; 372 + plays: number; 373 + featured: number; 374 + score: number; 375 + [key: string]: unknown; 376 + }; 377 + 378 + const authorCounts = new Map<string, number>(); 379 + const maxPlaysResult = dbCtx.db.prepare("SELECT MAX(plays) as max_plays FROM levels").get() as { max_plays: number } | undefined; 380 + const hasMatureData = (maxPlaysResult?.max_plays ?? 0) >= 100; 381 + 382 + // Calculate scores and apply diversity penalties 383 + const levelsWithScores = (levels as LevelWithScore[]).map(level => { 384 + // Calculate base score (same formula as SQL) 385 + let baseScore: number; 386 + if (hasMatureData) { 387 + baseScore = level.created_at / 86400.0 + level.favorites * 100 + level.plays; 388 + } else { 389 + baseScore = level.created_at / 86400.0 + level.favorites * 10 + level.plays / 10.0; 390 + } 391 + 392 + // Apply diversity penalty 393 + const authorCount = authorCounts.get(level.author) || 0; 394 + authorCounts.set(level.author, authorCount + 1); 395 + 396 + // Penalty increases exponentially: 0, -500, -1500, -3500, -7500... 397 + const diversityPenalty = authorCount === 0 ? 0 : -500 * (Math.pow(2, authorCount) - 1); 398 + 399 + return { 400 + ...level, 401 + score: baseScore + diversityPenalty 402 + }; 403 + }); 404 + 405 + // Re-sort by adjusted scores 406 + levelsWithScores.sort((a, b) => b.score - a.score); 407 + 408 + // Take only the requested limit 409 + levels = levelsWithScores.slice(0, limit); 410 + } 351 411 352 412 type LevelRow = { 353 413 id: number;