diff --git a/local_debug.py b/local_debug.py new file mode 100644 index 0000000..af3ab48 --- /dev/null +++ b/local_debug.py @@ -0,0 +1,201 @@ + +import time +import json +import base64 + +# Mock DB and objects +class MockCollection: + def __init__(self, name): + self.name = name + self.data = [] + + def find_one(self, query): + for item in self.data: + match = True + for k, v in query.items(): + if item.get(k) != v: + match = False + break + if match: + return item + return None + + def find(self, query, projection=None): + results = [] + for item in self.data: + match = True + for k, v in query.items(): + if item.get(k) != v: + match = False + break + if match: + if projection: + filtered = {k: v for k, v in item.items() if projection.get(k, False)} + if projection.get('_id') is None or projection.get('_id') is True: + filtered['_id'] = item.get('_id') + results.append(filtered) + else: + results.append(item) + return MockCursor(results) + + def insert_one(self, doc): + doc['_id'] = len(self.data) + 1 + print(f"[{self.name}] Inserted: {doc}") + self.data.append(doc) + + def count_documents(self, query): + return len(self.data) + +class MockCursor: + def __init__(self, data): + self.data = data + def sort(self, key, direction): + self.data.sort(key=lambda x: x.get(key, 0), reverse=(direction < 0)) + return self + def limit(self, n): + self.data = self.data[:n] + return self.data + +class MockDB: + def __init__(self): + self.users = MockCollection('users') + self.leaderboards = MockCollection('leaderboards') + +db = MockDB() +db.users.insert_one({'username': 'testuser', 'display_name': 'Test User'}) + +# --- Logic from app.py rewritten for standalone test --- + +def logic_submit(data, username): + print(f"\n--- Submitting {data.get('song_id')} ---") + user = db.users.find_one({'username': username}) + if not user: + return {'error': 'user_not_found'} + + song_id = data.get('song_id') + difficulty = data.get('difficulty') + score_data = data.get('score') + + current_month = time.strftime('%Y-%m', time.gmtime()) + + existing = db.leaderboards.find_one({ + 'song_id': song_id, + 'difficulty': difficulty, + 'username': username, + 'month': current_month + }) + + # Patched Logic from app.py + try: + if isinstance(score_data, str): + score_obj = json.loads(score_data) + else: + score_obj = score_data + + # Check for 'score' first, then 'points' + score_val = score_obj.get('score') + if score_val is None: + score_val = score_obj.get('points') + + score_value = int(score_val or 0) + print(f" Parsed Score Value: {score_value}") + except Exception as e: + print(f"Error parsing score: {e}") + return {'error': 'invalid_score_format'} + + if existing: + print(" Existing record found (simulated update).") + else: + print(" Inserting new record.") + db.leaderboards.insert_one({ + 'song_id': song_id, + 'difficulty': difficulty, + 'username': username, + 'display_name': user['display_name'], + 'score': score_data, + 'score_value': score_value, + 'submitted_at': time.time(), + 'month': current_month + }) + return {'status': 'ok'} + +def logic_get(args): + print(f"\n--- Getting {args.get('song_id')} ---") + song_id = args.get('song_id', None) + difficulty = args.get('difficulty', None) + + if not song_id or not difficulty: + return {'error': 'missing args'} + + try: + song_id = int(song_id) + except (ValueError, TypeError): + pass + + current_month = time.strftime('%Y-%m', time.gmtime()) + + leaderboard = list(db.leaderboards.find({ + 'song_id': song_id, + 'difficulty': difficulty, + 'month': current_month + }, { + '_id': False, + 'username': True, + 'score_value': True + }).sort('score_value', -1).limit(50)) + + return {'status': 'ok', 'leaderboard': leaderboard} + +# --- Test Cases --- + +# Case 1: Hash ID +print("=== TEST CASE 1: Hash ID ===") +logic_submit({ + 'song_id': "hash123", + 'difficulty': 'oni', + 'score': {'score': 1000000} +}, 'testuser') +res1 = logic_get({'song_id': "hash123", 'difficulty': 'oni'}) +if len(res1['leaderboard']) > 0: print("✅ Success") +else: print("❌ Failed") + +# Case 2: Numeric ID +print("\n=== TEST CASE 2: Numeric ID ===") +logic_submit({ + 'song_id': 123, + 'difficulty': 'oni', + 'score': {'score': 2000000} +}, 'testuser') +res2 = logic_get({'song_id': "123", 'difficulty': 'oni'}) +if len(res2['leaderboard']) > 0: print("✅ Success") +else: print("❌ Failed") + +# Case 3: Points Only (Frontend Payload Simulation) +print("\n=== TEST CASE 3: Points Only (Frontend Payload) ===") +# This simulates what the JS 'this.resultsObj' looks like (it has 'points', not 'score') +logic_submit({ + 'song_id': "points_song", + 'difficulty': 'hard', + 'score': {'points': 345678, 'bad': 0, 'good': 100} # No 'score' key +}, 'testuser') + +res3 = logic_get({'song_id': "points_song", 'difficulty': 'hard'}) +passed = False +if len(res3.get('leaderboard', [])) > 0: + val = res3['leaderboard'][0].get('score_value') + if val == 345678: + print(f"✅ SUCCESS: Correctly parsed 'points' -> {val}") + passed = True + else: + print(f"❌ FAILED: Score value mismatch. Expected 345678, got {val}") +else: + print("❌ FAILED: No record found") + +if passed: + print("\n---------------------------------------------------") + print("🎉 ALL LOCAL TESTS PASSED. BACKEND LOGIC VERIFIED.") + print("---------------------------------------------------") +else: + print("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + print("🛑 TESTS FAILED. CHECK LOGS.") + print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") diff --git a/public/src/js/leaderboard.js b/public/src/js/leaderboard.js index ab62e74..c71aa7c 100644 --- a/public/src/js/leaderboard.js +++ b/public/src/js/leaderboard.js @@ -380,7 +380,8 @@ class Leaderboard { this.ctx.fillText(displayName, modalX + 140, y + 2) // Score with formatting - var score = entry.score && entry.score.score ? entry.score.score : 0 + // Score with formatting + var score = entry.score_value !== undefined ? entry.score_value : (entry.score && entry.score.score ? entry.score.score : 0) this.ctx.fillStyle = rank <= 3 ? "#1a1a1a" : "#555555" this.ctx.font = (rank <= 3 ? "bold " : "") + "20px " + (strings.font || "sans-serif") this.ctx.textAlign = "right" diff --git a/public/src/js/scoresheet.js b/public/src/js/scoresheet.js index f0f7a8a..b9b54df 100644 --- a/public/src/js/scoresheet.js +++ b/public/src/js/scoresheet.js @@ -235,7 +235,7 @@ class Scoresheet { this.leaderboardData = { songId: songId, difficulty: results.difficulty, - scoreObj: Object.assign({}, results) + scoreObj: Object.assign({ score: results.points }, results) } // Clean up scoreObj if (this.leaderboardData.scoreObj) { @@ -1025,7 +1025,7 @@ class Scoresheet { this.leaderboardData = { songId: songId, difficulty: difficulty, - scoreObj: Object.assign({}, this.resultsObj) + scoreObj: Object.assign({ score: this.resultsObj.points }, this.resultsObj) } this.leaderboardSubmitted = false diff --git a/reproduce_issue.py b/reproduce_issue.py new file mode 100644 index 0000000..ec57b79 --- /dev/null +++ b/reproduce_issue.py @@ -0,0 +1,133 @@ + +import requests +import json +import base64 + +BASE_URL = "http://localhost:34801" + +def register_user(username, password): + url = f"{BASE_URL}/api/register" + data = {"username": username, "password": password} + try: + response = requests.post(url, json=data) + if response.status_code == 200: + return response.json() + print(f"Register failed: {response.text}") + except Exception as e: + print(f"Register error: {e}") + return None + +def login_user(username, password): + url = f"{BASE_URL}/api/login" + data = {"username": username, "password": password} + session = requests.Session() + try: + response = session.post(url, json=data) + if response.status_code == 200: + return session + print(f"Login failed: {response.text}") + except Exception as e: + print(f"Login error: {e}") + return None + +def get_csrf_token(session): + url = f"{BASE_URL}/api/csrftoken" + response = session.get(url) + if response.status_code == 200: + return response.json()['token'] + return None + +def submit_score(session, song_id, difficulty, score_obj, csrf_token): + url = f"{BASE_URL}/api/leaderboard/submit" + headers = {"X-CSRFToken": csrf_token} + data = { + "song_id": song_id, + "difficulty": difficulty, + "score": score_obj + } + response = session.post(url, json=data, headers=headers) + return response.json() + +def get_leaderboard(session, song_id, difficulty): + url = f"{BASE_URL}/api/leaderboard/get" + params = {"song_id": song_id, "difficulty": difficulty} + response = session.get(url, params=params) + return response.json() + +def main(): + username = "debug_user_1" + password = "password123" + + print("1. Registering/Logging in...") + # Try login first + session = login_user(username, password) + if not session: + # Register + reg = register_user(username, password) + if reg: + session = login_user(username, password) + else: + print("Could not register or login") + return + + print("2. Getting CSRF token...") + csrf_token = get_csrf_token(session) + if not csrf_token: + print("Could not get CSRF token") + return + + song_id = 1 # Assuming song 1 exists + difficulty = "oni" + + # Simulate scoresheet.js submission (points only) + score_obj = { + "points": 123456, + "good": 100, + "ok": 10, + "bad": 0, + "maxCombo": 110, + "drumroll": 5, + "crown": "gold" + } + + print(f"3. Submitting score: {score_obj['points']}") + submit_res = submit_score(session, song_id, difficulty, score_obj, csrf_token) + print(f"Submit response: {submit_res}") + + print("4. Fetching leaderboard...") + leaderboard_res = get_leaderboard(session, song_id, difficulty) + + if leaderboard_res['status'] == 'ok': + leaderboard = leaderboard_res['leaderboard'] + found = False + for entry in leaderboard: + if entry['username'] == username: + found = True + print(f"Found entry: {entry}") + print(f"entry.score_value: {entry.get('score_value')}") + print(f"entry.score: {entry.get('score')}") + + score_value = entry.get('score_value') + score_data = entry.get('score') + + # Check what leaderboard.js would see + if score_data and 'score' in score_data: + print(f"JS would assume score: {score_data['score']}") + else: + print("JS would assume score: 0 (undefined)") + + if score_value == 123456: + print("SUCCESS: score_value is correct.") + else: + print(f"FAILURE: score_value is incorrect (expected 123456, got {score_value})") + + if score_data and score_data.get('points') == 123456: + print("SUCCESS: points preserved in score object.") + + if not found: + print("Entry not found in leaderboard (maybe not top 50?)") + else: + print(f"Failed to get leaderboard: {leaderboard_res}") + +if __name__ == "__main__": + main() diff --git a/test_frontend_logic.js b/test_frontend_logic.js new file mode 100644 index 0000000..024c431 --- /dev/null +++ b/test_frontend_logic.js @@ -0,0 +1,32 @@ + +// Simulation of song objects +const songs = [ + { name: "Server Song", id: 123, hash: "hash123" }, + { name: "Custom Song (No ID)", id: undefined, hash: "hashCustom" }, + { name: "Custom Song (Null ID)", id: null, hash: "hashNull" }, + { name: "Edge Case (ID 0)", id: 0, hash: "hashZero" } // Checking if behavior is acceptable +]; + +console.log("=== Frontend Logic Test: Song ID Fallback ==="); + +songs.forEach(song => { + // Logic from scoresheet.js / songselect.js + const songId = song.id || song.hash; + + console.log(`Song: ${song.name}`); + console.log(` Raw ID: ${song.id}, Hash: ${song.hash}`); + console.log(` Resolved songId: ${songId}`); + + if (song.id && songId === song.id) { + console.log(" ✅ Correctly used ID"); + } else if (!song.id && songId === song.hash) { + console.log(" ✅ Correctly fell back to Hash"); + } else if (song.id === 0 && songId === song.hash) { + console.log(" ⚠️ ID was 0, used Hash. (Acceptable if ID > 0 always)"); + } else { + console.log(" ❌ FAILED logic mismatch"); + } + console.log("---"); +}); + +console.log("Tests Completed.");