Skip to content

Commit 456cd98

Browse files
committed
Wip
1 parent c71c1c7 commit 456cd98

1 file changed

Lines changed: 144 additions & 0 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
### 6. Allow snake to eat
2+
3+
We have the food, but our snake can not yet eat it, and we don't want to starve our little friend.
4+
5+
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 pallet will be placed in the snake world.
6+
7+
In other words, we need to check if the snake's head has the same coordinates of the food after every movement, and the function `move_snake/1` looks the natural place where to execute this check.
8+
9+
```elixir
10+
defp move_snake(%{snake: snake} = state) do
11+
%{body: body, size: size, direction: direction} = snake
12+
13+
# New head's position
14+
[head | _] = body
15+
new_head = move(state, head, direction)
16+
17+
# Place a new head on the tile that we want to move to
18+
# and remove the last tile from the snake tail
19+
new_body = List.delete_at([new_head | body], -1)
20+
21+
state
22+
|> put_in([:objects, :snake, :body], new_body)
23+
|> maybe_eat_food(new_head) # <-- let's check if the snake has eaten the food as last step of the state update (the pipe operator `|>` is really handy in this case)
24+
end
25+
```
26+
27+
So we have this new function `maybe_eat_food/2` which receives:
28+
29+
- The current state as 1st argument
30+
- The snake's head coordinates as 2nd argument
31+
32+
Then, in the case the snake's head overlaps the food pellet, the function will take care of:
33+
34+
- Grow the snake body of one unit
35+
- Place a new food pellet in the snake world
36+
37+
or otherwise, just return the current state without any changes.
38+
39+
Let's draft out the function and implement it step by step.
40+
41+
```elixir
42+
def maybe_eat_pellet(state = %{pellet: pellet}, snake_head) do
43+
if (pellet == snake_head)
44+
state
45+
|> grow_snake()
46+
|> place_pellet()
47+
else
48+
state
49+
end
50+
end
51+
```
52+
53+
Growing the snake body can be tricky, let's explore all our alternatives.
54+
55+
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:.
56+
57+
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 it ate the food. We could set a boolean flag `has_eaten` in the state and in the next game tick, if this flag is true, the last tile of the body will be preserved and not deleted as usual.
58+
59+
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.
60+
61+
```elixir
62+
defp move_snake(%{snake: snake} = state) do
63+
%{body: body, size: size, direction: direction} = snake
64+
65+
# New head's position
66+
[head | _] = body
67+
new_head = move(state, head, direction)
68+
69+
# Place a new head on the tile that we want to move to
70+
# and remove the last tile from the snake tail
71+
new_body = List.delete_at([new_head | body], -1)
72+
73+
state
74+
|> put_in([:snake, :body], new_body)
75+
|> maybe_eat_food(new_head)
76+
end
77+
78+
def maybe_eat_pellet(state = %{pellet: pellet}, snake_head) do
79+
if (pellet == snake_head)
80+
state
81+
|> grow_snake()
82+
|> place_pellet()
83+
else
84+
state
85+
end
86+
end
87+
88+
def grow_snake(state = %{%{snake: %{size: size}}) do
89+
put_in(state, [:snake, :has_eaten], true)
90+
end
91+
92+
def place_pellet(state = %{width: width, height: height, snake: %{body: snake_body}}) do
93+
pellet_coords = {
94+
Enum.random(0..(width - 1)),
95+
Enum.random(0..(height - 1))
96+
}
97+
98+
if pellet_coords in snake_body do
99+
place_pellet(state)
100+
else
101+
put_in(state, [:objects, :pellet], pellet_coords)
102+
end
103+
end
104+
```
105+
106+
Let's take a look to these two new functions:
107+
108+
- `grow_snake/1` simply sets the `:has_eatan` flag to true in the state
109+
- `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
110+
111+
We still need to update the `move_snake/1` function to use the `:has_eatan` flag.
112+
113+
```
114+
defp move_snake(%{snake: snake} = state) do
115+
%{body: body, direction: direction} = snake
116+
117+
# New head's position
118+
[head | _] = body
119+
new_head = move(state, head, direction)
120+
121+
# Place a new head on the tile that we want to move to
122+
# and remove the last tile from the snake tail if it has not eaten any pellet
123+
new_body = [new_head | body]
124+
new_body = if snake.has_eaten, do: new_body, else: List.delete_at(new_body, -1)
125+
126+
state
127+
|> put_in([:snake, :body], new_body)
128+
|> maybe_eat_food(new_head)
129+
end
130+
```
131+
132+
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 💪
133+
134+
And now let's run the game and see how if our snake grows when eating.
135+
136+
$ mix scenic.run
137+
138+
Oh snapp! Our snake is growing infinitely!! We forgot to reset the `:has_eaten` flag 🙈
139+
140+
TODO:
141+
142+
- reset flag
143+
- quick recap
144+
- add gif

0 commit comments

Comments
 (0)