User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'head.lastDir')' in or related to this line: 'if (head.lastDir === undefined) {' Line Number: 449
User prompt
If you make a sharp turn, it kills you
User prompt
Make the head spawn normally from the body
User prompt
Now it want move and it keeps killing me because the obstacles pawn on the snake
User prompt
Make the head spawn on the body
User prompt
Remove speed poweup
User prompt
Make the head spawn away from the body
User prompt
More content
User prompt
Move the head to be away from the body (not close)
User prompt
Mive the head foward
User prompt
Remove the blue food
User prompt
Make the head further away from the body
User prompt
Make the head further away from the body
User prompt
Don’t make the obstacles move
Code edit (1 edits merged)
Please save this source code
User prompt
Snake Evolution
Initial prompt
Snake game (with new content)
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // AISnake (AI snake that moves toward food and steals it) var AISnake = Container.expand(function () { var self = Container.call(this); self.length = 3 + Math.floor(Math.random() * 2); // 3-4 segments self.segments = []; self.dir = 0; self.speed = Math.max(4, SNAKE_INIT_SPEED * 0.35); // AI snake moves much slower than player self.target = null; self.stealing = false; self.lastFood = null; // Create head var head = self.attachAsset('aiSnakeHead', { anchorX: 0.5, anchorY: 0.5 }); self.asset = head; self.segments.push(self); // Create body segments for (var i = 1; i < self.length; ++i) { var seg = new Container(); seg.asset = seg.attachAsset('aiSnakeBody', { anchorX: 0.5, anchorY: 0.5 }); seg.x = self.x - i * 60; seg.y = self.y; self.addChild(seg); self.segments.push(seg); } // Custom hitbox self.hitboxScale = 0.6; // Custom intersects for head self.intersects = function (other) { var scaleA = typeof self.hitboxScale === "number" ? self.hitboxScale : 1; var scaleB = typeof other.hitboxScale === "number" ? other.hitboxScale : 1; var ax = self.x, ay = self.y, aw = self.asset.width * scaleA, ah = self.asset.height * scaleA; var bx = other.x, by = other.y, bw = other.asset && other.asset.width ? other.asset.width * scaleB : 0, bh = other.asset && other.asset.height ? other.asset.height * scaleB : 0; var rA = Math.max(aw, ah) / 2; var rB = Math.max(bw, bh) / 2; var dx = ax - bx, dy = ay - by; return dx * dx + dy * dy < (rA + rB) * (rA + rB); }; // Update method self.update = function () { // Save last position for event triggers if (self.lastX === undefined) self.lastX = self.x; if (self.lastY === undefined) self.lastY = self.y; // Find food if not already targeting if (!self.target || self.target.destroyed) { if (typeof food !== "undefined" && food && !food.destroyed) { self.target = food; } else { self.target = null; } } // Move toward food using player-like movement if (self.target) { // Calculate desired direction var dx = self.target.x - self.x; var dy = self.target.y - self.y; var desiredDir = Math.atan2(dy, dx); // Smoothly turn toward the target (like player) var d = desiredDir - self.dir; while (d > Math.PI) d -= 2 * Math.PI; while (d < -Math.PI) d += 2 * Math.PI; var turn = Math.sign(d) * Math.min(Math.abs(d), SNAKE_TURN_ANGLE); self.dir += turn; // Move head forward self.x += Math.cos(self.dir) * self.speed; self.y += Math.sin(self.dir) * self.speed; // Clamp to bounds self.x = Math.max(100 + self.asset.width / 2, Math.min(2048 - 100 - self.asset.width / 2, self.x)); self.y = Math.max(100 + self.asset.height / 2, Math.min(2732 - 100 - self.asset.height / 2, self.y)); // Move body segments to follow the previous segment, like player for (var i = 1; i < self.segments.length; ++i) { var seg = self.segments[i]; var target = self.segments[i - 1]; var sdx = target.x - seg.x; var sdy = target.y - seg.y; var sdist = Math.sqrt(sdx * sdx + sdy * sdy); var desired = self.speed * 0.9; if (sdist > desired) { var move = (sdist - desired) * 0.5; seg.x += sdx / sdist * move; seg.y += sdy / sdist * move; } } // Steal food if close enough if (self.target && self.intersects(self.target)) { self.stealing = true; // Remove food from game if (typeof food !== "undefined" && food === self.target) { food.destroy(); food = null; // Always respawn food after AI steals it if (typeof spawnFood === "function") { spawnFood(); } } self.target = null; } } // Despawn if off screen or after stealing if (self.stealing) { // Move off screen quickly self.x += Math.cos(self.dir) * self.speed * 2; self.y += Math.sin(self.dir) * self.speed * 2; if (self.x < -200 || self.x > 2048 + 200 || self.y < -200 || self.y > 2732 + 200) { self._despawn = true; } } self.lastX = self.x; self.lastY = self.y; }; return self; }); // Food var Food = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('food', { anchorX: 0.5, anchorY: 0.5 }); self.hitboxScale = 0.6; return self; }); // FoodPurple var FoodPurple = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('foodPurple', { anchorX: 0.5, anchorY: 0.5 }); self.hitboxScale = 0.6; return self; }); // FoodRainbow (rare, big bonus) var FoodRainbow = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('food', { anchorX: 0.5, anchorY: 0.5 }); self.hitboxScale = 0.6; // Animate color cycling // Animate color cycling using tween plugin API tween(self.asset, { tint: 0xff0000 }, { duration: 400, easing: tween.linear, onFinish: function onFinish() { // No-op, will be repeated } }); // Manually cycle through rainbow colors in update (since tween can't do this directly) self.update = function () { var t = LK.ticks % 60 / 60; var r = Math.floor(127 * (Math.sin(2 * Math.PI * t) + 1)); var g = Math.floor(127 * (Math.sin(2 * Math.PI * t + 2) + 1)); var b = Math.floor(127 * (Math.sin(2 * Math.PI * t + 4) + 1)); self.asset.tint = r << 16 | g << 8 | b; }; return self; }); // MovingObstacle var MovingObstacle = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('obstacleMoving', { anchorX: 0.5, anchorY: 0.5 }); self.hitboxScale = 0.7; // Pick a random direction and speed var angle = Math.random() * Math.PI * 2; var speed = 4 + Math.random() * 4; self.vx = Math.cos(angle) * speed; self.vy = Math.sin(angle) * speed; self.update = function () { // Save last position for event triggers if (self.lastX === undefined) self.lastX = self.x; if (self.lastY === undefined) self.lastY = self.y; self.x += self.vx; self.y += self.vy; // Despawn if out of bounds (with 150px margin) if (self.x < -150 || self.x > 2048 + 150 || self.y < -150 || self.y > 2732 + 150) { // Mark for removal by setting a flag self._despawn = true; return; } // Bounce off walls (100px margin) if (self.x < 100 + self.asset.width / 2 && self.vx < 0) self.vx *= -1; if (self.x > 2048 - 100 - self.asset.width / 2 && self.vx > 0) self.vx *= -1; if (self.y < 100 + self.asset.height / 2 && self.vy < 0) self.vy *= -1; if (self.y > 2732 - 100 - self.asset.height / 2 && self.vy > 0) self.vy *= -1; self.lastX = self.x; self.lastY = self.y; }; return self; }); // Obstacle (static) var Obstacle = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('obstacle', { anchorX: 0.5, anchorY: 0.5 }); self.hitboxScale = 0.7; // Add despawn timer and update method for static obstacles self._despawnTimer = 0; self._despawnInterval = 60 * 60; // Try to despawn every 60 seconds (3600 ticks) self.update = function () { if (self._despawnTimer === undefined) self._despawnTimer = 0; self._despawnTimer++; // Only check every _despawnInterval ticks if (self._despawnTimer >= self._despawnInterval) { self._despawnTimer = 0; // 40% chance to despawn (less often than moving obstacles) if (Math.random() < 0.4) { self._despawn = true; } } }; return self; }); // PowerUp var PowerUp = Container.expand(function () { var self = Container.call(this); self.type = 'invincible'; // or 'shrink' self.asset = null; self.hitboxScale = 0.6; self.setType = function (type) { if (self.asset) self.removeChild(self.asset); self.type = type; if (type === 'invincible') { self.asset = self.attachAsset('powerupInvincible', { anchorX: 0.5, anchorY: 0.5 }); } else if (type === 'shrink') { self.asset = self.attachAsset('powerupShrink', { anchorX: 0.5, anchorY: 0.5 }); } }; self.setType('invincible'); return self; }); // Snake Segment (body or head) var SnakeSegment = Container.expand(function () { var self = Container.call(this); self.isHead = false; self.asset = null; // Custom hitbox size (smaller than visual asset) self.hitboxScale = 0.6; // 60% of asset size for both head and body self.setType = function (type) { if (self.asset) self.removeChild(self.asset); if (type === 'head') { self.asset = self.attachAsset('snakeHead', { anchorX: 0.5, anchorY: 0.5 }); self.isHead = true; } else { self.asset = self.attachAsset('snakeBody', { anchorX: 0.5, anchorY: 0.5 }); self.isHead = false; } }; self.setType('body'); // Custom intersects method for smaller hitbox self.intersects = function (other) { // Use hitboxScale for both this and other if available, else default to 1 var scaleA = typeof self.hitboxScale === "number" ? self.hitboxScale : 1; var scaleB = typeof other.hitboxScale === "number" ? other.hitboxScale : 1; var ax = self.x, ay = self.y, aw = self.asset.width * scaleA, ah = self.asset.height * scaleA; var bx = other.x, by = other.y, bw = other.asset && other.asset.width ? other.asset.width * scaleB : 0, bh = other.asset && other.asset.height ? other.asset.height * scaleB : 0; // Circle collision (since snake is ellipse/circle) var rA = Math.max(aw, ah) / 2; var rB = Math.max(bw, bh) / 2; var dx = ax - bx; var dy = ay - by; return dx * dx + dy * dy < (rA + rB) * (rA + rB); }; return self; }); // SpikeObstacle (destroys snake segments it touches, rare spawn) var SpikeObstacle = Container.expand(function () { var self = Container.call(this); // Add spike obstacle asset first (so aura is in front) self.asset = self.attachAsset('spikeObstacle', { anchorX: 0.5, anchorY: 0.5 }); // Add spike aura in front of the spike asset self.aura = self.attachAsset('spikeAura', { anchorX: 0.5, anchorY: 0.5 }); self.aura.alpha = 0.35; self.hitboxScale = 0.7; // Add despawn timer and update method for static obstacles self._despawnTimer = 0; self._despawnInterval = 60 * 45; // Try to despawn every 45 seconds (less often than normal obstacles) self.update = function () { if (self._despawnTimer === undefined) self._despawnTimer = 0; self._despawnTimer++; // Only check every _despawnInterval ticks if (self._despawnTimer >= self._despawnInterval) { self._despawnTimer = 0; // 60% chance to despawn (spikes are rare, so despawn more often) if (Math.random() < 0.6) { self._despawn = true; } } // Animate aura pulse if (self.aura) { var t = LK.ticks % 60 / 60; self.aura.alpha = 0.25 + 0.15 * Math.sin(2 * Math.PI * t); var scale = 1.05 + 0.08 * Math.sin(2 * Math.PI * t); self.aura.scaleX = scale; self.aura.scaleY = scale; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181818 }); /**** * Game Code ****/ // Music // Sounds // Obstacles // Power-ups // Food // Snake head and body // --- Game constants --- var SNAKE_INIT_LENGTH = 5; var SNAKE_INIT_SPEED = 16; // pixels per tick var SNAKE_MIN_SPEED = 8; var SNAKE_MAX_SPEED = 40; var SNAKE_TURN_ANGLE = Math.PI / 18; // 10 degrees per input var FOOD_SCORE = 10; var POWERUP_SCORE = 25; var POWERUP_DURATION = 300; // ticks (5 seconds) var OBSTACLE_COUNT = 3; var OBSTACLE_SPEED = 6; // --- Game state --- var snake = []; var aiSnakes = []; // Array of AI snakes var snakeDir = 0; // in radians, 0 = right var snakeNextDir = 0; var snakeSpeed = SNAKE_INIT_SPEED; var snakeGrow = 0; var food = null; var powerup = null; var powerupActive = false; var powerupType = null; var powerupTimer = 0; var obstacles = []; var score = 0; var isInvincible = false; var lastTouch = null; var dragStart = null; var dragAngle = null; var dragActive = false; var gameOver = false; // --- GUI --- var scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // --- Helper functions --- function randomPos(margin, avoidList, avoidRadius) { margin = margin || 150; avoidList = avoidList || []; avoidRadius = avoidRadius || 120; // Default: avoid within 120px of any avoidList item var tries = 0; while (tries < 50) { var x = margin + Math.random() * (2048 - 2 * margin); var y = margin + Math.random() * (2732 - 2 * margin); var ok = true; for (var i = 0; i < avoidList.length; ++i) { var obj = avoidList[i]; var dx = obj.x - x; var dy = obj.y - y; if (dx * dx + dy * dy < avoidRadius * avoidRadius) { ok = false; break; } } if (ok) { return { x: x, y: y }; } tries++; } // Fallback: just return a random position if we can't find a good one return { x: margin + Math.random() * (2048 - 2 * margin), y: margin + Math.random() * (2732 - 2 * margin) }; } function dist2(a, b) { var dx = a.x - b.x, dy = a.y - b.y; return dx * dx + dy * dy; } function angleBetween(a, b) { return Math.atan2(b.y - a.y, b.x - a.x); } function wrapPos(pos) { // Not used, but could be for wrap-around if (pos.x < 0) pos.x += 2048; if (pos.x > 2048) pos.x -= 2048; if (pos.y < 0) pos.y += 2732; if (pos.y > 2732) pos.y -= 2732; } function spawnFood() { if (food) food.destroy(); // 5% chance for rainbow food, 20% for purple, else normal var r = Math.random(); if (r < 0.05) { food = new FoodRainbow(); food.isRainbow = true; food.isPurple = false; } else if (r < 0.25) { food = new FoodPurple(); food.isPurple = true; food.isRainbow = false; } else { food = new Food(); food.isPurple = false; food.isRainbow = false; } // Avoid spawning on the snake and obstacles var avoidList = []; for (var i = 0; i < snake.length; ++i) { avoidList.push(snake[i]); } // Also avoid obstacles for (var i = 0; i < obstacles.length; ++i) { avoidList.push(obstacles[i]); } var pos = randomPos(200, avoidList, 120); food.x = pos.x; food.y = pos.y; game.addChild(food); } function spawnPowerUp() { if (powerup) powerup.destroy(); powerup = new PowerUp(); // Avoid spawning on the snake var avoidList = []; for (var i = 0; i < snake.length; ++i) { avoidList.push(snake[i]); } var pos = randomPos(200, avoidList, 120); // 67% invincible, 33% shrink var r = Math.random(); var t; if (r < 0.67) t = 'invincible';else t = 'shrink'; powerup.setType(t); powerup.x = pos.x; powerup.y = pos.y; game.addChild(powerup); } function spawnObstacles() { for (var i = 0; i < obstacles.length; ++i) { obstacles[i].destroy(); } obstacles = []; // Avoid spawning on the snake var avoidList = []; for (var j = 0; j < snake.length; ++j) { avoidList.push(snake[j]); } for (var i = 0; i < OBSTACLE_COUNT; ++i) { var obs = new Obstacle(); var pos = randomPos(300, avoidList, 140); obs.x = pos.x; obs.y = pos.y; // Add this obstacle to avoidList so next ones don't overlap avoidList.push(obs); obstacles.push(obs); game.addChild(obs); } } function resetSnake() { for (var i = 0; i < snake.length; ++i) { snake[i].destroy(); } snake = []; var startX = 2048 / 2, startY = 2732 / 2; for (var i = 0; i < SNAKE_INIT_LENGTH; ++i) { var seg = new SnakeSegment(); if (i === 0) { seg.setType('head'); // Head spawns in front of the body, not on top of the first body segment seg.x = startX; seg.y = startY; } else { seg.setType('body'); // Each body segment spawns behind the previous one, extending left from the head seg.x = startX - i * snakeSpeed * 1.2; seg.y = startY; } game.addChild(seg); snake.push(seg); } snakeDir = 0; snakeNextDir = 0; snakeSpeed = SNAKE_INIT_SPEED; snakeGrow = 0; isInvincible = false; powerupActive = false; powerupType = null; powerupTimer = 0; } function updateScore(val) { score = val; scoreTxt.setText(score); LK.setScore(score); } function activatePowerUp(type) { powerupActive = true; powerupType = type; powerupTimer = POWERUP_DURATION; if (type === 'invincible') { isInvincible = true; // Flash snake for (var i = 0; i < snake.length; ++i) { LK.effects.flashObject(snake[i], 0xf1c40f, 500); } } else if (type === 'shrink') { // Remove 3 segments from the tail, but always keep at least 5 var minLen = 5; var removeCount = Math.min(3, snake.length - minLen); for (var i = 0; i < removeCount; ++i) { var seg = snake.pop(); seg.destroy(); } } } function deactivatePowerUp() { powerupActive = false; powerupType = null; powerupTimer = 0; isInvincible = false; } function endGame() { if (gameOver) return; gameOver = true; LK.effects.flashScreen(0xff0000, 1000); LK.getSound('hit').play(); LK.showGameOver(); } // --- Input handling (touch/drag to steer) --- game.down = function (x, y, obj) { // Only allow drag from outside top left 100x100 if (x < 100 && y < 100) return; dragStart = { x: x, y: y }; dragAngle = null; dragActive = true; }; game.move = function (x, y, obj) { if (!dragActive) return; if (!dragStart) return; // Calculate angle from snake head to drag point var head = snake[0]; var dx = x - head.x; var dy = y - head.y; if (dx * dx + dy * dy < 100) return; // Ignore tiny drags var angle = Math.atan2(dy, dx); snakeNextDir = angle; dragAngle = angle; }; game.up = function (x, y, obj) { dragActive = false; dragStart = null; dragAngle = null; }; // --- Main game update loop --- game.update = function () { if (gameOver) return; // Power-up timer if (powerupActive) { powerupTimer--; if (powerupTimer <= 0) { deactivatePowerUp(); } } // --- AI Snake logic --- // Update AI snakes for (var i = aiSnakes.length - 1; i >= 0; --i) { var ai = aiSnakes[i]; if (ai.update) ai.update(); // Check if AI snake collides with any player tail segment (not head) // Only if not already despawning if (!ai._despawn && snake.length > 4) { // Start from segment 4 (skip head and first 3 for leniency) for (var s = 4; s < snake.length; ++s) { var seg = snake[s]; // Only check if segment exists and not destroyed if (seg && !seg.destroyed) { // Track last intersection state for this ai/segment pair if (!ai.lastTailIntersect) ai.lastTailIntersect = {}; if (ai.lastTailIntersect[s] === undefined) ai.lastTailIntersect[s] = false; var nowIntersect = ai.intersects(seg); if (!ai.lastTailIntersect[s] && nowIntersect) { // AI snake dies! LK.effects.flashObject(ai, 0xff0000, 600); ai._despawn = true; // Optionally, give player a score bonus for killing AI updateScore(score + 30); break; } ai.lastTailIntersect[s] = nowIntersect; } } } // Remove if marked for despawn if (ai._despawn) { ai.destroy(); aiSnakes.splice(i, 1); } } // Spawn a new AI snake less frequently (about every 14-18 seconds, only one at a time) if (aiSnakes.length === 0 && LK.ticks > 60 * 5 && LK.ticks % (60 * (14 + Math.floor(Math.random() * 5))) === 0 && food && !food.isRainbow) { // Only spawn if food exists and is not rainbow (AI doesn't steal rainbow food) var avoidList = []; for (var j = 0; j < snake.length; ++j) avoidList.push(snake[j]); for (var k = 0; k < obstacles.length; ++k) avoidList.push(obstacles[k]); // Spawn at random edge var edge = Math.floor(Math.random() * 4); var pos; if (edge === 0) { // left pos = { x: 120, y: 200 + Math.random() * (2732 - 400) }; } else if (edge === 1) { // right pos = { x: 2048 - 120, y: 200 + Math.random() * (2732 - 400) }; } else if (edge === 2) { // top pos = { x: 200 + Math.random() * (2048 - 400), y: 120 }; } else { // bottom pos = { x: 200 + Math.random() * (2048 - 400), y: 2732 - 120 }; } // Avoid spawning on snake or obstacles var ok = true; for (var i = 0; i < avoidList.length; ++i) { var obj = avoidList[i]; var dx = obj.x - pos.x; var dy = obj.y - pos.y; if (dx * dx + dy * dy < 200 * 200) { ok = false; break; } } if (ok) { var ai = new AISnake(); ai.x = pos.x; ai.y = pos.y; aiSnakes.push(ai); game.addChild(ai); } } // Update obstacles (moving, static, and spike) for (var i = obstacles.length - 1; i >= 0; --i) { if (obstacles[i].update) obstacles[i].update(); // Remove moving obstacles that have been marked for despawn if (obstacles[i] instanceof MovingObstacle && obstacles[i]._despawn) { obstacles[i].destroy(); obstacles.splice(i, 1); continue; } // Remove static obstacles that have been marked for despawn if (obstacles[i] instanceof Obstacle && obstacles[i]._despawn) { obstacles[i].destroy(); obstacles.splice(i, 1); continue; } // Remove spike obstacles that have been marked for despawn if (typeof SpikeObstacle !== "undefined" && obstacles[i] instanceof SpikeObstacle && obstacles[i]._despawn) { obstacles[i].destroy(); obstacles.splice(i, 1); continue; } } // Obstacles are static, no update needed // Snake direction: smooth turn toward nextDir var d = snakeNextDir - snakeDir; while (d > Math.PI) d -= 2 * Math.PI; while (d < -Math.PI) d += 2 * Math.PI; if (Math.abs(d) > 0.01) { var turn = Math.sign(d) * Math.min(Math.abs(d), SNAKE_TURN_ANGLE); snakeDir += turn; } // Move snake head var head = snake[0]; var prevPos = { x: head.x, y: head.y }; head.x += Math.cos(snakeDir) * snakeSpeed; head.y += Math.sin(snakeDir) * snakeSpeed; // Clamp to bounds (leave 100px margin for UI) head.x = Math.max(100 + head.asset.width / 2, Math.min(2048 - 100 - head.asset.width / 2, head.x)); head.y = Math.max(100 + head.asset.height / 2, Math.min(2732 - 100 - head.asset.height / 2, head.y)); // Move body segments to follow for (var i = 1; i < snake.length; ++i) { var seg = snake[i]; var target = snake[i - 1]; var dx = target.x - seg.x; var dy = target.y - seg.y; var dist = Math.sqrt(dx * dx + dy * dy); var desired = snakeSpeed * 0.9; if (dist > desired) { var move = (dist - desired) * 0.5; seg.x += dx / dist * move; seg.y += dy / dist * move; } } // Grow snake if needed if (snakeGrow > 0) { var tail = snake[snake.length - 1]; var newSeg = new SnakeSegment(); newSeg.setType('body'); newSeg.x = tail.x; newSeg.y = tail.y; game.addChild(newSeg); snake.push(newSeg); snakeGrow--; } // Check collision with food if (food && head.intersects(food)) { LK.getSound('eat').play(); if (food.isRainbow) { updateScore(score + FOOD_SCORE * 10); snakeGrow += 12; // Flash the whole snake rainbow for (var i = 0; i < snake.length; ++i) { LK.effects.flashObject(snake[i], 0xffffff, 800); } } else if (food.isPurple) { updateScore(score + FOOD_SCORE * 3); snakeGrow += 6; } else { updateScore(score + FOOD_SCORE); snakeGrow += 2; } food.destroy(); food = null; // If an AI snake stole the food, don't respawn (AI snake update will handle it) // Only respawn food if it was NOT stolen by an AI snake this frame if (!aiSnakes.some(function (ai) { return ai.stealing; })) { spawnFood(); } // Chance to spawn powerup if (!powerup && Math.random() < 0.2) { spawnPowerUp(); } } // Check collision with powerup if (powerup && head.intersects(powerup)) { LK.getSound('powerup').play(); updateScore(score + POWERUP_SCORE); activatePowerUp(powerup.type); // Flash the screen for feedback LK.effects.flashScreen(0xf1c40f, 400); powerup.destroy(); powerup = null; } // Check collision with obstacles for (var i = 0; i < obstacles.length; ++i) { // SpikeObstacle: destroys segments it touches, reduces score if (typeof SpikeObstacle !== "undefined" && obstacles[i] instanceof SpikeObstacle) { // Check all segments (not just head) for (var s = 0; s < snake.length; ++s) { var seg = snake[s]; if (seg.intersects(obstacles[i])) { // Only destroy if not invincible if (!isInvincible) { // Flash segment and obstacle LK.effects.flashObject(seg, 0xda2cff, 400); LK.effects.flashObject(obstacles[i], 0xda2cff, 400); // Remove segment (but always keep at least 2: head + 1) if (snake.length > 2) { seg.destroy(); snake.splice(s, 1); // Reduce score, but not below zero updateScore(Math.max(0, score - 25)); // Play hit sound LK.getSound('hit').play(); // Do NOT remove spike after hit; spike stays infinitely until despawn break; // Only one segment hit per spike per frame } else { // If only 2 left, end game endGame(); return; } } else { // Invincible: just destroy spike LK.effects.flashObject(obstacles[i], 0xffffff, 300); obstacles[i].destroy(); obstacles.splice(i, 1); i--; break; } } } continue; } // Normal/moving obstacle logic if (head.intersects(obstacles[i])) { if (!isInvincible) { endGame(); return; } else { // Flash obstacle and destroy it LK.effects.flashObject(obstacles[i], 0xffffff, 300); obstacles[i].destroy(); obstacles.splice(i, 1); i--; } } } // Check collision with self (skip first 4 segments for leniency) if (!isInvincible) { for (var i = 4; i < snake.length; ++i) { if (head.intersects(snake[i])) { endGame(); return; } } } // Evolving obstacles: every 30 seconds, add a new one (max 8) if (LK.ticks % (60 * 30) === 0 && obstacles.length < 8) { var obs; // 40% chance for spike, 30% moving, 30% static var r = Math.random(); if (r < 0.40) { obs = new SpikeObstacle(); } else if (r < 0.70) { obs = new MovingObstacle(); } else { obs = new Obstacle(); } // Avoid spawning on the snake and other obstacles var avoidList = []; for (var j = 0; j < snake.length; ++j) { avoidList.push(snake[j]); } for (var k = 0; k < obstacles.length; ++k) { avoidList.push(obstacles[k]); } var pos = randomPos(300, avoidList, 140); obs.x = pos.x; obs.y = pos.y; obstacles.push(obs); game.addChild(obs); } // After 2 minutes, every 20 seconds, add a new moving obstacle (max 12) if (LK.ticks > 60 * 120 && LK.ticks % (60 * 20) === 0 && obstacles.length < 12) { var obs2 = new MovingObstacle(); // Avoid spawning on the snake and other obstacles var avoidList2 = []; for (var j = 0; j < snake.length; ++j) { avoidList2.push(snake[j]); } for (var k = 0; k < obstacles.length; ++k) { avoidList2.push(obstacles[k]); } var pos2 = randomPos(300, avoidList2, 140); obs2.x = pos2.x; obs2.y = pos2.y; obstacles.push(obs2); game.addChild(obs2); } }; // --- Game start/reset --- function startGame() { gameOver = false; updateScore(0); resetSnake(); // Remove all AI snakes for (var i = 0; i < aiSnakes.length; ++i) { aiSnakes[i].destroy(); } aiSnakes = []; spawnFood(); if (powerup) { powerup.destroy(); powerup = null; } spawnObstacles(); deactivatePowerUp(); LK.playMusic('snakebg', { fade: { start: 0, end: 1, duration: 1000 } }); } startGame(); // --- Game over/win handling is automatic by LK --- // --- Music is started on game start ---
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// AISnake (AI snake that moves toward food and steals it)
var AISnake = Container.expand(function () {
var self = Container.call(this);
self.length = 3 + Math.floor(Math.random() * 2); // 3-4 segments
self.segments = [];
self.dir = 0;
self.speed = Math.max(4, SNAKE_INIT_SPEED * 0.35); // AI snake moves much slower than player
self.target = null;
self.stealing = false;
self.lastFood = null;
// Create head
var head = self.attachAsset('aiSnakeHead', {
anchorX: 0.5,
anchorY: 0.5
});
self.asset = head;
self.segments.push(self);
// Create body segments
for (var i = 1; i < self.length; ++i) {
var seg = new Container();
seg.asset = seg.attachAsset('aiSnakeBody', {
anchorX: 0.5,
anchorY: 0.5
});
seg.x = self.x - i * 60;
seg.y = self.y;
self.addChild(seg);
self.segments.push(seg);
}
// Custom hitbox
self.hitboxScale = 0.6;
// Custom intersects for head
self.intersects = function (other) {
var scaleA = typeof self.hitboxScale === "number" ? self.hitboxScale : 1;
var scaleB = typeof other.hitboxScale === "number" ? other.hitboxScale : 1;
var ax = self.x,
ay = self.y,
aw = self.asset.width * scaleA,
ah = self.asset.height * scaleA;
var bx = other.x,
by = other.y,
bw = other.asset && other.asset.width ? other.asset.width * scaleB : 0,
bh = other.asset && other.asset.height ? other.asset.height * scaleB : 0;
var rA = Math.max(aw, ah) / 2;
var rB = Math.max(bw, bh) / 2;
var dx = ax - bx,
dy = ay - by;
return dx * dx + dy * dy < (rA + rB) * (rA + rB);
};
// Update method
self.update = function () {
// Save last position for event triggers
if (self.lastX === undefined) self.lastX = self.x;
if (self.lastY === undefined) self.lastY = self.y;
// Find food if not already targeting
if (!self.target || self.target.destroyed) {
if (typeof food !== "undefined" && food && !food.destroyed) {
self.target = food;
} else {
self.target = null;
}
}
// Move toward food using player-like movement
if (self.target) {
// Calculate desired direction
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var desiredDir = Math.atan2(dy, dx);
// Smoothly turn toward the target (like player)
var d = desiredDir - self.dir;
while (d > Math.PI) d -= 2 * Math.PI;
while (d < -Math.PI) d += 2 * Math.PI;
var turn = Math.sign(d) * Math.min(Math.abs(d), SNAKE_TURN_ANGLE);
self.dir += turn;
// Move head forward
self.x += Math.cos(self.dir) * self.speed;
self.y += Math.sin(self.dir) * self.speed;
// Clamp to bounds
self.x = Math.max(100 + self.asset.width / 2, Math.min(2048 - 100 - self.asset.width / 2, self.x));
self.y = Math.max(100 + self.asset.height / 2, Math.min(2732 - 100 - self.asset.height / 2, self.y));
// Move body segments to follow the previous segment, like player
for (var i = 1; i < self.segments.length; ++i) {
var seg = self.segments[i];
var target = self.segments[i - 1];
var sdx = target.x - seg.x;
var sdy = target.y - seg.y;
var sdist = Math.sqrt(sdx * sdx + sdy * sdy);
var desired = self.speed * 0.9;
if (sdist > desired) {
var move = (sdist - desired) * 0.5;
seg.x += sdx / sdist * move;
seg.y += sdy / sdist * move;
}
}
// Steal food if close enough
if (self.target && self.intersects(self.target)) {
self.stealing = true;
// Remove food from game
if (typeof food !== "undefined" && food === self.target) {
food.destroy();
food = null;
// Always respawn food after AI steals it
if (typeof spawnFood === "function") {
spawnFood();
}
}
self.target = null;
}
}
// Despawn if off screen or after stealing
if (self.stealing) {
// Move off screen quickly
self.x += Math.cos(self.dir) * self.speed * 2;
self.y += Math.sin(self.dir) * self.speed * 2;
if (self.x < -200 || self.x > 2048 + 200 || self.y < -200 || self.y > 2732 + 200) {
self._despawn = true;
}
}
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// Food
var Food = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('food', {
anchorX: 0.5,
anchorY: 0.5
});
self.hitboxScale = 0.6;
return self;
});
// FoodPurple
var FoodPurple = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('foodPurple', {
anchorX: 0.5,
anchorY: 0.5
});
self.hitboxScale = 0.6;
return self;
});
// FoodRainbow (rare, big bonus)
var FoodRainbow = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('food', {
anchorX: 0.5,
anchorY: 0.5
});
self.hitboxScale = 0.6;
// Animate color cycling
// Animate color cycling using tween plugin API
tween(self.asset, {
tint: 0xff0000
}, {
duration: 400,
easing: tween.linear,
onFinish: function onFinish() {
// No-op, will be repeated
}
});
// Manually cycle through rainbow colors in update (since tween can't do this directly)
self.update = function () {
var t = LK.ticks % 60 / 60;
var r = Math.floor(127 * (Math.sin(2 * Math.PI * t) + 1));
var g = Math.floor(127 * (Math.sin(2 * Math.PI * t + 2) + 1));
var b = Math.floor(127 * (Math.sin(2 * Math.PI * t + 4) + 1));
self.asset.tint = r << 16 | g << 8 | b;
};
return self;
});
// MovingObstacle
var MovingObstacle = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('obstacleMoving', {
anchorX: 0.5,
anchorY: 0.5
});
self.hitboxScale = 0.7;
// Pick a random direction and speed
var angle = Math.random() * Math.PI * 2;
var speed = 4 + Math.random() * 4;
self.vx = Math.cos(angle) * speed;
self.vy = Math.sin(angle) * speed;
self.update = function () {
// Save last position for event triggers
if (self.lastX === undefined) self.lastX = self.x;
if (self.lastY === undefined) self.lastY = self.y;
self.x += self.vx;
self.y += self.vy;
// Despawn if out of bounds (with 150px margin)
if (self.x < -150 || self.x > 2048 + 150 || self.y < -150 || self.y > 2732 + 150) {
// Mark for removal by setting a flag
self._despawn = true;
return;
}
// Bounce off walls (100px margin)
if (self.x < 100 + self.asset.width / 2 && self.vx < 0) self.vx *= -1;
if (self.x > 2048 - 100 - self.asset.width / 2 && self.vx > 0) self.vx *= -1;
if (self.y < 100 + self.asset.height / 2 && self.vy < 0) self.vy *= -1;
if (self.y > 2732 - 100 - self.asset.height / 2 && self.vy > 0) self.vy *= -1;
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// Obstacle (static)
var Obstacle = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
self.hitboxScale = 0.7;
// Add despawn timer and update method for static obstacles
self._despawnTimer = 0;
self._despawnInterval = 60 * 60; // Try to despawn every 60 seconds (3600 ticks)
self.update = function () {
if (self._despawnTimer === undefined) self._despawnTimer = 0;
self._despawnTimer++;
// Only check every _despawnInterval ticks
if (self._despawnTimer >= self._despawnInterval) {
self._despawnTimer = 0;
// 40% chance to despawn (less often than moving obstacles)
if (Math.random() < 0.4) {
self._despawn = true;
}
}
};
return self;
});
// PowerUp
var PowerUp = Container.expand(function () {
var self = Container.call(this);
self.type = 'invincible'; // or 'shrink'
self.asset = null;
self.hitboxScale = 0.6;
self.setType = function (type) {
if (self.asset) self.removeChild(self.asset);
self.type = type;
if (type === 'invincible') {
self.asset = self.attachAsset('powerupInvincible', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'shrink') {
self.asset = self.attachAsset('powerupShrink', {
anchorX: 0.5,
anchorY: 0.5
});
}
};
self.setType('invincible');
return self;
});
// Snake Segment (body or head)
var SnakeSegment = Container.expand(function () {
var self = Container.call(this);
self.isHead = false;
self.asset = null;
// Custom hitbox size (smaller than visual asset)
self.hitboxScale = 0.6; // 60% of asset size for both head and body
self.setType = function (type) {
if (self.asset) self.removeChild(self.asset);
if (type === 'head') {
self.asset = self.attachAsset('snakeHead', {
anchorX: 0.5,
anchorY: 0.5
});
self.isHead = true;
} else {
self.asset = self.attachAsset('snakeBody', {
anchorX: 0.5,
anchorY: 0.5
});
self.isHead = false;
}
};
self.setType('body');
// Custom intersects method for smaller hitbox
self.intersects = function (other) {
// Use hitboxScale for both this and other if available, else default to 1
var scaleA = typeof self.hitboxScale === "number" ? self.hitboxScale : 1;
var scaleB = typeof other.hitboxScale === "number" ? other.hitboxScale : 1;
var ax = self.x,
ay = self.y,
aw = self.asset.width * scaleA,
ah = self.asset.height * scaleA;
var bx = other.x,
by = other.y,
bw = other.asset && other.asset.width ? other.asset.width * scaleB : 0,
bh = other.asset && other.asset.height ? other.asset.height * scaleB : 0;
// Circle collision (since snake is ellipse/circle)
var rA = Math.max(aw, ah) / 2;
var rB = Math.max(bw, bh) / 2;
var dx = ax - bx;
var dy = ay - by;
return dx * dx + dy * dy < (rA + rB) * (rA + rB);
};
return self;
});
// SpikeObstacle (destroys snake segments it touches, rare spawn)
var SpikeObstacle = Container.expand(function () {
var self = Container.call(this);
// Add spike obstacle asset first (so aura is in front)
self.asset = self.attachAsset('spikeObstacle', {
anchorX: 0.5,
anchorY: 0.5
});
// Add spike aura in front of the spike asset
self.aura = self.attachAsset('spikeAura', {
anchorX: 0.5,
anchorY: 0.5
});
self.aura.alpha = 0.35;
self.hitboxScale = 0.7;
// Add despawn timer and update method for static obstacles
self._despawnTimer = 0;
self._despawnInterval = 60 * 45; // Try to despawn every 45 seconds (less often than normal obstacles)
self.update = function () {
if (self._despawnTimer === undefined) self._despawnTimer = 0;
self._despawnTimer++;
// Only check every _despawnInterval ticks
if (self._despawnTimer >= self._despawnInterval) {
self._despawnTimer = 0;
// 60% chance to despawn (spikes are rare, so despawn more often)
if (Math.random() < 0.6) {
self._despawn = true;
}
}
// Animate aura pulse
if (self.aura) {
var t = LK.ticks % 60 / 60;
self.aura.alpha = 0.25 + 0.15 * Math.sin(2 * Math.PI * t);
var scale = 1.05 + 0.08 * Math.sin(2 * Math.PI * t);
self.aura.scaleX = scale;
self.aura.scaleY = scale;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Music
// Sounds
// Obstacles
// Power-ups
// Food
// Snake head and body
// --- Game constants ---
var SNAKE_INIT_LENGTH = 5;
var SNAKE_INIT_SPEED = 16; // pixels per tick
var SNAKE_MIN_SPEED = 8;
var SNAKE_MAX_SPEED = 40;
var SNAKE_TURN_ANGLE = Math.PI / 18; // 10 degrees per input
var FOOD_SCORE = 10;
var POWERUP_SCORE = 25;
var POWERUP_DURATION = 300; // ticks (5 seconds)
var OBSTACLE_COUNT = 3;
var OBSTACLE_SPEED = 6;
// --- Game state ---
var snake = [];
var aiSnakes = []; // Array of AI snakes
var snakeDir = 0; // in radians, 0 = right
var snakeNextDir = 0;
var snakeSpeed = SNAKE_INIT_SPEED;
var snakeGrow = 0;
var food = null;
var powerup = null;
var powerupActive = false;
var powerupType = null;
var powerupTimer = 0;
var obstacles = [];
var score = 0;
var isInvincible = false;
var lastTouch = null;
var dragStart = null;
var dragAngle = null;
var dragActive = false;
var gameOver = false;
// --- GUI ---
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Helper functions ---
function randomPos(margin, avoidList, avoidRadius) {
margin = margin || 150;
avoidList = avoidList || [];
avoidRadius = avoidRadius || 120; // Default: avoid within 120px of any avoidList item
var tries = 0;
while (tries < 50) {
var x = margin + Math.random() * (2048 - 2 * margin);
var y = margin + Math.random() * (2732 - 2 * margin);
var ok = true;
for (var i = 0; i < avoidList.length; ++i) {
var obj = avoidList[i];
var dx = obj.x - x;
var dy = obj.y - y;
if (dx * dx + dy * dy < avoidRadius * avoidRadius) {
ok = false;
break;
}
}
if (ok) {
return {
x: x,
y: y
};
}
tries++;
}
// Fallback: just return a random position if we can't find a good one
return {
x: margin + Math.random() * (2048 - 2 * margin),
y: margin + Math.random() * (2732 - 2 * margin)
};
}
function dist2(a, b) {
var dx = a.x - b.x,
dy = a.y - b.y;
return dx * dx + dy * dy;
}
function angleBetween(a, b) {
return Math.atan2(b.y - a.y, b.x - a.x);
}
function wrapPos(pos) {
// Not used, but could be for wrap-around
if (pos.x < 0) pos.x += 2048;
if (pos.x > 2048) pos.x -= 2048;
if (pos.y < 0) pos.y += 2732;
if (pos.y > 2732) pos.y -= 2732;
}
function spawnFood() {
if (food) food.destroy();
// 5% chance for rainbow food, 20% for purple, else normal
var r = Math.random();
if (r < 0.05) {
food = new FoodRainbow();
food.isRainbow = true;
food.isPurple = false;
} else if (r < 0.25) {
food = new FoodPurple();
food.isPurple = true;
food.isRainbow = false;
} else {
food = new Food();
food.isPurple = false;
food.isRainbow = false;
}
// Avoid spawning on the snake and obstacles
var avoidList = [];
for (var i = 0; i < snake.length; ++i) {
avoidList.push(snake[i]);
}
// Also avoid obstacles
for (var i = 0; i < obstacles.length; ++i) {
avoidList.push(obstacles[i]);
}
var pos = randomPos(200, avoidList, 120);
food.x = pos.x;
food.y = pos.y;
game.addChild(food);
}
function spawnPowerUp() {
if (powerup) powerup.destroy();
powerup = new PowerUp();
// Avoid spawning on the snake
var avoidList = [];
for (var i = 0; i < snake.length; ++i) {
avoidList.push(snake[i]);
}
var pos = randomPos(200, avoidList, 120);
// 67% invincible, 33% shrink
var r = Math.random();
var t;
if (r < 0.67) t = 'invincible';else t = 'shrink';
powerup.setType(t);
powerup.x = pos.x;
powerup.y = pos.y;
game.addChild(powerup);
}
function spawnObstacles() {
for (var i = 0; i < obstacles.length; ++i) {
obstacles[i].destroy();
}
obstacles = [];
// Avoid spawning on the snake
var avoidList = [];
for (var j = 0; j < snake.length; ++j) {
avoidList.push(snake[j]);
}
for (var i = 0; i < OBSTACLE_COUNT; ++i) {
var obs = new Obstacle();
var pos = randomPos(300, avoidList, 140);
obs.x = pos.x;
obs.y = pos.y;
// Add this obstacle to avoidList so next ones don't overlap
avoidList.push(obs);
obstacles.push(obs);
game.addChild(obs);
}
}
function resetSnake() {
for (var i = 0; i < snake.length; ++i) {
snake[i].destroy();
}
snake = [];
var startX = 2048 / 2,
startY = 2732 / 2;
for (var i = 0; i < SNAKE_INIT_LENGTH; ++i) {
var seg = new SnakeSegment();
if (i === 0) {
seg.setType('head');
// Head spawns in front of the body, not on top of the first body segment
seg.x = startX;
seg.y = startY;
} else {
seg.setType('body');
// Each body segment spawns behind the previous one, extending left from the head
seg.x = startX - i * snakeSpeed * 1.2;
seg.y = startY;
}
game.addChild(seg);
snake.push(seg);
}
snakeDir = 0;
snakeNextDir = 0;
snakeSpeed = SNAKE_INIT_SPEED;
snakeGrow = 0;
isInvincible = false;
powerupActive = false;
powerupType = null;
powerupTimer = 0;
}
function updateScore(val) {
score = val;
scoreTxt.setText(score);
LK.setScore(score);
}
function activatePowerUp(type) {
powerupActive = true;
powerupType = type;
powerupTimer = POWERUP_DURATION;
if (type === 'invincible') {
isInvincible = true;
// Flash snake
for (var i = 0; i < snake.length; ++i) {
LK.effects.flashObject(snake[i], 0xf1c40f, 500);
}
} else if (type === 'shrink') {
// Remove 3 segments from the tail, but always keep at least 5
var minLen = 5;
var removeCount = Math.min(3, snake.length - minLen);
for (var i = 0; i < removeCount; ++i) {
var seg = snake.pop();
seg.destroy();
}
}
}
function deactivatePowerUp() {
powerupActive = false;
powerupType = null;
powerupTimer = 0;
isInvincible = false;
}
function endGame() {
if (gameOver) return;
gameOver = true;
LK.effects.flashScreen(0xff0000, 1000);
LK.getSound('hit').play();
LK.showGameOver();
}
// --- Input handling (touch/drag to steer) ---
game.down = function (x, y, obj) {
// Only allow drag from outside top left 100x100
if (x < 100 && y < 100) return;
dragStart = {
x: x,
y: y
};
dragAngle = null;
dragActive = true;
};
game.move = function (x, y, obj) {
if (!dragActive) return;
if (!dragStart) return;
// Calculate angle from snake head to drag point
var head = snake[0];
var dx = x - head.x;
var dy = y - head.y;
if (dx * dx + dy * dy < 100) return; // Ignore tiny drags
var angle = Math.atan2(dy, dx);
snakeNextDir = angle;
dragAngle = angle;
};
game.up = function (x, y, obj) {
dragActive = false;
dragStart = null;
dragAngle = null;
};
// --- Main game update loop ---
game.update = function () {
if (gameOver) return;
// Power-up timer
if (powerupActive) {
powerupTimer--;
if (powerupTimer <= 0) {
deactivatePowerUp();
}
}
// --- AI Snake logic ---
// Update AI snakes
for (var i = aiSnakes.length - 1; i >= 0; --i) {
var ai = aiSnakes[i];
if (ai.update) ai.update();
// Check if AI snake collides with any player tail segment (not head)
// Only if not already despawning
if (!ai._despawn && snake.length > 4) {
// Start from segment 4 (skip head and first 3 for leniency)
for (var s = 4; s < snake.length; ++s) {
var seg = snake[s];
// Only check if segment exists and not destroyed
if (seg && !seg.destroyed) {
// Track last intersection state for this ai/segment pair
if (!ai.lastTailIntersect) ai.lastTailIntersect = {};
if (ai.lastTailIntersect[s] === undefined) ai.lastTailIntersect[s] = false;
var nowIntersect = ai.intersects(seg);
if (!ai.lastTailIntersect[s] && nowIntersect) {
// AI snake dies!
LK.effects.flashObject(ai, 0xff0000, 600);
ai._despawn = true;
// Optionally, give player a score bonus for killing AI
updateScore(score + 30);
break;
}
ai.lastTailIntersect[s] = nowIntersect;
}
}
}
// Remove if marked for despawn
if (ai._despawn) {
ai.destroy();
aiSnakes.splice(i, 1);
}
}
// Spawn a new AI snake less frequently (about every 14-18 seconds, only one at a time)
if (aiSnakes.length === 0 && LK.ticks > 60 * 5 && LK.ticks % (60 * (14 + Math.floor(Math.random() * 5))) === 0 && food && !food.isRainbow) {
// Only spawn if food exists and is not rainbow (AI doesn't steal rainbow food)
var avoidList = [];
for (var j = 0; j < snake.length; ++j) avoidList.push(snake[j]);
for (var k = 0; k < obstacles.length; ++k) avoidList.push(obstacles[k]);
// Spawn at random edge
var edge = Math.floor(Math.random() * 4);
var pos;
if (edge === 0) {
// left
pos = {
x: 120,
y: 200 + Math.random() * (2732 - 400)
};
} else if (edge === 1) {
// right
pos = {
x: 2048 - 120,
y: 200 + Math.random() * (2732 - 400)
};
} else if (edge === 2) {
// top
pos = {
x: 200 + Math.random() * (2048 - 400),
y: 120
};
} else {
// bottom
pos = {
x: 200 + Math.random() * (2048 - 400),
y: 2732 - 120
};
}
// Avoid spawning on snake or obstacles
var ok = true;
for (var i = 0; i < avoidList.length; ++i) {
var obj = avoidList[i];
var dx = obj.x - pos.x;
var dy = obj.y - pos.y;
if (dx * dx + dy * dy < 200 * 200) {
ok = false;
break;
}
}
if (ok) {
var ai = new AISnake();
ai.x = pos.x;
ai.y = pos.y;
aiSnakes.push(ai);
game.addChild(ai);
}
}
// Update obstacles (moving, static, and spike)
for (var i = obstacles.length - 1; i >= 0; --i) {
if (obstacles[i].update) obstacles[i].update();
// Remove moving obstacles that have been marked for despawn
if (obstacles[i] instanceof MovingObstacle && obstacles[i]._despawn) {
obstacles[i].destroy();
obstacles.splice(i, 1);
continue;
}
// Remove static obstacles that have been marked for despawn
if (obstacles[i] instanceof Obstacle && obstacles[i]._despawn) {
obstacles[i].destroy();
obstacles.splice(i, 1);
continue;
}
// Remove spike obstacles that have been marked for despawn
if (typeof SpikeObstacle !== "undefined" && obstacles[i] instanceof SpikeObstacle && obstacles[i]._despawn) {
obstacles[i].destroy();
obstacles.splice(i, 1);
continue;
}
}
// Obstacles are static, no update needed
// Snake direction: smooth turn toward nextDir
var d = snakeNextDir - snakeDir;
while (d > Math.PI) d -= 2 * Math.PI;
while (d < -Math.PI) d += 2 * Math.PI;
if (Math.abs(d) > 0.01) {
var turn = Math.sign(d) * Math.min(Math.abs(d), SNAKE_TURN_ANGLE);
snakeDir += turn;
}
// Move snake head
var head = snake[0];
var prevPos = {
x: head.x,
y: head.y
};
head.x += Math.cos(snakeDir) * snakeSpeed;
head.y += Math.sin(snakeDir) * snakeSpeed;
// Clamp to bounds (leave 100px margin for UI)
head.x = Math.max(100 + head.asset.width / 2, Math.min(2048 - 100 - head.asset.width / 2, head.x));
head.y = Math.max(100 + head.asset.height / 2, Math.min(2732 - 100 - head.asset.height / 2, head.y));
// Move body segments to follow
for (var i = 1; i < snake.length; ++i) {
var seg = snake[i];
var target = snake[i - 1];
var dx = target.x - seg.x;
var dy = target.y - seg.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var desired = snakeSpeed * 0.9;
if (dist > desired) {
var move = (dist - desired) * 0.5;
seg.x += dx / dist * move;
seg.y += dy / dist * move;
}
}
// Grow snake if needed
if (snakeGrow > 0) {
var tail = snake[snake.length - 1];
var newSeg = new SnakeSegment();
newSeg.setType('body');
newSeg.x = tail.x;
newSeg.y = tail.y;
game.addChild(newSeg);
snake.push(newSeg);
snakeGrow--;
}
// Check collision with food
if (food && head.intersects(food)) {
LK.getSound('eat').play();
if (food.isRainbow) {
updateScore(score + FOOD_SCORE * 10);
snakeGrow += 12;
// Flash the whole snake rainbow
for (var i = 0; i < snake.length; ++i) {
LK.effects.flashObject(snake[i], 0xffffff, 800);
}
} else if (food.isPurple) {
updateScore(score + FOOD_SCORE * 3);
snakeGrow += 6;
} else {
updateScore(score + FOOD_SCORE);
snakeGrow += 2;
}
food.destroy();
food = null;
// If an AI snake stole the food, don't respawn (AI snake update will handle it)
// Only respawn food if it was NOT stolen by an AI snake this frame
if (!aiSnakes.some(function (ai) {
return ai.stealing;
})) {
spawnFood();
}
// Chance to spawn powerup
if (!powerup && Math.random() < 0.2) {
spawnPowerUp();
}
}
// Check collision with powerup
if (powerup && head.intersects(powerup)) {
LK.getSound('powerup').play();
updateScore(score + POWERUP_SCORE);
activatePowerUp(powerup.type);
// Flash the screen for feedback
LK.effects.flashScreen(0xf1c40f, 400);
powerup.destroy();
powerup = null;
}
// Check collision with obstacles
for (var i = 0; i < obstacles.length; ++i) {
// SpikeObstacle: destroys segments it touches, reduces score
if (typeof SpikeObstacle !== "undefined" && obstacles[i] instanceof SpikeObstacle) {
// Check all segments (not just head)
for (var s = 0; s < snake.length; ++s) {
var seg = snake[s];
if (seg.intersects(obstacles[i])) {
// Only destroy if not invincible
if (!isInvincible) {
// Flash segment and obstacle
LK.effects.flashObject(seg, 0xda2cff, 400);
LK.effects.flashObject(obstacles[i], 0xda2cff, 400);
// Remove segment (but always keep at least 2: head + 1)
if (snake.length > 2) {
seg.destroy();
snake.splice(s, 1);
// Reduce score, but not below zero
updateScore(Math.max(0, score - 25));
// Play hit sound
LK.getSound('hit').play();
// Do NOT remove spike after hit; spike stays infinitely until despawn
break; // Only one segment hit per spike per frame
} else {
// If only 2 left, end game
endGame();
return;
}
} else {
// Invincible: just destroy spike
LK.effects.flashObject(obstacles[i], 0xffffff, 300);
obstacles[i].destroy();
obstacles.splice(i, 1);
i--;
break;
}
}
}
continue;
}
// Normal/moving obstacle logic
if (head.intersects(obstacles[i])) {
if (!isInvincible) {
endGame();
return;
} else {
// Flash obstacle and destroy it
LK.effects.flashObject(obstacles[i], 0xffffff, 300);
obstacles[i].destroy();
obstacles.splice(i, 1);
i--;
}
}
}
// Check collision with self (skip first 4 segments for leniency)
if (!isInvincible) {
for (var i = 4; i < snake.length; ++i) {
if (head.intersects(snake[i])) {
endGame();
return;
}
}
}
// Evolving obstacles: every 30 seconds, add a new one (max 8)
if (LK.ticks % (60 * 30) === 0 && obstacles.length < 8) {
var obs;
// 40% chance for spike, 30% moving, 30% static
var r = Math.random();
if (r < 0.40) {
obs = new SpikeObstacle();
} else if (r < 0.70) {
obs = new MovingObstacle();
} else {
obs = new Obstacle();
}
// Avoid spawning on the snake and other obstacles
var avoidList = [];
for (var j = 0; j < snake.length; ++j) {
avoidList.push(snake[j]);
}
for (var k = 0; k < obstacles.length; ++k) {
avoidList.push(obstacles[k]);
}
var pos = randomPos(300, avoidList, 140);
obs.x = pos.x;
obs.y = pos.y;
obstacles.push(obs);
game.addChild(obs);
}
// After 2 minutes, every 20 seconds, add a new moving obstacle (max 12)
if (LK.ticks > 60 * 120 && LK.ticks % (60 * 20) === 0 && obstacles.length < 12) {
var obs2 = new MovingObstacle();
// Avoid spawning on the snake and other obstacles
var avoidList2 = [];
for (var j = 0; j < snake.length; ++j) {
avoidList2.push(snake[j]);
}
for (var k = 0; k < obstacles.length; ++k) {
avoidList2.push(obstacles[k]);
}
var pos2 = randomPos(300, avoidList2, 140);
obs2.x = pos2.x;
obs2.y = pos2.y;
obstacles.push(obs2);
game.addChild(obs2);
}
};
// --- Game start/reset ---
function startGame() {
gameOver = false;
updateScore(0);
resetSnake();
// Remove all AI snakes
for (var i = 0; i < aiSnakes.length; ++i) {
aiSnakes[i].destroy();
}
aiSnakes = [];
spawnFood();
if (powerup) {
powerup.destroy();
powerup = null;
}
spawnObstacles();
deactivatePowerUp();
LK.playMusic('snakebg', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
}
startGame();
// --- Game over/win handling is automatic by LK ---
// --- Music is started on game start ---