Skip to content

Step 3

Drawing the grid, the fruit, and the snake

We will assign some values to define what entity each cell of our grid can represent. For now, we will map these numeric values to draw a unique single character each as the visual representation.

value entity character
0 empty "."
1 fruit "F"
2 head "H"
3 body "B"
4 tail "T"

A snake has a head and a tail, and a body. We can represent these entities as a sequence of coordinates. The first element is the tail, the last one is the head, and everything in between is the body.

1
[(int2 1 2) (int2 2 2) (int2 3 2) (int2 3 3) (int2 4 3)] >= .snake

The fruit, however, occupies a single cell - so we just need one set of coordinates.

Every other cell is empty (or unoccupied) and is represented with a ‘.’.

1
(int2 4 4) >= .fruit

To position the cells, we will use a (UI.Area). By default, it is anchored at the top left corner, which means the position at that corner is (float2 0 0).

To calculate the position of our cell, we take into account the number of columns, the size in pixels we want our cell to have (cell-size) and additional x-offset and y-offset so that the overall grid is not stuck at the top left corner but slightly moved right and down.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
(def cell-size 18)
(def x-offset 48)
(def y-offset 36)
(defn render-cell [n] ;; (1)
  (| (int2 ;; (2)
      (% n grid-cols) ;; (3)
      (/ n grid-cols)) ;; (4)
    (ToFloat2) ;; (5)
    (Math.Multiply (float2 cell-size)) ;; (6)
    (Math.Add (float2 x-offset y-offset)) > .position) ;; (7)
  (| (Take n) ;; (8)
    (UI.Area
      :Position .position
      :Contents
      (-> (Match ;; (9)
          [0 (-> ".") ; empty
            1 (-> "F") ; fruit
            2 (-> "H") ; head
            3 (-> "B") ; body
            4 (-> "T") ; tail
            ]false)
          (UI.Label))))) ;; (10)
  1. The input of this function will be our grid. The parameter n is the cell index we want to render.
  2. We want to apply mathematical functions to integral numbers (so-called integers) so we explicitly use the (int2)type.
  3. % is the modulo function. We use it to get the x coordinate, i.e. the index of the column in our grid.
  4. / is the division function. We use it to get the y coordinate, i.e. the index of the row in our grid.
  5. (UI.Area) requires a (float2) type for its :Position parameter, so we do a conversion using (ToFloat2)
  6. We defined the size for a single cell to be cell-size. Without it, it would have been a 1x1 pixel which we could hardly see.
  7. Finally, we add the offsets so that our grid is more centered. The final value is saved into the .position variable.
  8. Now, we get the value of n-indexed cell from the .grid variable that was given as input to the render-area function.
  9. In that action, we (Match) the value of the cell to the corresponding character we have chosen.
  10. Then the matched character is displayed in place of that grid element using (UI.Label).

The above function only deals with a single cell. We want to render all cells. To do so we will slightly modify it to recursively apply to each cell index (from 0 to (- (* grid-cols grid-rows) 1), i.e. the number of columns times the number of rows, minus 1 because we start our index at 0).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(def cell-size 18)
(def x-offset 48)
(def y-offset 36)
(defn render-cells [n] ;; (1)
  (if
  (= n -1) ;; (2)
    nil  ;; (3)
    (-> ;; (4)
    (| (int2
        (% n grid-cols)
        (/ n grid-cols))
        (ToFloat2)
        (Math.Multiply (float2 cell-size))
        (Math.Add (float2 x-offset y-offset)) > .position)
    (| (Take n)
        (UI.Area
        :Position .position
        :Contents
        (-> (Match
              [0 (-> ".") ; empty
              1 (-> "F") ; fruit
              2 (-> "H") ; head
              3 (-> "B") ; body
              4 (-> "T") ; tail
              ]false)
            (UI.Label))))
    (render-cells (- n 1))))) ;; (5)
  1. We pluralized the function since it now renders several cells.
  2. We test whether n equals -1. Since we want to render cells from 0 to (- (* grid-cols grid-rows) 1), this is our stopping condition.
  3. If we are in that case, we do nothing (nil).
  4. Otherwise, we perform the same code as we did above.
  5. Finally we do the recursion, which is just calling the same render-cells function but with a decremented value for n.

Populating the grid

