Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@ instance/
# Virtual Environment
venv/
env/
ENV/
ENV/
.windsurf/rules/do-not-make-new-test-files.md
.windsurf/workflows/test-new-changes.md
.gitignore
.windsurf/rules/minimal-rules.md
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've added .gitignore to the .gitignore file on line 46. This will cause git to ignore the .gitignore file itself, which means future changes to this file won't be tracked. This is generally not recommended as it would make it difficult to maintain consistent ignore rules across the repository.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .gitignore file is being added to itself (line 46). This is unusual and can cause unexpected behavior with Git. A file shouldn't typically ignore itself as this can lead to confusion when tracking changes.

2 changes: 1 addition & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ def update_player():
return jsonify({'status': 'ok'})

if __name__ == '__main__':
app.run(debug=True)
app.run(debug=True, port=5001)
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -22,4 +23,4 @@
"^.+\\.js$": "babel-jest"
}
}
}
}
73 changes: 73 additions & 0 deletions static/js/__tests__/score-decay.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
4 changes: 2 additions & 2 deletions static/js/__tests__/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
4 changes: 4 additions & 0 deletions static/js/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
29 changes: 28 additions & 1 deletion static/js/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -164,7 +166,13 @@ function updateCellMerging() {
}
}

let lastUpdateTime = Date.now();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lastUpdateTime variable is initialized at line 169 but then immediately overwritten in the updatePlayer function. This could cause the first frame's deltaTime calculation to be incorrect (potentially 0). Consider removing the global initialization or using it properly for the first frame.


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);
Expand All @@ -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);
});
}

Expand Down Expand Up @@ -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
Comment on lines +269 to +270
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an issue with the lastUpdateTime variable. It's reset in updatePlayer() with lastUpdateTime = currentTime, but then updateAI() calculates deltaTime using the same variable. This will result in deltaTime being 0 or very small in updateAI() if it's called right after updatePlayer(), causing minimal score decay for AI players.

Comment on lines +269 to +270
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updateAI function is using lastUpdateTime which is updated in updatePlayer. This creates a dependency where updateAI must be called after updatePlayer to get the correct deltaTime. Consider passing deltaTime as a parameter or calculating it independently in each function.


gameState.aiPlayers.forEach(ai => {
if (Math.random() < 0.02) {
ai.direction = Math.random() * Math.PI * 2;
Expand All @@ -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);
});
}

Expand Down
37 changes: 32 additions & 5 deletions static/js/renderer.js
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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})`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no validation before converting colors with hexToRgb. If a color is already in rgba format or is invalid, this could cause errors. Consider adding a check to verify the color is a valid hex format before conversion.

} else {
ctx.fillStyle = color;
}

ctx.fill();

// Draw name
Expand Down Expand Up @@ -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() {
Expand Down