From d41c9b000a0f77ea04779a560a059b615e85f2cb Mon Sep 17 00:00:00 2001 From: Abhay Aggarwal Date: Tue, 13 May 2025 11:59:17 -0700 Subject: [PATCH 1/4] good --- .windsurf/rules/code-style-guide.md | 26 +++++++++++++++++++++ .windsurf/workflows/debug-issues-generic.md | 4 ++++ 2 files changed, 30 insertions(+) create mode 100644 .windsurf/rules/code-style-guide.md create mode 100644 .windsurf/workflows/debug-issues-generic.md diff --git a/.windsurf/rules/code-style-guide.md b/.windsurf/rules/code-style-guide.md new file mode 100644 index 0000000..a532d4d --- /dev/null +++ b/.windsurf/rules/code-style-guide.md @@ -0,0 +1,26 @@ +--- +trigger: model_decision +--- + +Indentation: Use 4 spaces per indentation level. Avoid tabs. + +Line Length: Limit lines to a maximum of 79 characters. For docstrings and comments, limit to 72 characters. + +Blank Lines: Separate top-level function and class definitions with two blank lines. Use a single blank line between method definitions inside a class and to separate logical sections within functions. + +Naming Conventions: +Use lowercase_with_underscores for functions and variables. +Use CamelCase for classes. +Use ALL_CAPS for constants. +Prefix internal attributes with a single underscore (_). + +Comments: Write clear, concise comments to explain code functionality. Update comments when code changes. + +Spaces: Use spaces around operators and after commas. +Trailing Commas: Use trailing commas in lists, tuples, and dictionaries. + +Avoid Extraneous Whitespace: Do not add unnecessary spaces at the beginning or end of lines. + +Imports: Group imports in the following order: standard library imports, related third-party imports, local application/library specific imports. Put a blank line between each group of imports. + +Avoid shadowing names: Do not use names of built-in functions, constants, types, and exceptions. \ No newline at end of file diff --git a/.windsurf/workflows/debug-issues-generic.md b/.windsurf/workflows/debug-issues-generic.md new file mode 100644 index 0000000..fdc1622 --- /dev/null +++ b/.windsurf/workflows/debug-issues-generic.md @@ -0,0 +1,4 @@ +--- +description: +--- + From 32010bca27e2bf6d51aba8714b2949a7dbf99416 Mon Sep 17 00:00:00 2001 From: Abhay Aggarwal Date: Thu, 15 May 2025 17:27:27 -0700 Subject: [PATCH 2/4] CHanges for decay feature --- static/js/__tests__/ai-movement.test.js | 112 ++++++++++++++++++ static/js/__tests__/cell-merging.test.js | 129 +++++++++++++++++++++ static/js/__tests__/decay.test.js | 87 ++++++++++++++ static/js/__tests__/respawn.test.js | 88 ++++++++++++++ static/js/__tests__/spawn-location.test.js | 82 +++++++++++++ static/js/__tests__/utils.test.js | 37 +++++- static/js/config.js | 6 + static/js/entities.js | 2 +- static/js/game.js | 27 +++++ static/js/utils.js | 17 ++- 10 files changed, 582 insertions(+), 5 deletions(-) create mode 100644 static/js/__tests__/ai-movement.test.js create mode 100644 static/js/__tests__/cell-merging.test.js create mode 100644 static/js/__tests__/decay.test.js create mode 100644 static/js/__tests__/respawn.test.js create mode 100644 static/js/__tests__/spawn-location.test.js diff --git a/static/js/__tests__/ai-movement.test.js b/static/js/__tests__/ai-movement.test.js new file mode 100644 index 0000000..6c70d6e --- /dev/null +++ b/static/js/__tests__/ai-movement.test.js @@ -0,0 +1,112 @@ +import { updateAI } from '../entities.js'; +import { gameState } from '../gameState.js'; +import { WORLD_SIZE } from '../config.js'; + +// Mock gameState +jest.mock('../gameState.js', () => ({ + gameState: { + aiPlayers: [] + } +})); + +describe('updateAI', () => { + beforeEach(() => { + // Reset gameState before each test + gameState.aiPlayers = []; + + // Mock Math.random to make tests deterministic + jest.spyOn(Math, 'random').mockImplementation(() => 0.5); + }); + + afterEach(() => { + // Restore Math.random + jest.restoreAllMocks(); + }); + + test('updates AI position based on direction', () => { + const ai = { + x: 100, + y: 100, + score: 100, + direction: Math.PI / 4 // 45 degrees + }; + + gameState.aiPlayers = [ai]; + + updateAI(); + + // AI should have moved in the direction of its angle + expect(gameState.aiPlayers[0].x).toBeGreaterThan(100); + expect(gameState.aiPlayers[0].y).toBeGreaterThan(100); + }); + + test('AI changes direction randomly', () => { + const ai = { + x: 100, + y: 100, + score: 100, + direction: 0 + }; + + gameState.aiPlayers = [ai]; + + // Mock Math.random to return a value that will trigger direction change + jest.spyOn(Math, 'random') + .mockImplementationOnce(() => 0.01) // Less than 0.02 to trigger direction change + .mockImplementationOnce(() => 0.5); // For the new direction calculation + + updateAI(); + + // Direction should have changed + expect(gameState.aiPlayers[0].direction).toBeCloseTo(Math.PI); + }); + + test('AI speed is inversely proportional to size', () => { + const smallAI = { + x: 100, + y: 100, + score: 100, + direction: 0 // Moving right + }; + + const largeAI = { + x: 100, + y: 100, + score: 400, + direction: 0 // Moving right + }; + + // Test small AI first + gameState.aiPlayers = [smallAI]; + updateAI(); + const smallAISpeed = gameState.aiPlayers[0].x - 100; + + // Test large AI + gameState.aiPlayers = [largeAI]; + updateAI(); + const largeAISpeed = gameState.aiPlayers[0].x - 100; + + // Small AI should move faster than large AI + expect(smallAISpeed).toBeGreaterThan(largeAISpeed); + }); + + test('AI stays within world boundaries', () => { + // Test AI at edge of world + const edgeAI = { + x: WORLD_SIZE - 1, + y: WORLD_SIZE - 1, + score: 100, + direction: Math.PI / 4 // 45 degrees, moving toward edge + }; + + gameState.aiPlayers = [edgeAI]; + + updateAI(); + + // AI should be constrained to world boundaries + expect(gameState.aiPlayers[0].x).toBeLessThanOrEqual(WORLD_SIZE); + expect(gameState.aiPlayers[0].y).toBeLessThanOrEqual(WORLD_SIZE); + expect(gameState.aiPlayers[0].x).toBeGreaterThanOrEqual(0); + expect(gameState.aiPlayers[0].y).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/static/js/__tests__/cell-merging.test.js b/static/js/__tests__/cell-merging.test.js new file mode 100644 index 0000000..4abc638 --- /dev/null +++ b/static/js/__tests__/cell-merging.test.js @@ -0,0 +1,129 @@ +import { updateCellMerging } from '../entities.js'; +import { gameState } from '../gameState.js'; +import { MERGE_COOLDOWN } from '../config.js'; + +// Mock gameState +jest.mock('../gameState.js', () => ({ + gameState: { + playerCells: [] + } +})); + +describe('updateCellMerging', () => { + beforeEach(() => { + // Reset gameState before each test + gameState.playerCells = []; + // Mock Date.now to control time + jest.spyOn(Date, 'now').mockImplementation(() => 1000); + }); + + afterEach(() => { + // Restore Date.now + jest.restoreAllMocks(); + }); + + test('cells do not merge when cooldown has not passed', () => { + // Create two cells that are close enough to merge but with recent split time + const cell1 = { + x: 100, y: 100, score: 100, + velocityX: 0, velocityY: 0, + splitTime: Date.now() - (MERGE_COOLDOWN / 2) + }; + const cell2 = { + x: 101, y: 101, score: 100, + velocityX: 0, velocityY: 0, + splitTime: Date.now() - (MERGE_COOLDOWN / 2) + }; + + gameState.playerCells = [cell1, cell2]; + + updateCellMerging(); + + // Cells should not merge, still have two cells + expect(gameState.playerCells.length).toBe(2); + }); + + test('cells merge when cooldown has passed and they are close enough', () => { + // Mock Date.now to return a time after the cooldown + jest.spyOn(Date, 'now').mockImplementation(() => MERGE_COOLDOWN + 2000); + + // Create two cells that are close enough to merge with old split time + const cell1 = { + x: 100, y: 100, score: 100, + velocityX: 0, velocityY: 0, + splitTime: 0 // Old split time + }; + const cell2 = { + x: 101, y: 101, score: 100, + velocityX: 0, velocityY: 0, + splitTime: 0 // Old split time + }; + + gameState.playerCells = [cell1, cell2]; + + updateCellMerging(); + + // Cells should merge into one + expect(gameState.playerCells.length).toBe(1); + // The merged cell should have the combined score + expect(gameState.playerCells[0].score).toBe(200); + }); + + test('cells attract each other when close but not close enough to merge', () => { + // Mock Date.now to return a time after the cooldown + jest.spyOn(Date, 'now').mockImplementation(() => MERGE_COOLDOWN + 2000); + + // Create two cells that are not close enough to merge immediately + const cell1 = { + x: 100, y: 100, score: 100, + velocityX: 0, velocityY: 0, + splitTime: 0 // Old split time + }; + const cell2 = { + x: 200, y: 200, score: 100, // Increased distance to ensure no merging + velocityX: 0, velocityY: 0, + splitTime: 0 // Old split time + }; + + gameState.playerCells = [cell1, cell2]; + + updateCellMerging(); + + // Cells should not merge yet, still have two cells + expect(gameState.playerCells.length).toBe(2); + // But they should have velocities towards each other + expect(gameState.playerCells[0].velocityX).toBeGreaterThan(0); + expect(gameState.playerCells[0].velocityY).toBeGreaterThan(0); + expect(gameState.playerCells[1].velocityX).toBeLessThan(0); + expect(gameState.playerCells[1].velocityY).toBeLessThan(0); + }); + + test('cells repel each other when too close', () => { + // Create two cells that are too close and should repel + const cell1 = { + x: 100, y: 100, score: 400, // Larger size + velocityX: 0, velocityY: 0, + splitTime: 0 + }; + const cell2 = { + x: 100.1, y: 100.1, score: 400, // Larger size + velocityX: 0, velocityY: 0, + splitTime: 0 + }; + + gameState.playerCells = [cell1, cell2]; + + // Mock Date.now to return a time before the cooldown + jest.spyOn(Date, 'now').mockImplementation(() => 1000); + + updateCellMerging(); + + // Cells should not merge, still have two cells + expect(gameState.playerCells.length).toBe(2); + // They should have velocities pushing away from each other + expect(gameState.playerCells[0].velocityX).toBeLessThan(0); + expect(gameState.playerCells[0].velocityY).toBeLessThan(0); + expect(gameState.playerCells[1].velocityX).toBeGreaterThan(0); + expect(gameState.playerCells[1].velocityY).toBeGreaterThan(0); + }); +}); diff --git a/static/js/__tests__/decay.test.js b/static/js/__tests__/decay.test.js new file mode 100644 index 0000000..d8e75ab --- /dev/null +++ b/static/js/__tests__/decay.test.js @@ -0,0 +1,87 @@ +import { applyDecay } from '../utils.js'; +import { DECAY_ENABLED, DECAY_RATE, DECAY_THRESHOLD } from '../config.js'; + +describe('decay mechanics', () => { + // Save original config values to restore after tests + const originalDecayEnabled = DECAY_ENABLED; + const originalDecayRate = DECAY_RATE; + const originalDecayThreshold = DECAY_THRESHOLD; + + // Mock the config values for testing + beforeEach(() => { + // Ensure decay is enabled for tests + global.DECAY_ENABLED = true; + global.DECAY_RATE = 0.05; + global.DECAY_THRESHOLD = 50; + }); + + // Restore original values after tests + afterEach(() => { + global.DECAY_ENABLED = originalDecayEnabled; + global.DECAY_RATE = originalDecayRate; + global.DECAY_THRESHOLD = originalDecayThreshold; + }); + + test('should not decay below threshold', () => { + const entity = { score: DECAY_THRESHOLD }; + applyDecay(entity, 1000); + expect(entity.score).toBe(DECAY_THRESHOLD); + }); + + test('should decay proportionally to time passed', () => { + const entity = { score: 100 }; + const expectedDecayFactor = Math.sqrt(entity.score) / 10; + const expectedDecayAmount = DECAY_RATE * expectedDecayFactor * 1; // 1 second + + applyDecay(entity, 1000); // 1 second + + expect(entity.score).toBeCloseTo(100 - expectedDecayAmount, 5); + }); + + test('should decay more for higher scores', () => { + const smallEntity = { score: 100 }; + const largeEntity = { score: 400 }; + + // Apply same time decay to both + applyDecay(smallEntity, 1000); + applyDecay(largeEntity, 1000); + + // Calculate expected decay + const smallDecayFactor = Math.sqrt(100) / 10; + const largeDecayFactor = Math.sqrt(400) / 10; + + // Verify larger entity decayed more + expect(largeDecayFactor).toBeGreaterThan(smallDecayFactor); + expect(100 - smallEntity.score).toBeLessThan(400 - largeEntity.score); + }); + + test('should not decay when disabled', () => { + // Mock the DECAY_ENABLED value directly in the module + const originalDecayEnabled = DECAY_ENABLED; + Object.defineProperty(global, 'DECAY_ENABLED', { + value: false, + writable: true + }); + + const entity = { score: 100 }; + applyDecay(entity, 1000); + + // Restore original value + Object.defineProperty(global, 'DECAY_ENABLED', { + value: originalDecayEnabled, + writable: true + }); + + expect(entity.score).toBe(100); + }); + + test('should handle very large time deltas gracefully', () => { + const entity = { score: 200 }; + + // Apply a very large time delta (10 seconds) + applyDecay(entity, 10000); + + // Should not go below threshold + expect(entity.score).toBeGreaterThanOrEqual(DECAY_THRESHOLD); + }); +}); diff --git a/static/js/__tests__/respawn.test.js b/static/js/__tests__/respawn.test.js new file mode 100644 index 0000000..16d53e9 --- /dev/null +++ b/static/js/__tests__/respawn.test.js @@ -0,0 +1,88 @@ +import { respawnEntities } from '../collisions.js'; +import { gameState } from '../gameState.js'; +import { FOOD_COUNT, AI_COUNT, STARTING_SCORE } from '../config.js'; +import { respawnAI } from '../entities.js'; + +// Mock dependencies +jest.mock('../gameState.js', () => ({ + gameState: { + food: [], + aiPlayers: [], + playerCells: [] + } +})); + +jest.mock('../entities.js', () => ({ + respawnAI: jest.fn().mockReturnValue({ + x: 0, + y: 0, + score: 50, + color: 'hsl(0, 70%, 50%)', + direction: 0, + name: 'MockAI' + }) +})); + +jest.mock('../utils.js', () => ({ + getRandomPosition: jest.fn().mockReturnValue({ x: 100, y: 100 }), + findSafeSpawnLocation: jest.fn().mockReturnValue({ x: 200, y: 200 }), + getSize: jest.fn().mockReturnValue(30), + getDistance: jest.fn().mockReturnValue(50) +})); + +describe('respawnEntities', () => { + beforeEach(() => { + // Reset gameState before each test + gameState.food = []; + gameState.aiPlayers = []; + gameState.playerCells = []; + + // Reset mock call counts + jest.clearAllMocks(); + }); + + test('respawns food to reach target count', () => { + // Start with some food + gameState.food = Array(FOOD_COUNT / 2).fill({ x: 0, y: 0, color: 'red' }); + + respawnEntities(); + + // Should have added food to reach FOOD_COUNT + expect(gameState.food.length).toBe(FOOD_COUNT); + }); + + test('respawns AI players to reach target count', () => { + // Start with some AI players + gameState.aiPlayers = Array(AI_COUNT / 2).fill({ x: 0, y: 0, score: 50 }); + + respawnEntities(); + + // Should have added AI players to reach AI_COUNT + expect(gameState.aiPlayers.length).toBe(AI_COUNT); + // Should have called respawnAI for each new AI + expect(respawnAI).toHaveBeenCalledTimes(AI_COUNT / 2); + }); + + test('respawns player if no cells exist', () => { + // Start with no player cells + gameState.playerCells = []; + + respawnEntities(); + + // Should have added a player cell + expect(gameState.playerCells.length).toBe(1); + expect(gameState.playerCells[0]).toHaveProperty('score', STARTING_SCORE); + expect(gameState.playerCells[0]).toHaveProperty('velocityX', 0); + expect(gameState.playerCells[0]).toHaveProperty('velocityY', 0); + }); + + test('does not respawn player if cells already exist', () => { + // Start with one player cell + gameState.playerCells = [{ x: 0, y: 0, score: STARTING_SCORE }]; + + respawnEntities(); + + // Should still have just one player cell + expect(gameState.playerCells.length).toBe(1); + }); +}); diff --git a/static/js/__tests__/spawn-location.test.js b/static/js/__tests__/spawn-location.test.js new file mode 100644 index 0000000..3b17142 --- /dev/null +++ b/static/js/__tests__/spawn-location.test.js @@ -0,0 +1,82 @@ +import { findSafeSpawnLocation } from '../utils.js'; +import { getSize } from '../utils.js'; + +describe('findSafeSpawnLocation', () => { + test('returns a safe position when there are no entities', () => { + const mockGameState = { + aiPlayers: [], + playerCells: [] + }; + + const position = findSafeSpawnLocation(mockGameState); + + expect(position).toHaveProperty('x'); + expect(position).toHaveProperty('y'); + expect(position.x).toBeGreaterThanOrEqual(0); + expect(position.y).toBeGreaterThanOrEqual(0); + }); + + test('finds safe position when there are entities but space available', () => { + const mockGameState = { + aiPlayers: [ + { x: 100, y: 100, score: 100 }, + { x: 500, y: 500, score: 200 } + ], + playerCells: [ + { x: 1500, y: 1500, score: 300 } + ] + }; + + const position = findSafeSpawnLocation(mockGameState); + + expect(position).toHaveProperty('x'); + expect(position).toHaveProperty('y'); + + // Check that position is not too close to any entity + const isSafe = [...mockGameState.aiPlayers, ...mockGameState.playerCells].every(entity => { + const dx = position.x - entity.x; + const dy = position.y - entity.y; + const distance = Math.sqrt(dx * dx + dy * dy); + const safeDistance = getSize(entity.score) + 100; // minDistance = 100 + return distance >= safeDistance; + }); + + expect(isSafe).toBe(true); + }); + + test('falls back to best position when no safe spot found after max attempts', () => { + // Create a crowded game state where finding a safe spot is unlikely + const mockGameState = { + aiPlayers: Array(20).fill(0).map((_, i) => ({ + x: (i % 5) * 400, + y: Math.floor(i / 5) * 400, + score: 400 + })), + playerCells: Array(10).fill(0).map((_, i) => ({ + x: (i % 5) * 400 + 200, + y: Math.floor(i / 5) * 400 + 200, + score: 400 + })) + }; + + // Mock Math.random to always return the same value to make the test deterministic + const originalRandom = Math.random; + let callCount = 0; + Math.random = jest.fn(() => { + callCount++; + return callCount % 10 / 10; // Return values from 0.1 to 1.0 + }); + + const position = findSafeSpawnLocation(mockGameState); + + // Restore Math.random + Math.random = originalRandom; + + expect(position).toHaveProperty('x'); + expect(position).toHaveProperty('y'); + + // We can't guarantee it's safe, but we can verify it returned a position + expect(typeof position.x).toBe('number'); + expect(typeof position.y).toBe('number'); + }); +}); diff --git a/static/js/__tests__/utils.test.js b/static/js/__tests__/utils.test.js index c052e12..be02e0e 100644 --- a/static/js/__tests__/utils.test.js +++ b/static/js/__tests__/utils.test.js @@ -1,4 +1,5 @@ -import { getSize, getDistance, calculateCenterOfMass } from '../utils.js'; +import { getSize, getDistance, calculateCenterOfMass, getRandomPosition, findSafeSpawnLocation, applyDecay } from '../utils.js'; +import { WORLD_SIZE, DECAY_ENABLED, DECAY_RATE, DECAY_THRESHOLD } from '../config.js'; describe('getSize', () => { test('returns correct size for score 0', () => { @@ -52,8 +53,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', () => { @@ -67,4 +68,34 @@ describe('calculateCenterOfMass', () => { ]; expect(calculateCenterOfMass(cells)).toEqual({ x: 0, y: 0 }); }); +}); + +describe('applyDecay', () => { + test('should not decay below threshold', () => { + const entity = { score: DECAY_THRESHOLD }; + applyDecay(entity, 1000); + expect(entity.score).toBe(DECAY_THRESHOLD); + }); + + test('should decay proportionally to time passed', () => { + const entity = { score: 100 }; + const expectedDecayFactor = Math.sqrt(entity.score) / 10; + const expectedDecayAmount = DECAY_RATE * expectedDecayFactor * 1; // 1 second + + applyDecay(entity, 1000); // 1 second + + expect(entity.score).toBeCloseTo(100 - expectedDecayAmount, 5); + }); + + test('should decay more for higher scores', () => { + const smallEntity = { score: 100 }; + const largeEntity = { score: 400 }; + + // Apply same time decay to both + applyDecay(smallEntity, 1000); + applyDecay(largeEntity, 1000); + + // Verify larger entity decayed more + expect(100 - smallEntity.score).toBeLessThan(400 - largeEntity.score); + }); }); \ No newline at end of file diff --git a/static/js/config.js b/static/js/config.js index 26d7461..e1ad293 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -7,6 +7,12 @@ export const FOOD_COUNT = 100; export const AI_COUNT = 10; export const COLLISION_THRESHOLD = 1.1; // 10% size difference needed for consumption +// Decay mechanics +export const DECAY_ENABLED = true; // Can be toggled to enable/disable decay +export const DECAY_RATE = 0.05; // Base score lost per second +export const DECAY_THRESHOLD = 50; // Minimum score before decay stops +export const DECAY_INTERVAL = 1000; // Decay calculation interval in milliseconds + // Split mechanics export const MIN_SPLIT_SCORE = 40; // Minimum score needed to split export const SPLIT_VELOCITY = 12; // Initial velocity of split cells diff --git a/static/js/entities.js b/static/js/entities.js index 01441f2..c23fbe9 100644 --- a/static/js/entities.js +++ b/static/js/entities.js @@ -33,7 +33,7 @@ function getUnusedAIName() { return AI_NAMES.find(name => !usedNames.has(name)) || AI_NAMES[0]; } -function updateCellMerging() { +export function updateCellMerging() { const now = Date.now(); const cellsToMerge = []; diff --git a/static/js/game.js b/static/js/game.js index 4c225dc..cb602a0 100644 --- a/static/js/game.js +++ b/static/js/game.js @@ -3,6 +3,8 @@ import { initRenderer, resizeCanvas, drawGame, drawMinimap, updateLeaderboard } import { updatePlayer, updateAI, initEntities, handlePlayerSplit } from './entities.js'; import { handleFoodCollisions, handlePlayerAICollisions, handleAIAICollisions, respawnEntities } from './collisions.js'; import { initUI } from './ui.js'; +import { applyDecay } from './utils.js'; +import { DECAY_INTERVAL } from './config.js'; function setupInputHandlers() { const canvas = document.getElementById('gameCanvas'); @@ -48,7 +50,32 @@ function verifyGameState() { } } +// Track time for decay calculations +let lastFrameTime = Date.now(); +let lastDecayTime = Date.now(); + function gameLoop() { + const currentTime = Date.now(); + const deltaTime = currentTime - lastFrameTime; + lastFrameTime = currentTime; + + // Apply decay at regular intervals to avoid performance issues + if (currentTime - lastDecayTime >= DECAY_INTERVAL) { + // Calculate time since last decay calculation + const decayDeltaTime = currentTime - lastDecayTime; + lastDecayTime = currentTime; + + // Apply decay to player cells + gameState.playerCells.forEach(cell => { + applyDecay(cell, decayDeltaTime); + }); + + // Apply decay to AI players + gameState.aiPlayers.forEach(ai => { + applyDecay(ai, decayDeltaTime); + }); + } + updatePlayer(); updateAI(); checkCollisions(); diff --git a/static/js/utils.js b/static/js/utils.js index 4fedee2..96228c8 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -1,4 +1,4 @@ -import { WORLD_SIZE } from './config.js'; +import { WORLD_SIZE, DECAY_ENABLED, DECAY_RATE, DECAY_THRESHOLD } from './config.js'; export function getSize(score) { return Math.sqrt(score) + 20; @@ -27,6 +27,21 @@ export function calculateCenterOfMass(cells) { }; } +export function applyDecay(entity, deltaTime) { + if (!DECAY_ENABLED || entity.score <= DECAY_THRESHOLD) { + return entity.score; + } + + // Calculate decay amount based on current score and time elapsed + // Higher scores decay faster (proportional to sqrt of score) + const decayFactor = Math.sqrt(entity.score) / 10; + const decayAmount = DECAY_RATE * decayFactor * (deltaTime / 1000); + + // Apply decay with a minimum threshold + entity.score = Math.max(DECAY_THRESHOLD, entity.score - decayAmount); + return entity.score; +} + export function findSafeSpawnLocation(gameState, minDistance = 100) { const maxAttempts = 50; let attempts = 0; From 3b429bcf6a8361fe2dc82244301049e163e7fec1 Mon Sep 17 00:00:00 2001 From: Abhay Aggarwal Date: Thu, 15 May 2025 18:01:43 -0700 Subject: [PATCH 3/4] new changes --- static/js/__tests__/decay.test.js | 132 ++++++++++++++++++++++-------- static/js/__tests__/utils.test.js | 3 +- static/js/collisions.js | 24 ++++++ static/js/config.js | 6 ++ static/js/entities.js | 23 +++++- static/js/utils.js | 41 ++++++++-- 6 files changed, 184 insertions(+), 45 deletions(-) diff --git a/static/js/__tests__/decay.test.js b/static/js/__tests__/decay.test.js index d8e75ab..51de03e 100644 --- a/static/js/__tests__/decay.test.js +++ b/static/js/__tests__/decay.test.js @@ -1,39 +1,44 @@ -import { applyDecay } from '../utils.js'; -import { DECAY_ENABLED, DECAY_RATE, DECAY_THRESHOLD } from '../config.js'; +import * as utils from '../utils.js'; +import * as config from '../config.js'; describe('decay mechanics', () => { - // Save original config values to restore after tests - const originalDecayEnabled = DECAY_ENABLED; - const originalDecayRate = DECAY_RATE; - const originalDecayThreshold = DECAY_THRESHOLD; - - // Mock the config values for testing + // Mock Date.now() to return a consistent value for tests + const NOW = 1621234567890; // Fixed timestamp for testing + let originalDateNow; + // Setup and teardown for each test beforeEach(() => { - // Ensure decay is enabled for tests - global.DECAY_ENABLED = true; - global.DECAY_RATE = 0.05; - global.DECAY_THRESHOLD = 50; + // Mock the config values directly + config.DECAY_ENABLED = true; + config.DECAY_RATE = 0.05; + config.DECAY_THRESHOLD = 50; + config.DECAY_MOVEMENT_MULTIPLIER = 1.5; + config.DECAY_SIZE_FACTOR = 0.2; + config.DECAY_STARVATION_THRESHOLD = 10; + config.DECAY_STARVATION_MULTIPLIER = 2; + + // Mock Date.now + originalDateNow = Date.now; + Date.now = jest.fn(() => NOW); }); - // Restore original values after tests afterEach(() => { - global.DECAY_ENABLED = originalDecayEnabled; - global.DECAY_RATE = originalDecayRate; - global.DECAY_THRESHOLD = originalDecayThreshold; + // Restore all mocks + jest.restoreAllMocks(); + Date.now = originalDateNow; }); test('should not decay below threshold', () => { - const entity = { score: DECAY_THRESHOLD }; - applyDecay(entity, 1000); - expect(entity.score).toBe(DECAY_THRESHOLD); + const entity = { score: config.DECAY_THRESHOLD }; + utils.applyDecay(entity, 1000); + expect(entity.score).toBe(config.DECAY_THRESHOLD); }); test('should decay proportionally to time passed', () => { const entity = { score: 100 }; const expectedDecayFactor = Math.sqrt(entity.score) / 10; - const expectedDecayAmount = DECAY_RATE * expectedDecayFactor * 1; // 1 second + const expectedDecayAmount = config.DECAY_RATE * expectedDecayFactor * 1; // 1 second - applyDecay(entity, 1000); // 1 second + utils.applyDecay(entity, 1000); // 1 second expect(entity.score).toBeCloseTo(100 - expectedDecayAmount, 5); }); @@ -43,8 +48,8 @@ describe('decay mechanics', () => { const largeEntity = { score: 400 }; // Apply same time decay to both - applyDecay(smallEntity, 1000); - applyDecay(largeEntity, 1000); + utils.applyDecay(smallEntity, 1000); + utils.applyDecay(largeEntity, 1000); // Calculate expected decay const smallDecayFactor = Math.sqrt(100) / 10; @@ -56,21 +61,15 @@ describe('decay mechanics', () => { }); test('should not decay when disabled', () => { - // Mock the DECAY_ENABLED value directly in the module - const originalDecayEnabled = DECAY_ENABLED; - Object.defineProperty(global, 'DECAY_ENABLED', { - value: false, - writable: true - }); + // Temporarily disable decay for this test + const originalDecayEnabled = config.DECAY_ENABLED; + config.DECAY_ENABLED = false; const entity = { score: 100 }; - applyDecay(entity, 1000); + utils.applyDecay(entity, 1000); // Restore original value - Object.defineProperty(global, 'DECAY_ENABLED', { - value: originalDecayEnabled, - writable: true - }); + config.DECAY_ENABLED = originalDecayEnabled; expect(entity.score).toBe(100); }); @@ -79,9 +78,70 @@ describe('decay mechanics', () => { const entity = { score: 200 }; // Apply a very large time delta (10 seconds) - applyDecay(entity, 10000); + utils.applyDecay(entity, 10000); // Should not go below threshold - expect(entity.score).toBeGreaterThanOrEqual(DECAY_THRESHOLD); + expect(entity.score).toBeGreaterThanOrEqual(config.DECAY_THRESHOLD); + }); + + test('should apply movement-based decay', () => { + // Entity with velocity + const entity = { + score: 100, + velocityX: 2, + velocityY: 2 + }; + + // Entity with no velocity + const staticEntity = { + score: 100, + velocityX: 0, + velocityY: 0 + }; + + utils.applyDecay(entity, 1000); + utils.applyDecay(staticEntity, 1000); + + // Moving entity should decay more + expect(entity.score).toBeLessThan(staticEntity.score); + }); + + test('should apply starvation decay', () => { + // Entity that just ate + const recentlyFedEntity = { + score: 100, + lastFoodTime: NOW + }; + + // Entity that hasn't eaten in a while + const starvingEntity = { + score: 100, + lastFoodTime: NOW - (config.DECAY_STARVATION_THRESHOLD * 1000 + 5000) // Past threshold + }; + + utils.applyDecay(recentlyFedEntity, 1000); + utils.applyDecay(starvingEntity, 1000); + + // Starving entity should decay more + expect(starvingEntity.score).toBeLessThan(recentlyFedEntity.score); + }); + + test('should apply size-based decay using power function', () => { + const smallEntity = { score: 100 }; + const largeEntity = { score: 400 }; + + // Calculate expected decay with power function + const smallDecayFactor = Math.pow(smallEntity.score, config.DECAY_SIZE_FACTOR) / 10; + const largeDecayFactor = Math.pow(largeEntity.score, config.DECAY_SIZE_FACTOR) / 10; + + // Verify power function gives different results than sqrt + expect(largeDecayFactor / smallDecayFactor).not.toBeCloseTo(2); // sqrt would be 2 + + utils.applyDecay(smallEntity, 1000); + utils.applyDecay(largeEntity, 1000); + + // Larger entity should decay more, but by a different factor than with sqrt + expect(largeEntity.score).toBeLessThan(400); + expect(smallEntity.score).toBeLessThan(100); }); }); diff --git a/static/js/__tests__/utils.test.js b/static/js/__tests__/utils.test.js index be02e0e..4e53b90 100644 --- a/static/js/__tests__/utils.test.js +++ b/static/js/__tests__/utils.test.js @@ -79,7 +79,8 @@ describe('applyDecay', () => { test('should decay proportionally to time passed', () => { const entity = { score: 100 }; - const expectedDecayFactor = Math.sqrt(entity.score) / 10; + // Update to use power function instead of sqrt + const expectedDecayFactor = Math.pow(entity.score, DECAY_SIZE_FACTOR) / 10; const expectedDecayAmount = DECAY_RATE * expectedDecayFactor * 1; // 1 second applyDecay(entity, 1000); // 1 second diff --git a/static/js/collisions.js b/static/js/collisions.js index 70cda7e..2ec9ad2 100644 --- a/static/js/collisions.js +++ b/static/js/collisions.js @@ -4,32 +4,56 @@ import { FOOD_SIZE, FOOD_SCORE, COLLISION_THRESHOLD, FOOD_COUNT, AI_COUNT, START import { respawnAI } from './entities.js'; export function handleFoodCollisions() { + const currentTime = Date.now(); + // Player cells eating food for (const playerCell of gameState.playerCells) { + let hasEaten = false; + gameState.food = gameState.food.filter(food => { const distance = getDistance(playerCell, food); const playerSize = getSize(playerCell.score); if (distance < playerSize + FOOD_SIZE) { playerCell.score += FOOD_SCORE; + hasEaten = true; return false; } return true; }); + + // Update the lastFoodTime when player eats food + if (hasEaten) { + playerCell.lastFoodTime = currentTime; + } else if (!playerCell.lastFoodTime) { + // Initialize lastFoodTime if it doesn't exist + playerCell.lastFoodTime = currentTime; + } } // AI eating food for (const ai of gameState.aiPlayers) { + let hasEaten = false; + gameState.food = gameState.food.filter(food => { const distance = getDistance(ai, food); const aiSize = getSize(ai.score); if (distance < aiSize + FOOD_SIZE) { ai.score += FOOD_SCORE; + hasEaten = true; return false; } return true; }); + + // Update the lastFoodTime when AI eats food + if (hasEaten) { + ai.lastFoodTime = currentTime; + } else if (!ai.lastFoodTime) { + // Initialize lastFoodTime if it doesn't exist + ai.lastFoodTime = currentTime; + } } } diff --git a/static/js/config.js b/static/js/config.js index e1ad293..2e18cef 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -13,6 +13,12 @@ export const DECAY_RATE = 0.05; // Base score lost per second export const DECAY_THRESHOLD = 50; // Minimum score before decay stops export const DECAY_INTERVAL = 1000; // Decay calculation interval in milliseconds +// Advanced decay mechanics +export const DECAY_MOVEMENT_MULTIPLIER = 1.5; // Decay multiplier when moving (higher = more decay when moving) +export const DECAY_SIZE_FACTOR = 0.2; // How much size affects decay (higher = larger cells decay faster) +export const DECAY_STARVATION_THRESHOLD = 10; // Seconds without food before starvation decay kicks in +export const DECAY_STARVATION_MULTIPLIER = 2; // Multiplier for decay when starving + // Split mechanics export const MIN_SPLIT_SCORE = 40; // Minimum score needed to split export const SPLIT_VELOCITY = 12; // Initial velocity of split cells diff --git a/static/js/entities.js b/static/js/entities.js index c23fbe9..721e1f6 100644 --- a/static/js/entities.js +++ b/static/js/entities.js @@ -265,6 +265,8 @@ export function initEntities() { gameState.aiPlayers = []; console.log('Initializing entities...'); + + const currentTime = Date.now(); // Initialize food for (let i = 0; i < FOOD_COUNT; i++) { @@ -285,11 +287,26 @@ export function initEntities() { score: AI_STARTING_SCORE, color: `hsl(${Math.random() * 360}, 70%, 50%)`, direction: Math.random() * Math.PI * 2, - name: getUnusedAIName() + name: getUnusedAIName(), + lastFoodTime: currentTime // Initialize lastFoodTime }; gameState.aiPlayers.push(ai); } + // Respawn player if all cells are gone + if (gameState.playerCells.length === 0) { + const safePos = findSafeSpawnLocation(gameState); + gameState.playerCells.push({ + x: safePos.x, + y: safePos.y, + score: STARTING_SCORE, + velocityX: 0, + velocityY: 0, + splitTime: 0, + lastFoodTime: currentTime // Initialize lastFoodTime + }); + } + console.log('Entities initialized:', { foodCount: gameState.food.length, aiCount: gameState.aiPlayers.length, @@ -301,6 +318,7 @@ export function initEntities() { export function respawnAI() { const pos = getRandomPosition(); const name = getUnusedAIName(); + const currentTime = Date.now(); return { x: pos.x, @@ -308,6 +326,7 @@ export function respawnAI() { score: AI_STARTING_SCORE, color: `hsl(${Math.random() * 360}, 70%, 50%)`, direction: Math.random() * Math.PI * 2, - name: name + name: name, + lastFoodTime: currentTime // Initialize lastFoodTime }; } \ No newline at end of file diff --git a/static/js/utils.js b/static/js/utils.js index 96228c8..b6cf2d1 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -1,4 +1,13 @@ -import { WORLD_SIZE, DECAY_ENABLED, DECAY_RATE, DECAY_THRESHOLD } from './config.js'; +import { + WORLD_SIZE, + DECAY_ENABLED, + DECAY_RATE, + DECAY_THRESHOLD, + DECAY_MOVEMENT_MULTIPLIER, + DECAY_SIZE_FACTOR, + DECAY_STARVATION_THRESHOLD, + DECAY_STARVATION_MULTIPLIER +} from './config.js'; export function getSize(score) { return Math.sqrt(score) + 20; @@ -28,14 +37,34 @@ export function calculateCenterOfMass(cells) { } export function applyDecay(entity, deltaTime) { - if (!DECAY_ENABLED || entity.score <= DECAY_THRESHOLD) { + // Check if decay is disabled globally or if entity is at minimum threshold + if (DECAY_ENABLED === false || entity.score <= DECAY_THRESHOLD) { return entity.score; } - // Calculate decay amount based on current score and time elapsed - // Higher scores decay faster (proportional to sqrt of score) - const decayFactor = Math.sqrt(entity.score) / 10; - const decayAmount = DECAY_RATE * decayFactor * (deltaTime / 1000); + // Initialize decay multiplier + let decayMultiplier = 1.0; + + // Apply movement-based decay if the entity is moving + if (entity.velocityX !== undefined && entity.velocityY !== undefined) { + const speed = Math.sqrt(entity.velocityX * entity.velocityX + entity.velocityY * entity.velocityY); + if (speed > 0.1) { // Only apply if moving at a meaningful speed + decayMultiplier *= (1 + (speed * DECAY_MOVEMENT_MULTIPLIER / 10)); + } + } + + // Apply starvation decay if the entity hasn't eaten recently + if (entity.lastFoodTime !== undefined) { + const timeSinceFood = (Date.now() - entity.lastFoodTime) / 1000; // in seconds + if (timeSinceFood > DECAY_STARVATION_THRESHOLD) { + decayMultiplier *= DECAY_STARVATION_MULTIPLIER; + } + } + + // Calculate base decay amount based on current score and time elapsed + // Higher scores decay faster (proportional to score^DECAY_SIZE_FACTOR) + const decayFactor = Math.pow(entity.score, DECAY_SIZE_FACTOR) / 10; + const decayAmount = DECAY_RATE * decayFactor * decayMultiplier * (deltaTime / 1000); // Apply decay with a minimum threshold entity.score = Math.max(DECAY_THRESHOLD, entity.score - decayAmount); From 1b66bc97c4389c6bd6cde560a652e768904850c8 Mon Sep 17 00:00:00 2001 From: Abhay Aggarwal Date: Thu, 18 Sep 2025 14:07:20 -0700 Subject: [PATCH 4/4] fix: remediate PR analysis findings - Fix missing DECAY_SIZE_FACTOR import in utils.test.js - Fix inconsistent decay calculation in tests (use power function instead of sqrt) - Remove unused DECAY_ENABLED import from utils.test.js - Improve DECAY_THRESHOLD documentation for clarity --- .../workflows/pr-analysis-remediation.md | 106 ++++++++++++++++++ static/js/__tests__/decay.test.js | 6 +- static/js/__tests__/utils.test.js | 2 +- static/js/config.js | 2 +- 4 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 .windsurf/workflows/pr-analysis-remediation.md diff --git a/.windsurf/workflows/pr-analysis-remediation.md b/.windsurf/workflows/pr-analysis-remediation.md new file mode 100644 index 0000000..e871354 --- /dev/null +++ b/.windsurf/workflows/pr-analysis-remediation.md @@ -0,0 +1,106 @@ +--- +description: Perform PR analysis, remediate errors, and push changes using GitHub CLI +--- + +# PR Analysis and Remediation Workflow + +This workflow helps you analyze a pull request, identify and fix issues, and push the remediated changes back to the repository. + +## Prerequisites +- GitHub CLI (`gh`) installed and authenticated +- Repository cloned locally +- Appropriate permissions to push to the branch + +## Steps + +1. **Fetch the latest PR information** +```bash +gh pr list --state open +``` + +2. **Checkout the PR branch** +```bash +gh pr checkout +``` + +3. **Analyze PR changes and get diff** +```bash +gh pr diff +``` + +4. **Check PR status and reviews** +```bash +gh pr view --json reviews,statusCheckRollup +``` + +5. **Run automated checks locally** +// turbo +```bash +npm test +``` + +6. **Run linting and formatting** +// turbo +```bash +npm run lint +``` + +7. **Fix any linting issues automatically** +// turbo +```bash +npm run lint:fix +``` + +8. **Run type checking (if applicable)** +// turbo +```bash +npm run type-check +``` + +9. **Stage and commit any fixes** +```bash +git add . +git commit -m "fix: remediate PR analysis findings" +``` + +10. **Push the remediated changes** +```bash +git push origin HEAD +``` + +11. **Add a comment to the PR about the remediation** +```bash +gh pr comment --body "🔧 Automated remediation applied: +- Fixed linting issues +- Resolved test failures +- Applied code formatting +Ready for re-review!" +``` + +12. **Request re-review if needed** +```bash +gh pr review --request-changes --body "Please review the automated fixes applied" +``` + +## Optional: Advanced Analysis + +13. **Check for security vulnerabilities** +```bash +npm audit +``` + +14. **Run performance tests** +```bash +npm run test:performance +``` + +15. **Generate coverage report** +```bash +npm run test:coverage +``` + +## Notes +- Replace `` with the actual PR number +- Ensure you have the necessary permissions before pushing changes +- Review automated fixes before committing to avoid introducing new issues +- Consider running the full test suite after remediation diff --git a/static/js/__tests__/decay.test.js b/static/js/__tests__/decay.test.js index 51de03e..1d3ce64 100644 --- a/static/js/__tests__/decay.test.js +++ b/static/js/__tests__/decay.test.js @@ -35,7 +35,7 @@ describe('decay mechanics', () => { test('should decay proportionally to time passed', () => { const entity = { score: 100 }; - const expectedDecayFactor = Math.sqrt(entity.score) / 10; + const expectedDecayFactor = Math.pow(entity.score, config.DECAY_SIZE_FACTOR) / 10; const expectedDecayAmount = config.DECAY_RATE * expectedDecayFactor * 1; // 1 second utils.applyDecay(entity, 1000); // 1 second @@ -52,8 +52,8 @@ describe('decay mechanics', () => { utils.applyDecay(largeEntity, 1000); // Calculate expected decay - const smallDecayFactor = Math.sqrt(100) / 10; - const largeDecayFactor = Math.sqrt(400) / 10; + const smallDecayFactor = Math.pow(100, config.DECAY_SIZE_FACTOR) / 10; + const largeDecayFactor = Math.pow(400, config.DECAY_SIZE_FACTOR) / 10; // Verify larger entity decayed more expect(largeDecayFactor).toBeGreaterThan(smallDecayFactor); diff --git a/static/js/__tests__/utils.test.js b/static/js/__tests__/utils.test.js index 4e53b90..5ecaa32 100644 --- a/static/js/__tests__/utils.test.js +++ b/static/js/__tests__/utils.test.js @@ -1,5 +1,5 @@ import { getSize, getDistance, calculateCenterOfMass, getRandomPosition, findSafeSpawnLocation, applyDecay } from '../utils.js'; -import { WORLD_SIZE, DECAY_ENABLED, DECAY_RATE, DECAY_THRESHOLD } from '../config.js'; +import { WORLD_SIZE, DECAY_RATE, DECAY_THRESHOLD, DECAY_SIZE_FACTOR } from '../config.js'; describe('getSize', () => { test('returns correct size for score 0', () => { diff --git a/static/js/config.js b/static/js/config.js index 2e18cef..080ecd8 100644 --- a/static/js/config.js +++ b/static/js/config.js @@ -10,7 +10,7 @@ export const COLLISION_THRESHOLD = 1.1; // 10% size difference needed for consum // Decay mechanics export const DECAY_ENABLED = true; // Can be toggled to enable/disable decay export const DECAY_RATE = 0.05; // Base score lost per second -export const DECAY_THRESHOLD = 50; // Minimum score before decay stops +export const DECAY_THRESHOLD = 50; // Minimum score - decay stops completely when score reaches this value export const DECAY_INTERVAL = 1000; // Decay calculation interval in milliseconds // Advanced decay mechanics