From c71c1c767f5f430b8d08e0d070a7252fc1de7673 Mon Sep 17 00:00:00 2001 From: nickgnd Date: Sat, 15 Feb 2020 11:19:47 +0100 Subject: [PATCH 1/4] [#26] Allow snake to eat From 2a7ff111d862fc3b5d706c18ba24c2ab20592cb7 Mon Sep 17 00:00:00 2001 From: nickgnd Date: Mon, 24 Feb 2020 08:58:53 +0100 Subject: [PATCH 2/4] Wip --- docs/tutorial/06-allow-snake-to-eat.md | 166 +++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 docs/tutorial/06-allow-snake-to-eat.md diff --git a/docs/tutorial/06-allow-snake-to-eat.md b/docs/tutorial/06-allow-snake-to-eat.md new file mode 100644 index 0000000..67a451e --- /dev/null +++ b/docs/tutorial/06-allow-snake-to-eat.md @@ -0,0 +1,166 @@ +### 6. Allow snake to eat + +We have the food, but our snake can't eat it yet, and we don't want to starve our little friend. + +The goal of this chapter is to allow the snake to eat the food and grow, but let's first quickly recap how it works. The snake eats the food when it overlaps it with its head, as soon the snake eats it, it will grow of one unit (a new tile will be appended to its body) and a new food pellet will be placed in the snake world. + +In other words, we need to check if the snake's head has the same coordinates of the pellet after every movement. The function `move_snake/1` looks the natural place where to execute this check. + +```elixir +defp move_snake(%{snake: snake} = state) do + %{body: body, size: size, direction: direction} = snake + + # New head's position + [head | _] = body + new_head = move(state, head, direction) + + # Place a new head on the tile that we want to move to + # and remove the last tile from the snake tail + new_body = List.delete_at([new_head | body], -1) + + state + |> put_in([:objects, :snake, :body], new_body) + |> maybe_eat_food(new_head) +end +``` + +πŸ‘†Let's add it at the end of the state update pipeline, the pipe operator `|>` comes in handy here πŸ’š + +Our new function `maybe_eat_food/2` receives: + +- The current state as 1st argument +- The snake's head coordinates as 2nd argument + +Then, in the case the snake's head overlaps the food pellet, the function will take care of: + +- Grow the snake body of one unit +- Place a new food pellet in the snake world + +or otherwise, just return the current state without any changes. + +Let's draft out the `maybe_eat_pellet/2` function and implement it step by step. + +```elixir +def maybe_eat_pellet(state = %{pellet: pellet}, snake_head) do + if (pellet == snake_head) + state + |> grow_snake() + |> place_pellet() + else + state + end +end +``` + +Growing the snake body can be tricky, let's explore all our alternatives. + +Prepending a new tile to the snake head is not a feasible solution because it will potentially lead to unexpected outcome like its dead :skull:. + +The most natural approach is to append a new tile at the end of the snake body, but how exactly? We can not simply add a new tile at the end like: `Enum.concat(body, [{x, y}])` since we don't have any information of the tail direction but only of its head. In other words, we can't infer the coordinates (`{x, y}`) of the tile to append. The only way to safely grow our snake it's to preserve its body when the snake ate the pellet. We could set a boolean flag `has_eaten` in the state and in the next game tick, don't delete the last snake's body tile when this flag is true πŸ€“. + +Remember, we set a timer at the beginning in out `init/1` function that periodically sends a message, which is intercepted by our `handle_info/2`, which in turn calls the `move_snake/1` function. That's our game tick. + +```elixir +defp move_snake(%{snake: snake} = state) do + %{body: body, size: size, direction: direction} = snake + + # New head's position + [head | _] = body + new_head = move(state, head, direction) + + # Place a new head on the tile that we want to move to + # and remove the last tile from the snake tail + new_body = List.delete_at([new_head | body], -1) + + state + |> put_in([:snake, :body], new_body) + |> maybe_eat_food(new_head) +end + +def maybe_eat_pellet(state = %{pellet: pellet}, snake_head) do + if (pellet == snake_head) + state + |> grow_snake() + |> place_pellet() + else + state + end +end + +def grow_snake(state = %{%{snake: %{size: size}}) do + put_in(state, [:snake, :has_eaten], true) +end + +def place_pellet(state = %{width: width, height: height, snake: %{body: snake_body}}) do + pellet_coords = { + Enum.random(0..(width - 1)), + Enum.random(0..(height - 1)) + } + + if pellet_coords in snake_body do + place_pellet(state) + else + put_in(state, [:objects, :pellet], pellet_coords) + end +end +``` + +Let's take a look to these two new functions: + +- `grow_snake/1` simply sets the `:has_eatan` flag to true in the state +- `place_pellet/1` computes a new pair of coordinates for the food, if the new value matches any tile in the snake's body, it recursively generate a new position until it does not overlap the snake + +We still need to update the `move_snake/1` function to use the `:has_eatan` flag. + +```elixir +defp move_snake(%{snake: snake} = state) do + %{body: body, direction: direction} = snake + + # New head's position + [head | _] = body + new_head = move(state, head, direction) + + # Place a new head on the tile that we want to move to + # and remove the last tile from the snake tail if it has not eaten any pellet + new_body = [new_head | body] + new_body = if snake.has_eaten, do: new_body, else: List.delete_at(new_body, -1) + + state + |> put_in([:snake, :body], new_body) + |> maybe_eat_food(new_head) +end +``` + +Like that, when the flag `:has_eatan` is true, the last tile from the snake's body is not removed anymore. This is the trick that allows us to grow the snake πŸ’ͺ + +And now let's run the game and see how if our snake grows when eating. + + $ mix scenic.run + +Oh snapp! Our snake is growing infinitely!! We forgot to reset the `:has_eaten` flag πŸ™ˆ + +```elixir +defp move_snake(%{snake: snake} = state) do + %{body: body, direction: direction} = snake + + # New head's position + [head | _] = body + new_head = move(state, head, direction) + + # Place a new head on the tile that we want to move to + # and remove the last tile from the snake tail if it has not eaten any pellet + new_body = [new_head | body] + new_body = if snake.has_eaten, do: new_body, else: List.delete_at(new_body, -1) + + state + |> put_in([:snake, :body], new_body) + |> put_in([::snake, :has_eaten], false) # Reset the `:has_eaten` flag before the next check + |> maybe_eat_food(new_head) +end +``` + +Ok, let's run it again, it should work like a charm now 🀞 + +TODO: + +- add gif From 567c7ddc654daa35e0949ce69ba7a1e5670528b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20G?= Date: Mon, 2 Mar 2020 09:02:24 +0100 Subject: [PATCH 3/4] Apply suggestions from code review Thanks @klappradla Co-Authored-By: Max Mulatz --- docs/tutorial/06-allow-snake-to-eat.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/tutorial/06-allow-snake-to-eat.md b/docs/tutorial/06-allow-snake-to-eat.md index 67a451e..770f4cb 100644 --- a/docs/tutorial/06-allow-snake-to-eat.md +++ b/docs/tutorial/06-allow-snake-to-eat.md @@ -1,10 +1,10 @@ ### 6. Allow snake to eat -We have the food, but our snake can't eat it yet, and we don't want to starve our little friend. +We have the food! But our snake can't eat it yet and we don't want to starve our little friend πŸ™€ -The goal of this chapter is to allow the snake to eat the food and grow, but let's first quickly recap how it works. The snake eats the food when it overlaps it with its head, as soon the snake eats it, it will grow of one unit (a new tile will be appended to its body) and a new food pellet will be placed in the snake world. +The goal of this chapter is to allow the snake to eat the food and grow. Let's quickly recap how it works: The snake "eats" the food when it overlaps it with its head. As soon the snake has eaten, it grows by one unit (a new tile will be appended to its body) and a new food pellet will be placed somewhere in the game. -In other words, we need to check if the snake's head has the same coordinates of the pellet after every movement. The function `move_snake/1` looks the natural place where to execute this check. +To achieve this, we'll check whether the snake's head has the same coordinates as the pellet. As we need to perform this check again after every movement, the `move_snake/1` function looks like a good place this. ```elixir defp move_snake(%{snake: snake} = state) do @@ -31,10 +31,7 @@ Our new function `maybe_eat_food/2` receives: - The current state as 1st argument - The snake's head coordinates as 2nd argument -Then, in the case the snake's head overlaps the food pellet, the function will take care of: - -- Grow the snake body of one unit -- Place a new food pellet in the snake world +If the snake's head overlaps the food pellet, we grow its body by one unit and place a new pellet somewhere in the game. or otherwise, just return the current state without any changes. @@ -52,11 +49,13 @@ def maybe_eat_pellet(state = %{pellet: pellet}, snake_head) do end ``` -Growing the snake body can be tricky, let's explore all our alternatives. +Growing the snake's body can be tricky. Let's explore some possibilities: + +Prepending a new tile before the snake's head is not feasible. It could lead to unexpected outcome: the snake hits its tail and dies ☠️ -Prepending a new tile to the snake head is not a feasible solution because it will potentially lead to unexpected outcome like its dead :skull:. +The most natural approach is appending a new tile to the end of the body. But how? We can't append a tile with `Enum.concat(body, [{x, y}])`. We can't infer the coordinates (`{x, y}`) for the new tile, since we don't know the tail's direction. We only know where the head is moving. -The most natural approach is to append a new tile at the end of the snake body, but how exactly? We can not simply add a new tile at the end like: `Enum.concat(body, [{x, y}])` since we don't have any information of the tail direction but only of its head. In other words, we can't infer the coordinates (`{x, y}`) of the tile to append. The only way to safely grow our snake it's to preserve its body when the snake ate the pellet. We could set a boolean flag `has_eaten` in the state and in the next game tick, don't delete the last snake's body tile when this flag is true πŸ€“. +The only way to safely grow our snake its to preserve its body when it ate the pellet. We can set a boolean flag `has_eaten` in the state and on the next tick not delete the last body tile if is set to `true` πŸ€“. This will naturally grow our snake by one tile. Remember, we set a timer at the beginning in out `init/1` function that periodically sends a message, which is intercepted by our `handle_info/2`, which in turn calls the `move_snake/1` function. That's our game tick. @@ -108,7 +107,7 @@ end Let's take a look to these two new functions: - `grow_snake/1` simply sets the `:has_eatan` flag to true in the state -- `place_pellet/1` computes a new pair of coordinates for the food, if the new value matches any tile in the snake's body, it recursively generate a new position until it does not overlap the snake +- `place_pellet/1` computes a new pair of coordinates for the food. If the new value matches any tile in the snake's body, it recursively generate a new position until it does not overlap any more. We still need to update the `move_snake/1` function to use the `:has_eatan` flag. @@ -131,7 +130,7 @@ defp move_snake(%{snake: snake} = state) do end ``` -Like that, when the flag `:has_eatan` is true, the last tile from the snake's body is not removed anymore. This is the trick that allows us to grow the snake πŸ’ͺ +When `:has_eaten` is true, the last tile from the snake's body is **not** removed. This is the trick that allows us to grow the snake πŸ’ͺ And now let's run the game and see how if our snake grows when eating. @@ -154,7 +153,7 @@ defp move_snake(%{snake: snake} = state) do state |> put_in([:snake, :body], new_body) - |> put_in([::snake, :has_eaten], false) # Reset the `:has_eaten` flag before the next check + |> put_in([:snake, :has_eaten], false) # Reset the `:has_eaten` flag before the next check |> maybe_eat_food(new_head) end ``` From a0ba98b91cd10f4280a9d323ad8efce22849cb0d Mon Sep 17 00:00:00 2001 From: nickgnd Date: Mon, 2 Mar 2020 09:11:25 +0100 Subject: [PATCH 4/4] Use guard for `maybe_eat_pellet` and other improvements --- docs/tutorial/06-allow-snake-to-eat.md | 52 ++++++++++++-------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/docs/tutorial/06-allow-snake-to-eat.md b/docs/tutorial/06-allow-snake-to-eat.md index 770f4cb..3b6e09e 100644 --- a/docs/tutorial/06-allow-snake-to-eat.md +++ b/docs/tutorial/06-allow-snake-to-eat.md @@ -6,6 +6,8 @@ The goal of this chapter is to allow the snake to eat the food and grow. Let's q To achieve this, we'll check whether the snake's head has the same coordinates as the pellet. As we need to perform this check again after every movement, the `move_snake/1` function looks like a good place this. +πŸ‘‡Let's add it at the end of the state update pipeline, the pipe operator `|>` comes in handy here πŸ’š + ```elixir defp move_snake(%{snake: snake} = state) do %{body: body, size: size, direction: direction} = snake @@ -20,13 +22,11 @@ defp move_snake(%{snake: snake} = state) do state |> put_in([:objects, :snake, :body], new_body) - |> maybe_eat_food(new_head) + |> maybe_eat_pellet(new_head) end ``` -πŸ‘†Let's add it at the end of the state update pipeline, the pipe operator `|>` comes in handy here πŸ’š - -Our new function `maybe_eat_food/2` receives: +Our new function `maybe_eat_pellet/2` receives: - The current state as 1st argument - The snake's head coordinates as 2nd argument @@ -38,15 +38,13 @@ or otherwise, just return the current state without any changes. Let's draft out the `maybe_eat_pellet/2` function and implement it step by step. ```elixir -def maybe_eat_pellet(state = %{pellet: pellet}, snake_head) do - if (pellet == snake_head) - state - |> grow_snake() - |> place_pellet() - else - state - end +def maybe_eat_pellet(state = %{pellet: pellet}, snake_head) when pellet == snake_head do + state + |> grow_snake() + |> place_pellet() end + +def maybe_eat_pellet(state, _snake_head), do: state ``` Growing the snake's body can be tricky. Let's explore some possibilities: @@ -59,6 +57,11 @@ The only way to safely grow our snake its to preserve its body when it ate the p Remember, we set a timer at the beginning in out `init/1` function that periodically sends a message, which is intercepted by our `handle_info/2`, which in turn calls the `move_snake/1` function. That's our game tick. +Let's add these two new functions in our code: + +- `grow_snake/1` sets the `:has_eatan` flag to true in the state +- `place_pellet/1` computes a new pair of coordinates for the food. If the new value matches any tile in the snake's body, it recursively generate a new position until it does not overlap any more. + ```elixir defp move_snake(%{snake: snake} = state) do %{body: body, size: size, direction: direction} = snake @@ -73,19 +76,17 @@ defp move_snake(%{snake: snake} = state) do state |> put_in([:snake, :body], new_body) - |> maybe_eat_food(new_head) + |> maybe_eat_pellet(new_head) end -def maybe_eat_pellet(state = %{pellet: pellet}, snake_head) do - if (pellet == snake_head) - state - |> grow_snake() - |> place_pellet() - else - state - end +def maybe_eat_pellet(state = %{pellet: pellet}, snake_head) when pellet == snake_head do + state + |> grow_snake() + |> place_pellet() end +def maybe_eat_pellet(state, _snake_head), do: state + def grow_snake(state = %{%{snake: %{size: size}}) do put_in(state, [:snake, :has_eaten], true) end @@ -104,11 +105,6 @@ def place_pellet(state = %{width: width, height: height, snake: %{body: snake_bo end ``` -Let's take a look to these two new functions: - -- `grow_snake/1` simply sets the `:has_eatan` flag to true in the state -- `place_pellet/1` computes a new pair of coordinates for the food. If the new value matches any tile in the snake's body, it recursively generate a new position until it does not overlap any more. - We still need to update the `move_snake/1` function to use the `:has_eatan` flag. ```elixir @@ -126,7 +122,7 @@ defp move_snake(%{snake: snake} = state) do state |> put_in([:snake, :body], new_body) - |> maybe_eat_food(new_head) + |> maybe_eat_pellet(new_head) end ``` @@ -154,7 +150,7 @@ defp move_snake(%{snake: snake} = state) do state |> put_in([:snake, :body], new_body) |> put_in([:snake, :has_eaten], false) # Reset the `:has_eaten` flag before the next check - |> maybe_eat_food(new_head) + |> maybe_eat_pellet(new_head) end ```