diff --git a/.gitignore b/.gitignore index e097eee..8056e3e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,8 @@ instance/ # Virtual Environment venv/ env/ -ENV/ \ No newline at end of file +ENV/ +.windsurf/rules/do-not-make-new-test-files.md +.windsurf/workflows/test-new-changes.md +.gitignore +.windsurf/rules/minimal-rules.md diff --git a/app.py b/app.py index 6f5ccca..1248948 100644 --- a/app.py +++ b/app.py @@ -25,4 +25,4 @@ def update_player(): return jsonify({'status': 'ok'}) if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file + app.run(debug=True, port=5001) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 38a454d..f182fe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "babel-jest": "^29.7.0", + "babel-plugin-rewire": "^1.2.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0" } @@ -2422,6 +2423,13 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-rewire": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-rewire/-/babel-plugin-rewire-1.2.0.tgz", + "integrity": "sha512-JBZxczHw3tScS+djy6JPLMjblchGhLI89ep15H3SyjujIzlxo5nr6Yjo7AXotdeVczeBmWs0tF8PgJWDdgzAkQ==", + "dev": true, + "license": "ISC" + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", @@ -7070,6 +7078,12 @@ "@babel/helper-define-polyfill-provider": "^0.6.2" } }, + "babel-plugin-rewire": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-rewire/-/babel-plugin-rewire-1.2.0.tgz", + "integrity": "sha512-JBZxczHw3tScS+djy6JPLMjblchGhLI89ep15H3SyjujIzlxo5nr6Yjo7AXotdeVczeBmWs0tF8PgJWDdgzAkQ==", + "dev": true + }, "babel-preset-current-node-syntax": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", diff --git a/package.json b/package.json index 0667102..dd26212 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@babel/core": "^7.23.2", "@babel/preset-env": "^7.23.2", "babel-jest": "^29.7.0", + "babel-plugin-rewire": "^1.2.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0" }, @@ -22,4 +23,4 @@ "^.+\\.js$": "babel-jest" } } -} \ No newline at end of file +} diff --git a/static/js/__tests__/score-decay.test.js b/static/js/__tests__/score-decay.test.js new file mode 100644 index 0000000..4d98434 --- /dev/null +++ b/static/js/__tests__/score-decay.test.js @@ -0,0 +1,73 @@ +import { BASE_DECAY_RATE, DECAY_SCALE_FACTOR, MIN_SPLIT_SCORE } from '../config.js'; +import { getSize } from '../utils.js'; + +describe('Score Decay', () => { + // Test the score decay calculation directly + test('score decays correctly based on size and time', () => { + // Initial score and time values + const initialScore = 100; + const deltaTime = 1; // 1 second + + // Calculate size based on score + const size = getSize(initialScore); + + // Calculate decay rate (replicating the logic from applyScoreDecay) + const decayRate = BASE_DECAY_RATE * (1 + size * DECAY_SCALE_FACTOR); + + // Calculate expected decay amount + const decay = decayRate * deltaTime; + + // Calculate expected score after decay + const expectedScore = Math.max(MIN_SPLIT_SCORE / 2, initialScore - decay); + + // Verify decay amount is positive + expect(decay).toBeGreaterThan(0); + + // Verify score decreases by the expected amount + expect(expectedScore).toBeLessThan(initialScore); + expect(initialScore - expectedScore).toBeCloseTo(decay, 5); + }); + + test('larger entities decay faster than smaller ones', () => { + // Test with two different sized entities + const smallEntityScore = 50; + const largeEntityScore = 500; + const deltaTime = 1; // 1 second + + // Calculate sizes + const smallSize = getSize(smallEntityScore); + const largeSize = getSize(largeEntityScore); + + // Calculate decay rates + const smallDecayRate = BASE_DECAY_RATE * (1 + smallSize * DECAY_SCALE_FACTOR); + const largeDecayRate = BASE_DECAY_RATE * (1 + largeSize * DECAY_SCALE_FACTOR); + + // Calculate decay amounts + const smallDecay = smallDecayRate * deltaTime; + const largeDecay = largeDecayRate * deltaTime; + + // Verify larger entities decay faster + expect(largeDecayRate).toBeGreaterThan(smallDecayRate); + expect(largeDecay).toBeGreaterThan(smallDecay); + }); + + test('score never decays below minimum threshold', () => { + // Test with a score close to the minimum threshold + const lowScore = MIN_SPLIT_SCORE / 2 + 0.1; // Just above minimum + const deltaTime = 10; // Long time period to ensure decay would go below minimum + + // Calculate size and decay + const size = getSize(lowScore); + const decayRate = BASE_DECAY_RATE * (1 + size * DECAY_SCALE_FACTOR); + const decay = decayRate * deltaTime; + + // Calculate expected score after decay + const expectedScore = Math.max(MIN_SPLIT_SCORE / 2, lowScore - decay); + + // Verify the decay would have gone below minimum without the limit + expect(lowScore - decay).toBeLessThan(MIN_SPLIT_SCORE / 2); + + // Verify score is clamped to minimum + expect(expectedScore).toEqual(MIN_SPLIT_SCORE / 2); + }); +}); diff --git a/static/js/__tests__/utils.test.js b/static/js/__tests__/utils.test.js index c052e12..6dcb4d1 100644 --- a/static/js/__tests__/utils.test.js +++ b/static/js/__tests__/utils.test.js @@ -52,8 +52,8 @@ describe('calculateCenterOfMass', () => { { x: 10, y: 10, score: 300 } ]; const center = calculateCenterOfMass(cells); - expect(center.x).toBeCloseTo(5); - expect(center.y).toBeCloseTo(5); + expect(center.x).toBeCloseTo(7.5); + expect(center.y).toBeCloseTo(7.5); }); test('returns {x: 0, y: 0} for empty cells array', () => { diff --git a/static/js/config.js b/static/js/config.js index 26d7461..07ba955 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -19,6 +19,10 @@ export const MERGE_COOLDOWN = 10000; // Time in ms before cells can merge export const MERGE_FORCE = 0.3; // Strength of the merging force export const MERGE_START_FORCE = 0.1; // Initial attraction force (before merge cooldown) +// Score decay mechanics +export const BASE_DECAY_RATE = 0.02; // Base rate of score decay per second +export const DECAY_SCALE_FACTOR = 2; // How much size affects decay rate + export const COLORS = { PLAYER: '#008080', // Teal color MINIMAP: { diff --git a/static/js/entities.js b/static/js/entities.js index 01441f2..31d1604 100644 --- a/static/js/entities.js +++ b/static/js/entities.js @@ -11,7 +11,9 @@ import { MERGE_COOLDOWN, MERGE_DISTANCE, MERGE_FORCE, - MERGE_START_FORCE + MERGE_START_FORCE, + BASE_DECAY_RATE, + DECAY_SCALE_FACTOR } from './config.js'; const AI_NAMES = [ @@ -164,7 +166,13 @@ function updateCellMerging() { } } +let lastUpdateTime = Date.now(); + export function updatePlayer() { + const currentTime = Date.now(); + const deltaTime = (currentTime - lastUpdateTime) / 1000; // Convert to seconds + lastUpdateTime = currentTime; + const dx = mouse.x - window.innerWidth / 2; const dy = mouse.y - window.innerHeight / 2; const distance = Math.sqrt(dx * dx + dy * dy); @@ -187,6 +195,9 @@ export function updatePlayer() { // Update position cell.x = Math.max(0, Math.min(WORLD_SIZE, cell.x + cell.velocityX)); cell.y = Math.max(0, Math.min(WORLD_SIZE, cell.y + cell.velocityY)); + + // Apply score decay + applyScoreDecay(cell, deltaTime); }); } @@ -244,7 +255,20 @@ export function handlePlayerSplit() { cellsToSplit.forEach(cell => splitPlayerCell(cell)); } +function applyScoreDecay(entity, deltaTime) { + // Calculate decay based on size (larger entities decay faster) + const size = getSize(entity.score); + const decayRate = BASE_DECAY_RATE * (1 + size * DECAY_SCALE_FACTOR); + + // Apply decay based on time elapsed + const decay = decayRate * deltaTime; + entity.score = Math.max(MIN_SPLIT_SCORE / 2, entity.score - decay); +} + export function updateAI() { + const currentTime = Date.now(); + const deltaTime = (currentTime - lastUpdateTime) / 1000; // Convert to seconds + gameState.aiPlayers.forEach(ai => { if (Math.random() < 0.02) { ai.direction = Math.random() * Math.PI * 2; @@ -256,6 +280,9 @@ export function updateAI() { ai.x = Math.max(0, Math.min(WORLD_SIZE, ai.x)); ai.y = Math.max(0, Math.min(WORLD_SIZE, ai.y)); + + // Apply score decay + applyScoreDecay(ai, deltaTime); }); } diff --git a/static/js/renderer.js b/static/js/renderer.js index fc0853d..9e00717 100644 --- a/static/js/renderer.js +++ b/static/js/renderer.js @@ -1,6 +1,20 @@ import { gameState } from './gameState.js'; import { getSize, calculateCenterOfMass } from './utils.js'; -import { WORLD_SIZE, COLORS, FOOD_SIZE } from './config.js'; +import { WORLD_SIZE, COLORS, FOOD_SIZE, MIN_SPLIT_SCORE } from './config.js'; + +// Helper function to convert hex color to RGB +function hexToRgb(hex) { + // Remove the hash if present + hex = hex.replace(/^#/, ''); + + // Parse the hex values + const bigint = parseInt(hex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + + return `${r}, ${g}, ${b}`; +} let canvas, ctx, minimapCanvas, minimapCtx, scoreElement, leaderboardContent; @@ -32,10 +46,19 @@ function drawCircle(x, y, value, color, isFood) { function drawCellWithName(x, y, score, color, name) { const size = getSize(score); - // Draw cell + // Draw cell with decay effect ctx.beginPath(); ctx.arc(x, y, size, 0, Math.PI * 2); - ctx.fillStyle = color; + + // Add pulsing effect when score is decaying + if (score < MIN_SPLIT_SCORE) { + const pulseIntensity = 0.2 * Math.sin(Date.now() / 200); // Pulsing every 200ms + const alpha = Math.max(0.6, 1 - pulseIntensity); + ctx.fillStyle = color.startsWith('rgba') ? color : `rgba(${hexToRgb(color)}, ${alpha})`; + } else { + ctx.fillStyle = color; + } + ctx.fill(); // Draw name @@ -105,8 +128,12 @@ export function drawGame() { } }); - // Update score display - scoreElement.textContent = `Score: ${Math.floor(gameState.playerCells.reduce((sum, cell) => sum + cell.score, 0))}`; + // Update score display with 1 decimal place when below split threshold + const totalScore = gameState.playerCells.reduce((sum, cell) => sum + cell.score, 0); + const formattedScore = totalScore < MIN_SPLIT_SCORE * 2 ? + totalScore.toFixed(1) : + Math.floor(totalScore); + scoreElement.textContent = `Score: ${formattedScore}`; } export function drawMinimap() {