I was fortunate enough to have attend last years Power Platform conference in Las Vegas, and one of my favourite presentations was the Github Copilot demo. In it Copilot was able to create 90% of snake in JavasScript/HTML/CSS, it was very cool, but also made me think…. could I do that in a Power App 😎
My normal reason for Power App games is to learn new techniques, but this one is purely to scratch that itch. Hopefully I will learn something on the way. My requirements were:
- Snake changes direction on button press
- That change is at the body part (so the snake can curl)
- Apples increase snake length
- Snake dies if eats its own body
- In homage to Nokia we need to be styled as a feature phone
1. Setup
My approach was to have a gallery which is a 10 by 10 grid, each of the squares will be a part of the snake/apple. I create a collection to populate the galleery (colGrid) and use a template version to reset for each new game.
App-OnStart
ClearCollect(colGridTemplate, {id:1,snake:true,apple:false}
);
ForAll(Sequence(100,1,1), Patch(colGridTemplate,{id:ThisRecord.Value},{ snake:false, apple:false, id:ThisRecord.Value } )
);
ClearCollect(colGrid,colGridTemplate);
The grid states if it it is part of the snake or apple (we use this to show on the gallery).
The snake is a rectangle component within the gallery, with the fill set by if the item has a snake (If(ThisItem.snake,Color.Black,RGBA(187, 221, 140, 1))
).
2. Movement
The snake will be its own collection, that way we can add to it and position it. We position it as it stores the grid quare it is in (the id of the grid item.
Start Button-OnSelect
ClearCollect(colSnake,[35,45,55]);
Set(viDirection,10);
this shows the starting position of the snake is squares 35,45,55
For the movement Im going to use my old friend the timer. Each cycle will loop over the snake collection and increment it. The increment is determind by the direction. So if its up we -10 squares, down +10 squres, left -1 and right +1.
After we have updated the snake we first reset the board, and then loop over the snake and update each relevent board items snake value.
Timer-OnTimerEnd
ForAll(colSnake, If(ThisRecord.Value+viDirection>100, Patch(colSnake,ThisRecord,{Value:ThisRecord.Value-90}); , If(ThisRecord.Value+viDirection<1, Patch(colSnake,ThisRecord,{Value:ThisRecord.Value+90}); , Patch(colSnake,ThisRecord,{Value:ThisRecord.Value+viDirection}); ) )
); ClearCollect(colGrid,colGridTemplate);
ForAll(colSnake, Patch(colGrid,{id:ThisRecord.Value},{ snake:true } );
);
You can also see some extra logic to handle the wrap of the snake (goes off bottom screen and appears on the top).
Unfortuntaley that didnt quite work, we ended up moving the entire snake, not the snakes head.
So back to the drawing baord, the new approach would be to add a new field to the board collection that saves the direction. That way we can store multiple directions across the board, with only the head following the set direction.
First thing I realised was my snake was wrong way around (the first item should have been its head 🤦♂️ (colSnake,[55,45,35]
). Next was I could not reset the board each move, instead I would need to just update the exited square to snake:false, I did this by storing last part of the snake in a variable but moving it, then patched that grid sqaure. So I ended up with:
Timer-OnTimerEnd
//// get grid square to remove snake
Set(viClearGrid,Last(colSnake).Value);
ForAll(Sequence(CountRows(colSnake)) As item, With( { snake:Index(colSnake,item.Value) }, If(item.Value=1, Patch(colSnake,snake,{Value:snake.Value+viDirection}); , Patch(colSnake,snake,{Value:snake.Value+Index(colGrid,snake.Value).direction}); ) ) );
////fix over flows
ForAll(colSnake, If(ThisRecord.Value>100, Patch(colSnake,ThisRecord,{Value:ThisRecord.Value-90}); , If(ThisRecord.Value<1, Patch(colSnake,ThisRecord,{Value:ThisRecord.Value+90}); ) )
);
//// move snake
ForAll(colSnake, If(ThisRecord.Value=Index(colSnake,2).Value Or ThisRecord.Value=Index(colSnake,1).Value, Patch(colGrid,{id:ThisRecord.Value},{ direction:viDirection, snake:true } ) , Patch(colGrid,{id:ThisRecord.Value},{ snake:true } ) ) );
///set grid square to remove snake
Patch(colGrid,{id:viClearGrid},{ snake:false }
);
For the move snake I need to also update the board to the new direction, I updated both the first and second items because I need to leave a direction trail (first item), and the actual change in direction was on the last turn, which was before the first item moved (ie now the second item).
I also had to setup the board differently on start. For the snake to move every starting square it is on must have a direction too. So I simply loop over it and patch the relevent grid squares.
Start Button-OnSelect
Set(viDirection,10);
ClearCollect(colSnake,[55,45,35]);
ClearCollect(colGrid,colGridTemplate);
ForAll(colSnake, Patch(colGrid,{id:ThisRecord.Value},{ snake:true, direction:viDirection } )
);
Close but not quite there, I realised even though I had fixed the top and bottom overflow, I had forgotten the sides. So it would always move up a row on the right and drop a row on the left. To fix this I created a clone of the snake before moving it (colSnakePast), I can then check where the snake came from and add 2 more checks:
- If Snake is in last column (Mod square 10) and past snake was in first columm (Mod square-1 10) = Overflow from left
- If Snake is in first column (Mod square-1 10) and past snake was in last columm (Mod square 10) = Overflow from right
ForAll(Sequence(CountRows(colSnake)) As item, With( { snake:Index(colSnake,item.Value), pastSnake:Index(colSnakePast,item.Value).Value }, If( snake.Value>100, Patch(colSnake,snake,{Value:snake.Value-100}), snake.Value<0, Patch(colSnake,snake,{Value:snake.Value+100}), Mod(snake.Value-1,10)=0 And Mod(pastSnake,10)=0,Patch(colSnake,snake,{Value:snake.Value-10}), Mod(snake.Value,10)=0 And Mod(pastSnake-1,10)=0, Patch(colSnake,snake,{Value:snake.Value+10}) ); )
);
I also tied it up with a with 😎
3. Collision Detector
Now our snake is moving ok we need to add that when it hits itself it dies. Luckily our current setup is almost there, we just need to filter our pastSnake collection to see if it contains the head square. That result can then either trigger game over or continue with our movement code.
Timer-OnTimerEnd
If(!IsEmpty(Filter(colSnakePast,Value=First(colSnake).Value)), Set(vbStart,false); Notify("Game Over");
, ////fix over flows ////move snake ////set grid square to remove snake
)
4. Eat a Apple to grow
Now we need to add the difficulty, and thats done with apples. Eating a apple increases the length of the snake, and we also need to randomly place them.
The eating is easy, we are going to kind of cheat and not actually add to the snake. Instead we are not going to delete the previous tail square.
So we wrap our ‘set grid square to remove snake’ inside a condition, if square is Apple, remove Apple, else delete last snake square:
Timer-OnTimerEnd
With( { head:Index(colGrid,First(colSnake).Value).id, tail:Last(colSnakePast).Value }, If(Index(colGrid,head).apple, Patch(colGrid,{id:head}, { apple:false } ); Collect(colSnake,tail); , ///set grid square to remove snake Patch(colGrid,{id:tail}, { snake:false } ); ); );
Next we need to add the apples. This can be very similar to the snake, we create a collection and loop over adding them. There are a couple of additional points:
- Deleting old apples – loop over collection and change board square apple to fales
- Add apples – create 3 random numbers between 1 and 100, loop over and if not snake, change board square apple to true
- When – I do on every 24th cycle, but this could be nth or just plan random.
Timer-OnTimerEnd
////create apples If(Mod(viApples,24)=0, ForAll(colApples, Patch(colGrid,Index(colGrid,ThisRecord.Value),{apple:false}) ); ClearCollect(colApples,[RandBetween(1,100),RandBetween(1,100),RandBetween(1,100)]); ForAll(colApples, If(!Index(colGrid,ThisRecord.Value).snake, Patch(colGrid,Index(colGrid,ThisRecord.Value),{apple:true}) ) ) );
5. The Icing on the Cake
Everything is working now so we just need to add the right look and few extra features.
For the look I added a Nokia phone as a background and then removed the padding on the board gallery. I alson made all of the buttons transparent and postioned them over the relevent button on the backgound image (this might cause issues on scaling on certain phones but thats a UAT problem 😎)
Finally I added a label at the top and used the snake length as a level indicator.
I always try to learn from innovation projects like this, but this one was more for fun then learning. But like a muscle needs evercise, so does our problem solving skills, and I find doing fun games like this the best exercies.
And it again shows that with a little creativity you can do nearly anything in LowCode.
As always the solution is availble here to download and have a closer look.