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 JavaScript/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 gallery (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 I'm going to use my old friend the timer. Each cycle will loop over the snake collection and increment it. The increment is determined by the direction. So if its up we -10 squares, down +10 squares, 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 relevant 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).
Unfortunately that didn't quite work, we ended up moving the entire snake, not the snakes head.
So back to the drawing board, 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 square. 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 relevant 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 column (Mod square-1 10) = Overflow from left
- If Snake is in first column (Mod square-1 10) and past snake was in last column (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 that's 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 false
- 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 also made all of the buttons transparent and positioned them over the relevant button on the background image (this might cause issues on scaling on certain phones but that's 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 exercise, so does our problem solving skills, and I find doing fun games like this the best exercise.
And it again shows that with a little creativity you can do nearly anything in LowCode.
As always the solution is available here to download and have a closer look.
Top comments (9)
This blog brings back memories of the good old, robust NOKIA phone. I love the Snake game and appreciate how youβve created a tutorial to revive it using low code
Superb!!! Can't say anything else than AMAZING!!!
I chose the simple soluion and just based the X speed on the distance between the middle of the pad and the pong divided by 4 to not make it bounce crazily from side to side π
For an advanced exercise, some trigonometry could be used I presume
The way your mind thinks is an inspiration β¨οΈ
Simply Awesome
Brings back those old good memories
I did pong in power apps a few months ago. That was also a fun project.
Anyways, good article and a nice project ππΌ
That's on my to do list, love you see what you did
https://www.linkedin.com/posts/martin-pedersen-044102161_microsoft-power-platform-med-alle-dets-v%C3%A6rkt%C3%B8jer-activity-7186082594558513153-2eiZ?utm_source=share&utm_medium=member_android
It's in danish, but the attached video shows the final product.
I ended up using multiple timers because it couldn't finish all the calculations before the next timer reset.
Pong movement, collision with sides and backwalls, collision with pads, calculating distance from pad center to pong for new direction and enemy pad movement was all the calculations needed, I believe.
Have fun when getting to it on your todo list π
Thats amazing π must be some crazy sums to calculate the angles etc