Before we can draw anything we need to update the grid with the fruit and the snake. To update a sequence at a given index, we can use the (Assoc) shard. And since the snake is saved as a sequence itself, we need to iterate through all its elements. However, the head, tail, and body are represented by different values, so we will handle them separately.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
(defshards populate-grid [fruit snake]
  ; saves the input into a variable
  >= .tmp-grid

  ; first the snake tail and body
  snake (Take 0) (get-index) >= .tail-index ;; (1)
  [.tail-index 4] (Assoc .tmp-grid) ;; (2)
  snake (Slice 1 -1) ;; (3)
  (ForEach
   (-> (get-index) >= .limb-index
       [.limb-index 3] (Assoc .tmp-grid)))

  ; then the fruit
  fruit (get-index) >= .fruit-index
  [.fruit-index 1] (Assoc .tmp-grid)

  ; finally the snake head
  snake (RTake 0) (get-index) >= .head-index ;; (4)
  [.head-index 2] (Assoc .tmp-grid)

  ; return the populated grid
  .tmp-grid)
  1. We have already seen (Take) and get-index in step 2.
  2. Assoc lets us update the sequence.
  3. (Slice) gives a part of a sequence in a range. 1 means we start at the second element of the sequence (in other words, we skip 1 element), and -1 means we stop at one element before the last (in other words, we skip 1 element from the end).
  4. (RTake) is similar to (Take), except it starts from the end of the sequence instead of the beginning (i.e. "reverse take").

This new function populate-grid will take our empty grid as input and return a populated grid. That is why we need a temporary variable inside the function (.tmp-grid).

Note

Another alternative would have been, to have a single grid and erase the previous positions of the fruit and the snake before updating to their new positions. We find it easier to just update the whole grid at once for this tutorial.

Let's try it out!

Let's put into practice all that we have seen so far.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
(def grid-cols 12)
(def grid-rows 10)
(def empty-grid
  [0 0 0 0 0 0 0 0 0 0 0 0
   0 0 0 0 0 0 0 0 0 0 0 0
   0 0 0 0 0 0 0 0 0 0 0 0
   0 0 0 0 0 0 0 0 0 0 0 0
   0 0 0 0 0 0 0 0 0 0 0 0
   0 0 0 0 0 0 0 0 0 0 0 0
   0 0 0 0 0 0 0 0 0 0 0 0
   0 0 0 0 0 0 0 0 0 0 0 0
   0 0 0 0 0 0 0 0 0 0 0 0
   0 0 0 0 0 0 0 0 0 0 0 0])

(defshards get-index []
  (| (Take 0) >= .x)
  (| (Take 1) >= .y)
  .y (Math.Multiply grid-cols) (Math.Add .x))

(defshards populate-grid [fruit snake]
  ; saves the input into a variable
  >= .tmp-grid

  ; first the snake tail and body
  snake (Take 0) (get-index) >= .tail-index
  [.tail-index 4] (Assoc .tmp-grid)
  snake (Slice 1 -1)
  (ForEach
   (-> (get-index) >= .limb-index
       [.limb-index 3] (Assoc .tmp-grid)))

  ; then the fruit
  fruit (get-index) >= .fruit-index
  [.fruit-index 1] (Assoc .tmp-grid)

  ; finally the snake head
  snake (RTake 0) (get-index) >= .head-index
  [.head-index 2] (Assoc .tmp-grid)

  ; return the populated grid
  .tmp-grid)

(def cell-size 18)
(def x-offset 48)
(def y-offset 36)
(defn render-cells [n]
  (if
   (= n -1)
    nil
    (->
     (| (int2
         (% n grid-cols)
         (/ n grid-cols))
        (ToFloat2)
        (Math.Multiply (float2 cell-size))
        (Math.Add (float2 x-offset y-offset)) > .position)
     (| (Take n)
        (UI.Area
         :Position .position
         :Contents
         (-> (Match
              [0 (-> ".") ; empty
               1 (-> "F") ; fruit
               2 (-> "H") ; head
               3 (-> "B") ; body
               4 (-> "T") ; tail
               ]false)
             (UI.Label))))
     (render-cells (- n 1)))))

(defloop main-wire
  ; logic
  [(int2 1 2) (int2 2 2) (int2 3 2) (int2 3 3) (int2 4 3)] >= .snake
  (int2 6 7) >= .fruit
  empty-grid (populate-grid .fruit .snake) >= .grid
  ; window
  (GFX.MainWindow
   :Title "Snake game" :Width 480 :Height 360
   :Contents
   (->
    (Setup
     (GFX.DrawQueue) >= .ui-draw-queue
     (GFX.UIPass .ui-draw-queue) >> .render-steps)
    .ui-draw-queue (GFX.ClearQueue)

    (UI
     .ui-draw-queue
     (->
      .grid
      (Setup (float2 0) >= .position)
      (render-cells (- (* grid-cols grid-rows) 1))))

    (GFX.Render :Steps .render-steps))))

(defmesh root)
(schedule root main-wire)
(run root (/ 1.0 60))