DEV Community

Cover image for Creating Snake Game In Power Apps
david wyatt
david wyatt Subscriber

Posted on • Updated on

Creating Snake Game In Power Apps

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

nokia snake


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);
Enter fullscreen mode Exit fullscreen mode

The grid states if it it is part of the snake or apple (we use this to show on the gallery).

power apps gallery item

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);
Enter fullscreen mode Exit fullscreen mode

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
        }
    );
);
Enter fullscreen mode Exit fullscreen mode

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.
not working gif

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
    }
);
Enter fullscreen mode Exit fullscreen mode

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
        }
    )
);      
Enter fullscreen mode Exit fullscreen mode

not working again

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})
        );       
    )
);
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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
                }
            );
        );
    );
Enter fullscreen mode Exit fullscreen mode

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})
            )
        )
    );
Enter fullscreen mode Exit fullscreen mode

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.

snake demo


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)

Collapse
 
balagmadhu profile image
Bala Madhusoodhanan

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

Chri

Collapse
 
jaloplo profile image
Jaime LΓ³pez

Superb!!! Can't say anything else than AMAZING!!!

Collapse
 
martin_pedersen_97d58f9cf profile image
Martin Pedersen

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

Collapse
 
datadragyn profile image
DataDragyn

The way your mind thinks is an inspiration ✨️

Collapse
 
jeet0127 profile image
Jeet Majumdar

Simply Awesome
Brings back those old good memories

Collapse
 
martin_pedersen_97d58f9cf profile image
Martin Pedersen

I did pong in power apps a few months ago. That was also a fun project.
Anyways, good article and a nice project πŸ‘πŸΌ

Collapse
 
wyattdave profile image
david wyatt

That's on my to do list, love you see what you did

Collapse
 
martin_pedersen_97d58f9cf profile image
Martin Pedersen

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 😁

Collapse
 
wyattdave profile image
david wyatt

Thats amazing πŸ‘ must be some crazy sums to calculate the angles etc