-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[Sprig App] glowySheep #3490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[Sprig App] glowySheep #3490
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces "Glowy Sheep", a puzzle/simulation game where players shepherd healthy sheep into a pen while avoiding radioactive sheep that can infect the herd. The game features 9 progressive levels with increasing difficulty and various spatial challenges.
Key changes:
- Complete game implementation with sprite definitions, movement mechanics, and infection system
- 9 levels with different layouts featuring walls, pens, healthy sheep, and radioactive sheep
- AI behavior for sheep (pen attraction, player avoidance, random movement) and infection mechanics
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function getClosestPen(s) { | ||
| const allPens = getAll(pen) | ||
| let closest = null | ||
| let minDst = 999 |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The magic number 999 is used as an initial maximum distance value. This should be defined as a named constant like MAX_DISTANCE or INFINITY_DISTANCE to clarify its purpose and make the code more maintainable.
| if (nearestPen && nearestPen.dist < 5) { | ||
| moved = moveToTarget(s, nearestPen.tile.x, nearestPen.tile.y) | ||
| } | ||
| } | ||
|
|
||
| if (!moved) { | ||
| const distToPlayer = Math.abs(p.x - s.x) + Math.abs(p.y - s.y) | ||
|
|
||
| if (distToPlayer < 4) { | ||
| let dx = 0; let dy = 0; | ||
| if (p.x < s.x) dx = 1; else if (p.x > s.x) dx = -1; | ||
| if (p.y < s.y) dy = 1; else if (p.y > s.y) dy = -1; | ||
|
|
||
| if (!tryMove(s, dx, dy)) { | ||
| if (!tryMove(s, dx, 0)) { | ||
| if (!tryMove(s, 0, dy)) { | ||
| if (dx !== 0 && !tryMove(s, 0, 1)) tryMove(s, 0, -1); | ||
| else if (dy !== 0 && !tryMove(s, 1, 0)) tryMove(s, -1, 0); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| else { | ||
| if (Math.random() < 0.15) { |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic numbers 5, 4, and 0.15 appear throughout the code without explanation. These control important game mechanics (pen attraction distance, player influence range, random movement probability) and should be defined as named constants at the top of the file (e.g., PEN_ATTRACTION_DISTANCE = 5, PLAYER_INFLUENCE_RANGE = 4, RANDOM_MOVE_PROBABILITY = 0.15) for easier tuning and better code clarity.
| if (dx === 0 && dy === 0) return false; | ||
| const destX = s.x + dx; | ||
| const destY = s.y + dy; | ||
| if (destX < 0 || destX > 15 || destY < 0 || destY > 15) return false; |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The boundary check destX > 15 || destY > 15 assumes a fixed map size of 16x16, but the actual maps in this game are 14x9 (14 columns, 9 rows). This will allow sheep to move into positions that don't exist on the actual map, potentially causing unexpected behavior. The bounds should match the actual map dimensions or use width() and height() methods if available in the Sprig API.
| if (destX < 0 || destX > 15 || destY < 0 || destY > 15) return false; | |
| // Map is 14 columns (0-13), 9 rows (0-8) | |
| if (destX < 0 || destX > 13 || destY < 0 || destY > 8) return false; |
| const bads = getAll(radSheep) | ||
| const goods = getAll(sheep) | ||
| goods.forEach(g => { | ||
| bads.forEach(b => { |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The variable names bads and goods are informal and unclear. Consider using more descriptive names like radioactiveSheep and healthySheep to improve code readability.
| const bads = getAll(radSheep) | |
| const goods = getAll(sheep) | |
| goods.forEach(g => { | |
| bads.forEach(b => { | |
| const radioactiveSheep = getAll(radSheep) | |
| const healthySheep = getAll(sheep) | |
| healthySheep.forEach(g => { | |
| radioactiveSheep.forEach(b => { |
| if (!levelActive) return; | ||
| levelActive = false | ||
| addText(msg, {x: 2, y: 7, color: color`6`}) | ||
| setTimeout(() => loadLevel(currentLevel), 2000) |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using setTimeout in a game loop can cause issues if the player triggers multiple win/loss conditions rapidly. If the player loses twice in quick succession (e.g., by moving quickly), multiple timeouts will be queued, causing the level to reload multiple times. Consider adding a check to prevent multiple simultaneous timeouts or clearing existing timeouts before setting new ones.
| targets.forEach(t => t.remove()) | ||
| addSprite(x, y, radSheep) | ||
| addText("INFECTED!", {x: 4, y: 14, color: color`5`}) | ||
| setTimeout(() => clearText(), 800) |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The clearText() call in sfx_infect happens after an 800ms delay, but this text could be overwritten by subsequent infection events or win/loss messages before the timeout completes. This creates a race condition where text management becomes unpredictable. Consider managing text state more explicitly or ensuring only one message is active at a time.
| if (dist <= 1) sfx_infect(g.x, g.y); | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| function sfx_infect(x, y) { |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function name sfx_infect is misleading - it suggests this function only plays sound effects, but it actually handles the core infection logic (removing healthy sheep, adding radioactive sheep, and displaying text). Consider renaming to infectSheep or convertToRadioactive to better reflect its actual purpose.
| if (dist <= 1) sfx_infect(g.x, g.y); | |
| }) | |
| }) | |
| } | |
| function sfx_infect(x, y) { | |
| if (dist <= 1) infectSheep(g.x, g.y); | |
| }) | |
| }) | |
| } | |
| function infectSheep(x, y) { |
| levelActive = false | ||
| currentLevel++ | ||
| addText("LEVEL CLEAR", {x: 4, y: 7, color: color`3`}) | ||
| setTimeout(() => loadLevel(currentLevel), 2000) |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same setTimeout issue exists here. Multiple rapid wins could queue multiple level transitions. While the levelActive flag helps prevent re-entry, it doesn't prevent multiple timeouts from being queued before the first one executes.
| let minDst = 999 | ||
|
|
||
| allPens.forEach(p => { | ||
| const d = Math.abs(p.x - s.x) + Math.abs(p.y - s.y) | ||
| if (d < minDst) { | ||
| minDst = d | ||
| closest = p | ||
| } | ||
| }) | ||
|
|
||
| if (closest) return { tile: closest, dist: minDst } |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The variable minDst is an unclear abbreviation. Consider using the full word minDistance for better readability.
| let minDst = 999 | |
| allPens.forEach(p => { | |
| const d = Math.abs(p.x - s.x) + Math.abs(p.y - s.y) | |
| if (d < minDst) { | |
| minDst = d | |
| closest = p | |
| } | |
| }) | |
| if (closest) return { tile: closest, dist: minDst } | |
| let minDistance = 999 | |
| allPens.forEach(p => { | |
| const d = Math.abs(p.x - s.x) + Math.abs(p.y - s.y) | |
| if (d < minDistance) { | |
| minDistance = d | |
| closest = p | |
| } | |
| }) | |
| if (closest) return { tile: closest, dist: minDistance } |
Author name
Author: atomicra1n
About your game
What is your game about?
You are a shepherd who must get all sheep into the pen. However, your herd has been contaminated by radioactive sheep who will try to infect your herd! Get as many sheep inside the pen and make sure that no radioactive sheep enters the pen.
How do you play your game?
Simply move using WASD, you can restart the level with J. Get as many healthy sheep inside the pen as possible. If a radioactive sheep enters the pen, or all of your sheep get infected, you will lose and the level will restart.