<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Matias Finochio]]></title><description><![CDATA[Creator of S2D. Low Level programming & Game Development enjoyer]]></description><link>https://matiasfinochio.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!LGS4!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fmatiasfinochio.substack.com%2Fimg%2Fsubstack.png</url><title>Matias Finochio</title><link>https://matiasfinochio.substack.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 27 Aug 2026 03:04:35 GMT</lastBuildDate><atom:link href="https://matiasfinochio.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Matias Finochio]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[matiasfinochio@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[matiasfinochio@substack.com]]></itunes:email><itunes:name><![CDATA[Matias Finochio]]></itunes:name></itunes:owner><itunes:author><![CDATA[Matias Finochio]]></itunes:author><googleplay:owner><![CDATA[matiasfinochio@substack.com]]></googleplay:owner><googleplay:email><![CDATA[matiasfinochio@substack.com]]></googleplay:email><googleplay:author><![CDATA[Matias Finochio]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[S2D Dev Series — Snake, Day 7: Score, Game Over and Restart]]></title><description><![CDATA[Welcome to Day 7 - the final day of the series! Today we do the finishing touches to the game. We will add a font, display the score, game over screen and also let the player restart.]]></description><link>https://matiasfinochio.substack.com/p/s2d-dev-series-snake-day-7-score</link><guid isPermaLink="false">https://matiasfinochio.substack.com/p/s2d-dev-series-snake-day-7-score</guid><dc:creator><![CDATA[Matias Finochio]]></dc:creator><pubDate>Wed, 15 Jul 2026 18:47:09 GMT</pubDate><content:encoded><![CDATA[<h4>Current state</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*

val cellSize     = 20
val columns      = 20
val rows         = 20
val gameW        = columns * cellSize
val gameH        = rows    * cellSize
val scoreBarH    = 60
val moveInterval = 0.15

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

enum Direction:
  case Up, Down, Left, Right

enum GameState:
  case Playing, GameOver

case class Cell(col: Int, row: Int)

def cellToPixel(cell: Cell): (Float, Float) =
  (offsetX.toFloat + cell.col * cellSize, offsetY.toFloat + cell.row * cellSize)

def randomFood(snake: List[Cell]): Cell =
  val free = for
    c &lt;- 0 until columns
    r &lt;- 0 until rows
    cell = Cell(c, r)
    if !snake.contains(cell)
  yield cell
  free(Random.int(0, free.length - 1))

def initialSnake: List[Cell] =
  List(Cell(10, 10), Cell(9, 10), Cell(8, 10))

@main
def main(): Unit =
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  var snake         = initialSnake
  var direction     = Direction.Right
  var nextDirection = Direction.Right
  var moveTimer     = 0.0
  var food          = randomFood(snake)
  var state         = GameState.Playing

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    state match
      case GameState.Playing =&gt;
        if      Input.isKeyPressed(Key.Up)    &amp;&amp; direction != Direction.Down  then nextDirection = Direction.Up
        else if Input.isKeyPressed(Key.Down)  &amp;&amp; direction != Direction.Up    then nextDirection = Direction.Down
        else if Input.isKeyPressed(Key.Left)  &amp;&amp; direction != Direction.Right then nextDirection = Direction.Left
        else if Input.isKeyPressed(Key.Right) &amp;&amp; direction != Direction.Left  then nextDirection = Direction.Right

        moveTimer += Timing.delta

        if moveTimer &gt;= moveInterval then
          moveTimer = 0.0
          direction = nextDirection

          val head = snake.head
          val next = direction match
            case Direction.Up    =&gt; Cell(head.col,     head.row - 1)
            case Direction.Down  =&gt; Cell(head.col,     head.row + 1)
            case Direction.Left  =&gt; Cell(head.col - 1, head.row    )
            case Direction.Right =&gt; Cell(head.col + 1, head.row    )

          val hitWall    = next.col &lt; 0 || next.col &gt;= columns || next.row &lt; 0 || next.row &gt;= rows
          val ate        = next == food
          val bodyToCheck = if ate then snake else snake.init
          val hitSelf    = bodyToCheck.contains(next)

          if hitWall || hitSelf then
            state = GameState.GameOver
          else
            snake = if ate then next :: snake else next :: snake.init
            if ate then food = randomFood(snake)

        drawArena()
        drawFood(food)
        drawSnake(snake)

      case GameState.GameOver =&gt;
        drawArena()
        drawFood(food)
        drawSnake(snake)

    Drawing.endFrame()

  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)

def drawSnake(snake: List[Cell]): Unit =
  snake.zipWithIndex.foreach: (cell, index) =&gt;
    val (x, y) = cellToPixel(cell)
    val color  = if index == 0 then Color.Lime else Color.Green
    Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)

def drawFood(food: Cell): Unit =
  val (x, y) = cellToPixel(food)
  Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, Color.Red)</code></pre></div><p><em><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">If you haven&#8217;t read Day 6, I&#8217;d recommend doing that first.</mark></em></p><h4>Summary for today</h4><ul><li><p>A new import: <strong>s2d.font</strong></p></li><li><p>New class: <strong>SnakeGame</strong></p></li><li><p>Font handling (loading/unloading)</p></li><li><p>New functions: <strong>drawScore </strong>&amp; <strong>drawGameOver</strong></p></li><li><p>Restart logic</p></li></ul><h4>The font</h4><p>Before writing any code, we need a font file. S2D uses &#8220;stb_truetype&#8221; in the backend to handle fonts, so we need to download a &#8220;.ttf&#8221; file.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://matiasfinochio.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Go to <a href="https://fonts.google.com/">Google Fonts</a> and download <strong>Roboto.</strong> <br>From the .zip file, take the file &#8220;Robot-Regular.ttf&#8221; and place it inside the &#8220;assets/&#8221; folder of your project.</p><p>The project structure should look something like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">snake-game/
  assets/
    font.ttf
  project.scala
  main.scala</code></pre></div><p><em><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">I&#8217;d recommend renaming the file to something like &#8220;font.ttf&#8221; to keep it simple.</mark></em></p><h4>Final code for today</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*
import s2d.font.*
import scala.util.boundary, boundary.break

val cellSize     = 20
val columns      = 20
val rows         = 20
val gameW        = columns * cellSize
val gameH        = rows    * cellSize
val scoreBarH    = 60
val moveInterval = 0.15

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

enum Direction:
  case Up, Down, Left, Right

enum GameState:
  case Playing, GameOver

case class Cell(col: Int, row: Int)

case class SnakeGame(
  snake:         List[Cell],
  food:          Cell,
  direction:     Direction,
  nextDirection: Direction,
  state:         GameState,
  score:         Int,
  moveTimer:     Double
)

def cellToPixel(cell: Cell): (Float, Float) =
  (offsetX.toFloat + cell.col * cellSize, offsetY.toFloat + cell.row * cellSize)

def randomFood(snake: List[Cell]): Cell =
  val free = for
    c &lt;- 0 until columns
    r &lt;- 0 until rows
    cell = Cell(c, r)
    if !snake.contains(cell)
  yield cell
  free(Random.int(0, free.length - 1))

def initialSnake: List[Cell] =
  List(Cell(10, 10), Cell(9, 10), Cell(8, 10))

def initialState: SnakeGame =
  val snake = initialSnake
  SnakeGame(
    snake         = snake,
    food          = randomFood(snake),
    direction     = Direction.Right,
    nextDirection = Direction.Right,
    state         = GameState.Playing,
    score         = 0,
    moveTimer     = 0.0
  )

@main
def main(): Unit = boundary:
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  val font = Font.load("assets/font.ttf", 24.0f).getOrElse:
    println("Could not load font. Make sure assets/font.ttf exists.")
    Window.close()
    break(())

  var game = initialState

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    game.state match
      case GameState.Playing =&gt;
        if      Input.isKeyPressed(Key.Up)    &amp;&amp; game.direction != Direction.Down  then game = game.copy(nextDirection = Direction.Up)
        else if Input.isKeyPressed(Key.Down)  &amp;&amp; game.direction != Direction.Up    then game = game.copy(nextDirection = Direction.Down)
        else if Input.isKeyPressed(Key.Left)  &amp;&amp; game.direction != Direction.Right then game = game.copy(nextDirection = Direction.Left)
        else if Input.isKeyPressed(Key.Right) &amp;&amp; game.direction != Direction.Left  then game = game.copy(nextDirection = Direction.Right)

        game = game.copy(moveTimer = game.moveTimer + Timing.delta)

        if game.moveTimer &gt;= moveInterval then
          val direction = game.nextDirection
          val head      = game.snake.head
          val next      = direction match
            case Direction.Up    =&gt; Cell(head.col,     head.row - 1)
            case Direction.Down  =&gt; Cell(head.col,     head.row + 1)
            case Direction.Left  =&gt; Cell(head.col - 1, head.row    )
            case Direction.Right =&gt; Cell(head.col + 1, head.row    )

          val hitWall     = next.col &lt; 0 || next.col &gt;= columns || next.row &lt; 0 || next.row &gt;= rows
          val ate         = next == game.food
          val bodyToCheck = if ate then game.snake else game.snake.init
          val hitSelf     = bodyToCheck.contains(next)

          if hitWall || hitSelf then
            game = game.copy(state = GameState.GameOver, moveTimer = 0.0)
          else
            val newSnake = if ate then next :: game.snake else next :: game.snake.init
            val newFood  = if ate then randomFood(newSnake) else game.food
            val newScore = if ate then game.score + 1 else game.score
            game = game.copy(
              snake     = newSnake,
              food      = newFood,
              direction = direction,
              score     = newScore,
              moveTimer = 0.0
            )

        drawArena()
        drawFood(game.food)
        drawSnake(game.snake)
        drawScore(game.score, font)

      case GameState.GameOver =&gt;
        if Input.isKeyPressed(Key.Enter) then
          game = initialState

        drawArena()
        drawFood(game.food)
        drawSnake(game.snake)
        drawGameOver(game.score, font)

    Drawing.endFrame()

  Font.unload(font)
  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)

def drawSnake(snake: List[Cell]): Unit =
  snake.zipWithIndex.foreach: (cell, index) =&gt;
    val (x, y) = cellToPixel(cell)
    val color  = if index == 0 then Color.Lime else Color.Green
    Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)

def drawFood(food: Cell): Unit =
  val (x, y) = cellToPixel(food)
  Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, Color.Red)

def drawScore(score: Int, font: FontType): Unit =
  val text = s"Score: $score"
  val size = Text.measure(text, font)
  val x    = (Window.width - size.x) / 2.0f
  val y    = (offsetY - size.y) / 2.0f
  Text.draw(text, x, y, font, Color.White)

def drawGameOver(score: Int, font: FontType): Unit =
  Basics.rectangle(offsetX, offsetY, gameW, gameH, Color(0, 0, 0, 50))

  val gameOverText = "Game Over"
  val scoreText    = s"Score: $score"
  val restartText  = "Press Enter to restart"

  val goSize = Text.measure(gameOverText, font)
  val scSize = Text.measure(scoreText, font)
  val reSize = Text.measure(restartText, font)

  val centerX = offsetX + gameW / 2.0f
  val centerY = offsetY + gameH / 2.0f

  Text.draw(gameOverText, centerX - goSize.x / 2, centerY - goSize.y - 16, font, Color.Red)
  Text.draw(scoreText,    centerX - scSize.x / 2, centerY - scSize.y / 2,  font, Color.White)
  Text.draw(restartText,  centerX - reSize.x / 2, centerY + reSize.y + 8,  font, Color.LightGray)</code></pre></div><div><hr></div><h4>The new imports</h4><p>At the top of the file we added these lines:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.font.*
import scala.util.boundary, boundary.break</code></pre></div><p><strong>s2d.font</strong> gives us access to S2D&#8217;s <strong>Font, Text, </strong>and <strong>FontType. </strong>We are going to use all three today.</p><p><strong>scala.util&#8230; </strong>is part of the Scala standard library. We will go over what these mean in a bit.</p><h4>SnakeGame - handling the game state correctly</h4><p>Over the past few days, we&#8217;ve been adding new <strong>var</strong> fields to <strong>main (</strong>snake, direction, nextDirection, moveTimer, food, state) and now we are adding a new one: <strong>score</strong>.</p><p>That is a lot of separate variables, all of which describe different parts of the game state.</p><p>Imagine that we need to restart the game. We would need to reset all of these variables individually. This can get messy and annoying fast.</p><p>The solution for this is to group these values into a single <strong>case class</strong> like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">case class SnakeGame(
  snake:         List[Cell],
  food:          Cell,
  direction:     Direction,
  nextDirection: Direction,
  state:         GameState,
  score:         Int,
  moveTimer:     Double
)</code></pre></div><p><strong>SnakeGame</strong> holds everything that can change during a game session.<br>Instead of having lots of different vars, we have one.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">var game = initialState</code></pre></div><p>And resetting the state would only be one line:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">game = initialState</code></pre></div><p><strong>Case classes</strong> in Scala also give us the &#8220;.copy&#8221; method. This creates a new <strong>instance</strong> of the class with only the fields you specify changed, while everything else stays the same.</p><p>This is really useful throughout the game loop:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">game = game.copy(nextDirection = Direction.Up)</code></pre></div><p>&#8220;Give me a new game state identical to the current one, but with <strong>nextDirection</strong> set to <strong>Up.</strong>&#8221;</p><h4>initialState</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">def initialState: SnakeGame =
  val snake = initialSnake
  SnakeGame(
    snake         = snake,
    food          = randomFood(snake),
    direction     = Direction.Right,
    nextDirection = Direction.Right,
    state         = GameState.Playing,
    score         = 0,
    moveTimer     = 0.0
  )</code></pre></div><p><strong>initialSnake</strong> is a function that creates a new <strong>SnakeGame</strong> with all its values set to their starting point. <br>This is useful for two reasons:</p><ul><li><p>When the game starts, <strong>initialState</strong> defines the values the game will use, so if we want to change the starting direction for example, we would simply change the value from &#8220;Right&#8221; to let&#8217;s say &#8220;Left&#8221;, and the snake would start moving to the left.</p></li><li><p>Every time we want to restart the game we can simply overwrite the current game state with <strong>initialState</strong> (like we saw above)</p></li></ul><h4>Loading the font: boundary and break</h4><p>Take a look at these lines:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">@main
def main(): Unit = boundary:
  // ...
  val font = Font.load("assets/font.ttf", 24.0f).getOrElse:
    println("Could not load font. Make sure assets/font.ttf exists.")
    Window.close()
    break(())</code></pre></div><p><strong>Font.load</strong> returns an <strong>Option[FontType]</strong>. Don&#8217;t worry, options are not as bad as they sound.</p><p>An <strong>Option</strong> in Scala represents a value that might or might not be there. This gives us two possible cases:</p><ul><li><p><strong>Some(font)</strong>: The actual value we want to use.</p></li><li><p><strong>None:</strong> No value at all.</p></li></ul><p>This is really useful in the current context because we want to load a <strong>Font</strong> but we don&#8217;t want the game to crash if it can&#8217;t load the file. We want to be able to decide what happens in both cases.</p><p>We could use a <strong>match</strong> expression here with <strong>Some(T)</strong> and <strong>None</strong> like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">Font.load("assets/font.ttf", 24.0f) match
  case Some(font) =&gt;
    // use font
  case None =&gt;
    // handle missing font</code></pre></div><p>But we will use a more convenient method. The function <strong>getOrElse()</strong>.</p><p>If the <strong>Option</strong> contains a Font (meaning it loaded correctly), <strong>getOrElse</strong> gives us that Font like this: <strong>Some(font) =&gt; font.</strong></p><p>If the <strong>Option</strong> is <strong>None </strong>(meaning it couldn&#8217;t load), the <strong>else</strong> block gets executed.</p><p><strong>None =&gt; getOrElse else block</strong></p><p>If you look at the code, inside the <strong>else</strong> block we call <strong>break(()).</strong> What is this?</p><p>You could in theory write something like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val font = Font.load("assets/font.ttf", 24.0f).getOrElse: 
  println("Could not load font. Make sure assets/font.ttf exists.") 
  Window.close() 
  return</code></pre></div><p>However, in Scala, returning from inside this nested block would be a <strong>non-local return</strong>. Modern Scala doesn&#8217;t let us do this, instead we have to use <strong>boundary </strong>and <strong>break.</strong></p><p>First we wrap the body of <strong>main</strong> in a <strong>boundary</strong>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">@main
def main(): Unit = boundary:</code></pre></div><p>This tells Scala that the main function is a boundary that we can exit from.</p><p>Then, when the font file cannot be loaded, we call:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">break(())</code></pre></div><p>Notice the double parentheses. Why is that?</p><p>Well, the <strong>main</strong> function is of type <strong>Unit</strong>. To represent that in Scala we use and empty set of parentheses like this &#8220;()&#8221;. (Unit represent no meaningful result)</p><p>So <strong>break(())</strong> tells Scala to stop the execution of that boundary and return a <strong>Unit </strong>value.</p><p>Since the <strong>boundary</strong> contains the body of <strong>main</strong>, the entire game stops executing.</p><h4>Score</h4><p>Score lives inside <strong>SnakeGame</strong> as <strong>score: Int</strong>. It starts at 0.<br>It increases by one each time the snake eats:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val newScore = if ate then game.score + 1 else game.score
game = game.copy(score = newScore, ...)</code></pre></div><p>Notice how we use the &#8220;.copy()&#8221; method that we described above to update the current state&#8217;s score.</p><h4>drawScore</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">def drawScore(score: Int, font: FontType): Unit =
  val text = s"Score: $score"
  val size = Text.measure(text, font)
  val x    = (Window.width - size.x) / 2.0f
  val y    = (offsetY - size.y) / 2.0f
  Text.draw(text, x, y, font, Color.White)</code></pre></div><p>Just like we did for other elements in our game, we have a separate function to draw the score.</p><p>The function takes two parameters as input, an Int for the score number and a font.</p><p>Inside the function we first build the actual text that we want to render on the screen. <br>We want a small text that says &#8220;Score: &#8220; plus the actual score number next to it.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val text = s"Score: $score"</code></pre></div><p>The &#8220;s&#8221; prefix lets us embed values directly inside a string with &#8220;$&#8221;. This is better than doing string concatenation because it looks cleaner and reads better too.</p><p>Now, we also need to know how big the text is to know where to place it on the screen.<br>For this we can use S2D&#8217;s <strong>Text.measure</strong>. <br>It returns a <strong>Vector2</strong> containing the width and height of the text.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val size = Text.measure(text, font)</code></pre></div><p>Now we calculate the x and y positions of the screen where we want to render text:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">  val x    = (Window.width - size.x) / 2.0f
  val y    = (offsetY - size.y) / 2.0f</code></pre></div><p>And then we call the <strong>Text.draw</strong> function, which takes care of rendering the text on the screen at the given position, with the given font and color.</p><h4>drawGameOver</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">def drawGameOver(score: Int, font: FontType): Unit =
  Basics.rectangle(offsetX, offsetY, gameW, gameH, Color(0, 0, 0, 50))

  val gameOverText = "Game Over"
  val scoreText    = s"Score: $score"
  val restartText  = "Press Enter to restart"

  val goSize = Text.measure(gameOverText, font)
  val scSize = Text.measure(scoreText, font)
  val reSize = Text.measure(restartText, font)

  val centerX = offsetX + gameW / 2.0f
  val centerY = offsetY + gameH / 2.0f

  Text.draw(gameOverText, centerX - goSize.x / 2, centerY - goSize.y - 16, font, Color.Red)
  Text.draw(scoreText,    centerX - scSize.x / 2, centerY - scSize.y / 2,  font, Color.White)
  Text.draw(restartText,  centerX - reSize.x / 2, centerY + reSize.y + 8,  font, Color.LightGray)</code></pre></div><p>Similar to the <strong>drawScore</strong> function, this takes two values as input.<br>The game over screen will show the final score the player had when they lost and some extra text.</p><p>First we render a <strong>rectangle</strong> with a small transparency. This would be like a panel where the game over screen elements would sit. (it is purely aesthetic).</p><p>Then we create three values for the three texts we will show on the game over screen.<br>The &#8220;Game Over&#8221; text, &#8220;Score&#8221; with the final score and the &#8220;Action&#8221; the player has to perform to replay the game.</p><p>Then we calculate the size of each text and save them in three separate values.</p><p>Now we want to know where the center of the screen is so we can position the three texts there. We save those values in <strong>centerX </strong>and <strong>centerY</strong>.</p><p>And finally we draw the three texts, using a small offset on the Y axis to position one below the other.</p><p>We will call this <strong>drawGameOver</strong> function from the &#8220;GameOver&#8221; branch of the loop.<br>We call it <strong>after</strong> <strong>drawArena, drawFood </strong>and <strong>drawSnake</strong> so it renders on top of everything else.</p><h4>Restarting the game</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">case GameState.GameOver =&gt;
  if Input.isKeyPressed(Key.Enter) then
    game = initialState</code></pre></div><p>When the game ends we want the player to be able to restart and play again.<br>For this we check if they player presses the <strong>Enter</strong> key and set the game state to the <strong>initialState</strong> value that we already discussed.</p><p>As you can see, having all of the values from the GameState in one single case class lets us restart the game really easily.</p><h4>Cleaning up</h4><p>When the game loop ends, we need to clean up the font that we uploaded before closing the window.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">Font.unload(font)
Window.close()</code></pre></div><p>For this we use the <strong>Font.unload</strong> function. This frees the OpenGL texture that was created when the font was loaded.</p><p>It is really important that you always unload your assets before closing the window.</p><div><hr></div><h4>Running the code</h4><p>Go ahead and run the code like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">scala-cli run .</code></pre></div><p>You should now see the &#8220;Score&#8221; text plus the number increasing every time you eat food.<br>You should also see the game over screen when you lose and the restart logic should be working too.</p><h4>Final thoughts</h4><p>Over these 7 days we&#8217;ve built a complete game from scratch using S2D. Here is a quick look at what we covered:</p><ul><li><p>Day 1 - Project setup, opening a window.</p></li><li><p>Day 2 - The <strong>shapes</strong> package, drawing the arena.</p></li><li><p>Day 3 - <strong>Cell, cellToPixel</strong>, drawing the snake.</p></li><li><p>Day 4 - <strong>Direction</strong>, movement, <strong>Timing.delta</strong>, input.</p></li><li><p>Day 5 - Food, for-comprehensions, <strong>Random.</strong></p></li><li><p>Day 6 - <strong>GameState, </strong>collision detection, direction buffering.</p></li><li><p>Day 7 - <strong>SnakeGame</strong>, font loading, score, game over, restart.</p></li></ul><p>Every concept we used had a reason and thought process behind. We chose to use <strong>case class</strong> instead of separate values, <strong>enum</strong> for the GameState instead of <strong>booleans</strong>.<br><strong>val</strong> vs <strong>var</strong> for immutability.<br><strong>def </strong>vs <strong>val</strong> for live recalculation.<br><strong>match</strong> for branching on types.<br><strong>Option</strong> for values that might not exist.</p><p>We not only talked about S2D and how to use the library, we also went over the what decisions to make when using Scala as a language. We went over some concepts that we used on the game and why chose them over other possibilities.</p><div><hr></div><p>Now it is time to think about the next game we will build for the next series.</p><p>Thank you for following along and I hope you enjoyed the series!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://matiasfinochio.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[S2D Dev Series — Snake, Day 6: Collision and Game State]]></title><description><![CDATA[Welcome to Day 6 of the Snake tutorial series! After a well-deserved vacation break, we are back at it. Today we will work on Collisions and we will discuss something called "Game State".]]></description><link>https://matiasfinochio.substack.com/p/s2d-dev-series-snake-day-6-collision</link><guid isPermaLink="false">https://matiasfinochio.substack.com/p/s2d-dev-series-snake-day-6-collision</guid><dc:creator><![CDATA[Matias Finochio]]></dc:creator><pubDate>Wed, 08 Jul 2026 23:25:24 GMT</pubDate><content:encoded><![CDATA[<h4>Current state</h4><p>The code you should have at the moment should look like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*

val cellSize     = 20
val columns      = 20
val rows         = 20
val gameW        = columns * cellSize
val gameH        = rows    * cellSize
val scoreBarH    = 60
val moveInterval = 0.15

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

enum Direction:
  case Up, Down, Left, Right

case class Cell(col: Int, row: Int)

def cellToPixel(cell: Cell): (Float, Float) =
  (offsetX.toFloat + cell.col * cellSize, offsetY.toFloat + cell.row * cellSize)

def randomFood(snake: List[Cell]): Cell =
  val free = for
    c &lt;- 0 until columns
    r &lt;- 0 until rows
    cell = Cell(c, r)
    if !snake.contains(cell)
  yield cell
  free(Random.int(0, free.length - 1))

def initialSnake: List[Cell] =
  List(Cell(10, 10), Cell(9, 10), Cell(8, 10))

@main
def main(): Unit =
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  var snake     = initialSnake
  var direction = Direction.Right
  var moveTimer = 0.0
  var food      = randomFood(snake)

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    if      Input.isKeyPressed(Key.Up)    then direction = Direction.Up
    else if Input.isKeyPressed(Key.Down)  then direction = Direction.Down
    else if Input.isKeyPressed(Key.Left)  then direction = Direction.Left
    else if Input.isKeyPressed(Key.Right) then direction = Direction.Right

    moveTimer += Timing.delta

    if moveTimer &gt;= moveInterval then
      moveTimer = 0.0

      val head = snake.head
      val next = direction match
        case Direction.Up    =&gt; Cell(head.col,     head.row - 1)
        case Direction.Down  =&gt; Cell(head.col,     head.row + 1)
        case Direction.Left  =&gt; Cell(head.col - 1, head.row    )
        case Direction.Right =&gt; Cell(head.col + 1, head.row    )

      val ate = next == food

      snake = if ate then next :: snake else next :: snake.init
      if ate then food = randomFood(snake)

    drawArena()
    drawFood(food)
    drawSnake(snake)

    Drawing.endFrame()

  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)

def drawSnake(snake: List[Cell]): Unit =
  snake.zipWithIndex.foreach: (cell, index) =&gt;
    val (x, y) = cellToPixel(cell)
    val color  = if index == 0 then Color.Lime else Color.Green
    Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)

def drawFood(food: Cell): Unit =
  val (x, y) = cellToPixel(food)
  Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, Color.Red)</code></pre></div><p><em><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">If you&#8217;ve skipped previous days I would highly recommend you read through them before doing Day 6.</mark></em></p><h4>Summary for today</h4><ul><li><p>The &#8220;Game State&#8221; enum.</p></li><li><p>&#8220;state&#8221; and &#8220;nextDirection&#8221; values.</p></li><li><p>Collision detection.</p></li><li><p>A bit of a restructure of the game loop.</p></li></ul><h4>Final code for today</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*

val cellSize     = 20
val columns      = 20
val rows         = 20
val gameW        = columns * cellSize
val gameH        = rows    * cellSize
val scoreBarH    = 60
val moveInterval = 0.15

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

enum Direction:
  case Up, Down, Left, Right

enum GameState:
  case Playing, GameOver

case class Cell(col: Int, row: Int)

def cellToPixel(cell: Cell): (Float, Float) =
  (offsetX.toFloat + cell.col * cellSize, offsetY.toFloat + cell.row * cellSize)

def randomFood(snake: List[Cell]): Cell =
  val free = for
    c &lt;- 0 until columns
    r &lt;- 0 until rows
    cell = Cell(c, r)
    if !snake.contains(cell)
  yield cell
  free(Random.int(0, free.length - 1))

def initialSnake: List[Cell] =
  List(Cell(10, 10), Cell(9, 10), Cell(8, 10))

@main
def main(): Unit =
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  var snake         = initialSnake
  var direction     = Direction.Right
  var nextDirection = Direction.Right
  var moveTimer     = 0.0
  var food          = randomFood(snake)
  var state         = GameState.Playing

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    state match
      case GameState.Playing =&gt;
        if      Input.isKeyPressed(Key.Up)    &amp;&amp; direction != Direction.Down  then nextDirection = Direction.Up
        else if Input.isKeyPressed(Key.Down)  &amp;&amp; direction != Direction.Up    then nextDirection = Direction.Down
        else if Input.isKeyPressed(Key.Left)  &amp;&amp; direction != Direction.Right then nextDirection = Direction.Left
        else if Input.isKeyPressed(Key.Right) &amp;&amp; direction != Direction.Left  then nextDirection = Direction.Right

        moveTimer += Timing.delta

        if moveTimer &gt;= moveInterval then
          moveTimer = 0.0
          direction = nextDirection

          val head = snake.head
          val next = direction match
            case Direction.Up    =&gt; Cell(head.col,     head.row - 1)
            case Direction.Down  =&gt; Cell(head.col,     head.row + 1)
            case Direction.Left  =&gt; Cell(head.col - 1, head.row    )
            case Direction.Right =&gt; Cell(head.col + 1, head.row    )

          val hitWall    = next.col &lt; 0 || next.col &gt;= columns || next.row &lt; 0 || next.row &gt;= rows
          val ate        = next == food
          val bodyToCheck = if ate then snake else snake.init
          val hitSelf    = bodyToCheck.contains(next)

          if hitWall || hitSelf then
            state = GameState.GameOver
          else
            snake = if ate then next :: snake else next :: snake.init
            if ate then food = randomFood(snake)

        drawArena()
        drawFood(food)
        drawSnake(snake)

      case GameState.GameOver =&gt;
        drawArena()
        drawFood(food)
        drawSnake(snake)

    Drawing.endFrame()

  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)

def drawSnake(snake: List[Cell]): Unit =
  snake.zipWithIndex.foreach: (cell, index) =&gt;
    val (x, y) = cellToPixel(cell)
    val color  = if index == 0 then Color.Lime else Color.Green
    Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)

def drawFood(food: Cell): Unit =
  val (x, y) = cellToPixel(food)
  Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, Color.Red)</code></pre></div><div><hr></div><h4>Introducing <strong>Game State</strong></h4><p><strong>&#8220;GameState&#8221;</strong> is an enum that contains two different cases:</p><ul><li><p>Playing.</p></li><li><p>GameOver.</p></li></ul><p>For our small Snake game that is all we need to determine if the player is playing the game or if they lost.</p><p>We already used &#8220;enums&#8221; before (Day 4 &#8220;Direction&#8221; enum) and today we will do something similar. We need a way to represent a set of values that <strong>won&#8217;t change</strong>, we use an &#8220;enum&#8221; for that.</p><p><strong>GameState</strong> follows the same idea as <strong>Direction</strong>.</p><p>Now the same question we asked on Day 4 &#8220;why not just use a bool like <strong>var isGameOver = false</strong>?&#8221; If we only need two states a boolean should technically work right? </p><p>Yes a boolean would work but it&#8217;s not the best way to handle these kinds of scenarios. <br>Let&#8217;s say in the future we want to add a <strong>pause menu</strong>, if we use the <strong>GameState</strong> enum we simply add a new entry <strong>Paused</strong> and we handle that case, that&#8217;s it. <br>If we had a boolean called &#8220;<strong>isGameOver&#8221;</strong> or something similar we would need to add another boolean like &#8220;<strong>isGamePaused&#8221;</strong> and write the logic only for that particular boolean value.</p><p>It is also a good way to keep the code clean and readable. If you read this code, you understand what is going on only by looking at it:</p><p><code>state = GameState.GameOver</code></p><p>You know the game is over, you need no other context to understand that.</p><h4>nextDirection and the &#8220;direction buffer&#8221;</h4><p>In Day 4 we had an intentional bug that we already fixed.<br>Input was applied to <strong>direction</strong> immediately but the snake was moving on a <strong>fixed timer</strong>.<br>If the player changes direction multiple times before the snake performs its next movement step, <code>direction</code> could change immediately even though the snake has not moved yet. This could allow the snake to reverse into itself.</p><p>For this we will use something called a <strong>buffer</strong>. If you want to read more about &#8220;buffers&#8221; you can read <a href="https://en.wikipedia.org/wiki/Data_buffer#:~:text=In%20computer%20science%2C%20a%20data,from%20one%20place%20to%20another.">this</a>, it is a bit of a complex matter but I will try to explain it easily for our purpose.</p><p>We separate the <strong>direction</strong> the player requested (by pressing a key) from the actual <strong>direction</strong> that the snake is moving.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">var direction     = Direction.Right
var nextDirection = Direction.Right</code></pre></div><p><strong>nextDirection</strong> gets updated every single frame, whereas <strong>direction</strong> is only updated at the moment the snake actually takes a step:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">if moveTimer &gt;= moveInterval then
  moveTimer = 0.0
  direction = nextDirection</code></pre></div><p>In simple words this means that no matter how fast the player spams a key, the snake can only change direction once per &#8220;move step&#8221;. Only the last input before a new step is the one that counts.</p><h4>Collision detection</h4><p>Collisions are another big and complex matter.<br>S2D offers a basic collisions package but we won&#8217;t be using that today.</p><p>We need two collision checks for our snake game:</p><ul><li><p><strong>Wall collision.</strong></p></li><li><p><strong>Self collision.</strong></p></li></ul><p>For the <strong>wall collision</strong> we can do a simple <strong>bounds check</strong>. </p><p>In our game we know the two things for our bounds check:</p><ul><li><p>Snake&#8217;s current position.</p></li><li><p>Arena (map) size.</p></li></ul><p>Look at this code and think about what it does:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val hitWall    = next.col &lt; 0 || next.col &gt;= columns || next.row &lt; 0 || next.row &gt;= rows</code></pre></div><p>We know that if the snake&#8217;s position is less than the minimum amount of columns it means it is outside of the Arena. The same goes for the maximum number of columns.<br>Note that we check if the <strong>next</strong> position the snake will be is outside. This is because we must be able to place the snake character right in the border without losing.</p><p>The same goes for the rows, we check both min and max amount.</p><p><em>Remember columns are X axis and rows are Y axis (right/left - up/down)</em></p><p>The <strong>hitWall</strong> variable is a <strong>boolean</strong>, it will return <strong>True</strong> if any of those conditions is met.</p><p>Now for the <strong>self collision</strong> we must consider something.<br>Look at this line:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val bodyToCheck = if ate then snake else snake.init
val hitSelf    = bodyToCheck.contains(next)</code></pre></div><p>The value <strong>bodyToCheck</strong> is the same as <strong>snake.init</strong> when the snake isn&#8217;t eating, and <strong>snake</strong> when it is eating. Why do we need these two values?</p><p>If the snake is not eating the tail moves away on the same step. The tail cell is about to be vacated, so the snake&#8217;s head moving into it should be allowed (because it would be moving into an empty space the next step). If we were to check against the full snake we would be blocking a valid move. This is a rule I set for the game but you could also remove this and invalidate that particular move.</p><p>When the snake eats, the tail doesn&#8217;t move, the body grows.<br>In this case we must check against the entire body.</p><h4>Performing the collision</h4><p>Since both values are booleans we do a simple check:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">if hitWall || hitSelf then
  state = GameState.GameOver
else
  snake = if ate then next :: snake else next :: snake.init
  if ate then food = randomFood(snake)</code></pre></div><p>If any of the two values is true, we set the <strong>GameState</strong> to <strong>GameOver</strong>, meaning the player lost and the game should stop.</p><p>Otherwise, the movement is valid, so we update the snake and generate new food if it ate.</p><h4>Restructuring the game loop with <strong>state match</strong></h4><p>Ok since now we have two different states the game could be in we must do a bit of a refactor in our code.<br>Look at this code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">state match
  case GameState.Playing =&gt;
    // input, movement, collision

  case GameState.GameOver =&gt;
    // just draw, nothing else</code></pre></div><p>In Scala, <strong>match</strong> is an expression that checks a value against a set of <strong>cases</strong>.<br>It runs the <strong>case</strong> of the matching branch.</p><p>We went over this on Day 4 but it is good to refresh concepts sometimes.</p><p>In today&#8217;s case we are splitting the entire <strong>game loop</strong> in two separate behaviours.</p><ul><li><p><strong>Playing</strong>: The playing branch runs all the logic we&#8217;ve built over the past few days. It checks for player&#8217;s input, moves the snake, draws on the screen, etc.</p></li><li><p><strong>GameOver</strong>: This state currently does nothing because we will handle the actual Game Over logic on Day 7. So for now it only stops the game completely and draws the arena and the snake (no input or gameplay).</p></li></ul><p>Notice what we discussed above about having a <strong>GameState</strong> enum instead of an <strong>isGameOver</strong> variable. <br>This lets us separate two big chunks of code and still have a very readable and clean structure. </p><p>If the state is <strong>Playing</strong>, the game runs and plays.<br>If the state is <strong>GameOver</strong>, the game stops.</p><h4>Running the code</h4><p>Go ahead and run the command:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">scala-cli run .</code></pre></div><p>The game plays exactly as on Day 5 but now if the snake collides with a wall or itself, the game freezes.</p><h4>Recommendations</h4><p>Try changing the collision rules and see what happens, mess around with the <strong>bound check</strong> or with the <strong>self collision</strong> to understand how it works.</p><p>Get familiar with the concept of <strong>Collisions</strong>, it is really important in game development, no matter what type of game you are working on.</p><p></p><p>Thanks a lot for reading, see you on Day 7 &#8212; The final day!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://matiasfinochio.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[S2D Development Series - Snake,Day 5: Food]]></title><description><![CDATA[Welcome to Day 5! Today we will work on adding food! By the end of the day you will see a red square randomly placed on the screen and the snake will grow when it reaches it.]]></description><link>https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-5</link><guid isPermaLink="false">https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-5</guid><dc:creator><![CDATA[Matias Finochio]]></dc:creator><pubDate>Thu, 18 Jun 2026 17:04:35 GMT</pubDate><content:encoded><![CDATA[<h4>Current state</h4><p>The code you should have at the moment should look like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*

val cellSize     = 20
val columns      = 20
val rows         = 20
val gameW        = columns * cellSize
val gameH        = rows    * cellSize
val scoreBarH    = 60
val moveInterval = 0.15

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

enum Direction:
  case Up, Down, Left, Right

case class Cell(col: Int, row: Int)

def cellToPixel(cell: Cell): (Float, Float) =
  (offsetX.toFloat + cell.col * cellSize, offsetY.toFloat + cell.row * cellSize)

def initialSnake: List[Cell] =
  List(Cell(10, 10), Cell(9, 10), Cell(8, 10))

@main
def main(): Unit =
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  var snake     = initialSnake
  var direction = Direction.Right
  var moveTimer = 0.0

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    if      Input.isKeyPressed(Key.Up)    then direction = Direction.Up
    else if Input.isKeyPressed(Key.Down)  then direction = Direction.Down
    else if Input.isKeyPressed(Key.Left)  then direction = Direction.Left
    else if Input.isKeyPressed(Key.Right) then direction = Direction.Right

    moveTimer += Timing.delta

    if moveTimer &gt;= moveInterval then
      moveTimer = 0.0

      val head = snake.head
      val next = direction match
        case Direction.Up    =&gt; Cell(head.col,     head.row - 1)
        case Direction.Down  =&gt; Cell(head.col,     head.row + 1)
        case Direction.Left  =&gt; Cell(head.col - 1, head.row    )
        case Direction.Right =&gt; Cell(head.col + 1, head.row    )

      snake = next :: snake.init

    drawArena()
    drawSnake(snake)

    Drawing.endFrame()

  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)

def drawSnake(snake: List[Cell]): Unit =
  snake.zipWithIndex.foreach: (cell, index) =&gt;
    val (x, y) = cellToPixel(cell)
    val color  = if index == 0 then Color.Lime else Color.Green
    Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)</code></pre></div><p><em><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">If you haven&#8217;t read Day 4, I&#8217;d recommend doing that first.</mark></em></p><div><hr></div><h4>Summary for today</h4><ul><li><p>Function <code>randomFood</code></p></li><li><p>Function <code>drawFood</code></p></li><li><p>Snake eating logic</p></li></ul><div><hr></div><h4>Final code for today</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*

val cellSize     = 20
val columns      = 20
val rows         = 20
val gameW        = columns * cellSize
val gameH        = rows    * cellSize
val scoreBarH    = 60
val moveInterval = 0.15

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

enum Direction:
  case Up, Down, Left, Right

case class Cell(col: Int, row: Int)

def cellToPixel(cell: Cell): (Float, Float) =
  (offsetX.toFloat + cell.col * cellSize, offsetY.toFloat + cell.row * cellSize)

def randomFood(snake: List[Cell]): Cell =
  val free = for
    c &lt;- 0 until columns
    r &lt;- 0 until rows
    cell = Cell(c, r)
    if !snake.contains(cell)
  yield cell
  free(Random.int(0, free.length - 1))

def initialSnake: List[Cell] =
  List(Cell(10, 10), Cell(9, 10), Cell(8, 10))

@main
def main(): Unit =
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  var snake     = initialSnake
  var direction = Direction.Right
  var moveTimer = 0.0
  var food      = randomFood(snake)

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    if      Input.isKeyPressed(Key.Up)    then direction = Direction.Up
    else if Input.isKeyPressed(Key.Down)  then direction = Direction.Down
    else if Input.isKeyPressed(Key.Left)  then direction = Direction.Left
    else if Input.isKeyPressed(Key.Right) then direction = Direction.Right

    moveTimer += Timing.delta

    if moveTimer &gt;= moveInterval then
      moveTimer = 0.0

      val head = snake.head
      val next = direction match
        case Direction.Up    =&gt; Cell(head.col,     head.row - 1)
        case Direction.Down  =&gt; Cell(head.col,     head.row + 1)
        case Direction.Left  =&gt; Cell(head.col - 1, head.row    )
        case Direction.Right =&gt; Cell(head.col + 1, head.row    )

      val ate = next == food

      snake = if ate then next :: snake else next :: snake.init
      if ate then food = randomFood(snake)

    drawArena()
    drawFood(food)
    drawSnake(snake)

    Drawing.endFrame()

  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)

def drawSnake(snake: List[Cell]): Unit =
  snake.zipWithIndex.foreach: (cell, index) =&gt;
    val (x, y) = cellToPixel(cell)
    val color  = if index == 0 then Color.Lime else Color.Green
    Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)

def drawFood(food: Cell): Unit =
  val (x, y) = cellToPixel(food)
  Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, Color.Red)</code></pre></div><div><hr></div><h4><strong>Spawning food randomly</strong></h4><p>We are going to start with the most important function for the day. </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">def randomFood(snake: List[Cell]): Cell =
  val free = for
    c &lt;- 0 until columns
    r &lt;- 0 until rows
    cell = Cell(c, r)
    if !snake.contains(cell)
  yield cell
  free(Random.int(0, free.length - 1))</code></pre></div><p>This function takes the current snake as a parameter and returns a Cell where the food should appear. It does two things: </p><ul><li><p>Finds every cell that the snake is not currently on.</p></li><li><p>Picks one of those cells at random.</p></li></ul><p>Let&#8217;s go through it:</p><p><strong>The for-comprehensionn</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val free = for
  c &lt;- 0 until columns
  r &lt;- 0 until rows
  cell = Cell(c, r)
  if !snake.contains(cell)
yield cell</code></pre></div><p>This code right here is called &#8220;for-comprehension&#8221; in Scala. It might look similar to a normal &#8220;for&#8221; loop in other languages but it works differently.</p><p>A normal &#8220;for&#8221; loop usually means that a certain piece of code will run repeatedly a certain number of times. A &#8220;for-comprehension&#8221; means that the code will loop over the values, it will transform those values (or filter them) and it will return a different collection with the new values inside.</p><p>In this case the &#8220;free&#8221; value becomes a collection of all the cells that are not occupied by the snake.</p><ul><li><p><code>val free = for</code> - This is the first line, it starts the for-comprehension loop and stores the result in a value named &#8220;free&#8221;</p></li><li><p><code>c &#8592; 0 until columns</code> - &#8220;c&#8221; is a new local variable that will take every integer value from 0 up to columns (not including columns itself). So basically &#8220;c&#8221; will be 0, 1, 2, &#8230; up to the amount of columns.</p></li><li><p><code>r &#8592; 0 until rows</code> - Similar to the line above, &#8220;r&#8221; is a new local variable that takes every integer value from 0 up to rows (not including rows itself). So again, &#8220;r&#8221; will be 0, 1, 2, &#8230; up to the amount of rows.</p></li><li><p><code>cell = Cell(c, r)</code> - For every combination of &#8220;c&#8221; and &#8220;r&#8221; we need to create a new Cell. This line uses &#8220;=&#8221; instead of &#8220;&#8592;&#8221; because we are not iterating over a collection, we just want to give a name to a new value.</p></li><li><p><code>if !snake.contains(cell)</code> - This line is called a &#8220;guard&#8221;. It&#8217;s purpose is to filter out any cell that the snake is already occupying. So if the current cell we are checking has a segment of the snake in it, we skip it, hence it won&#8217;t be part of the final collection.</p></li><li><p><code>yield cell</code> - The last line adds a new cell to the collection. So every cell that made it through the guard (meaning it is empty) is included in the collection returned by the for-comprehension.</p></li></ul><p>The end result stored in &#8220;free&#8221; is a collection of every cell on the grid where the snake is not present.</p><p><strong>Picking a random cell</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">free(Random.int(0, free.length - 1))</code></pre></div><p>To access the value of a collection in Scala you do this &#8220;collectionName(value)&#8221;. <br>If you take a look at the code you will see that we are doing exactly that with the &#8220;free&#8221; collection but we are choosing a random value instead of a specific one.</p><p>We will use the &#8220;Random&#8221; package from S2D for the first time.</p><p>&#8220;Random.int()&#8221; returns a random integer between a minimum and maximum value, including both ends.<br>In the code we pass &#8220;0&#8221; as the minimum and &#8220;free.length - 1&#8221; as the maximum (we need to subtract 1 from the &#8220;free&#8221; collection because it starts at index 0). </p><p><strong>randomFood</strong> takes the snake as a parameter because it is the best way to guarantee the food never spawns on top of the snake.</p><h4>Drawing the food</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">def drawFood(food: Cell): Unit =
  val (x, y) = cellToPixel(food)
  Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, Color.Red)</code></pre></div><p>If you read Day 3, this function should look familiar to you. It is almost the same as the <code>drawSnake</code> function!</p><p>We take a &#8220;cell&#8221; as the only parameter, convert that cell from grid coordinates (rows and columns) into screen coordinates (pixels) and then we draw a &#8220;rectangle&#8221;.</p><p>If you want to see how this function works in depth, please read <a href="https://open.substack.com/pub/matiasfinochio/p/s2d-development-series-snakeday-3?r=8k6nt1&amp;utm_campaign=post-expanded-share&amp;utm_medium=post%20viewer">Day 3</a>.</p><p>The main difference is that for the snake we iterate over every cell of the snake&#8217;s body because it occupies more than one cell at a time. For the food we don&#8217;t need to do that, since it is just one square that occupies one cell.</p><h4>Adding food to main</h4><p>Add this line after &#8220;moveTimer&#8221; inside the main function:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">var food = randomFood(snake)</code></pre></div><p>We pass the &#8220;snake&#8221; to the &#8220;randomFood&#8221; function so the food doesn&#8217;t spawn on top of the initial snake body.</p><p>&#8220;food&#8221; is a var because it will change every time the snake eats it.</p><h4>The eating logic</h4><p>Inside the movement block, after &#8220;next&#8221;, add this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val ate = next == fod

snake = if ate then next :: snake else next :: snake.init
if ate then food = randomFood(snake)</code></pre></div><p>Ok this is quite a bit of code, let&#8217;s go over it:</p><ul><li><p><code>val ate = next == food </code>- Here we check if the next head position is the same cell as the food cell. &#8220;Cell&#8221; is a case class, this is why we can do a simple equality check (if not we would need to do something like next.x  food.x and same for y)<br>&#8221;next == food&#8221; is true when both cells (next and food) have the exact same row and column. We are not doing a pixel comparison here, it is purely grid-based.</p></li><li><p><code>snake = if ate then next :: snake else next :: snake.init</code> - This is a small change that we are doing to the code from Day 4. <br>When the snake eats we now prepend the new head but keep the current body. So we basically add a new segment and we don&#8217;t remove the tail. This way we make the snake grow.<br>When the snake doesn&#8217;t eat, we just do the same as on Day 4 (next :: snake.init) keeping the same size.</p></li><li><p><code>if ate then food = randomFood(snake)</code> - Once the snake eats the food we calculate a new random position. We pass the updated snake (which will have +1 segment) so the new food cannot appear on any of the snake&#8217;s new cells.</p></li></ul><h4>Draw order</h4><p>Your drawing logic should look like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">drawArena()
drawFood(food)
drawSnake(snake)</code></pre></div><p>We call &#8220;drawFood&#8221; before &#8220;drawSnake&#8221;. <br>This means the food will be drawn under the snake, which is the best approach in case food somehow spawns on top of the snake (it cannot happen due to the logic we have but it is good to keep this as a good practice).</p><h4>Running the code</h4><p>Open a terminal and run this code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">scala-cli run .</code></pre></div><p>You should see a red square somewhere in the grid. Eat it with the snake and the red square should appear in a different position.</p><p>The snake still has no collisions logic so we will focus on that in the next day!</p><h4>Recommendations</h4><p>Try changing the &#8220;Color.Red&#8221; in &#8220;drawFood&#8221; to something else. <br>Try making the food spawn only in a specific region of the grid instead by adding extra checks on the logic.<br>Try printing &#8220;free.length&#8221; before picking the random index to see how many free cells are available.</p><p>Like I say every day, play around with the code and get familiar with it.</p><p>Thanks for reading and see you on Day 6!</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://matiasfinochio.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[S2D Development Series - Snake,Day 4: Movement and Input.]]></title><description><![CDATA[Welcome to Day 4! In Day 3 we drew a static snake on screen. Today we make it move. By the end of this post the snake will move around the arena!]]></description><link>https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-4</link><guid isPermaLink="false">https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-4</guid><dc:creator><![CDATA[Matias Finochio]]></dc:creator><pubDate>Mon, 15 Jun 2026 14:18:47 GMT</pubDate><content:encoded><![CDATA[<h4>Current state</h4><p>The code you should have at the moment should look like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*

val cellSize  = 20
val columns   = 20
val rows      = 20
val gameW     = columns * cellSize
val gameH     = rows    * cellSize
val scoreBarH = 60

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

case class Cell(col: Int, row: Int)

def cellToPixel(cell: Cell): (Float, Float) =
  (offsetX.toFloat + cell.col * cellSize, offsetY.toFloat + cell.row * cellSize)

def initialSnake: List[Cell] =
  List(Cell(10, 10), Cell(9, 10), Cell(8, 10))

@main
def main(): Unit =
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  val snake = initialSnake

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    drawArena()
    drawSnake(snake)

    Drawing.endFrame()

  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)

def drawSnake(snake: List[Cell]): Unit =
  snake.zipWithIndex.foreach: (cell, index) =&gt;
    val (x, y) = cellToPixel(cell)
    val color  = if index == 0 then Color.Lime else Color.Green
    Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)</code></pre></div><p>If you haven&#8217;t read Day 3, I&#8217;d recommend doing that first.</p><div><hr></div><h4>Summary for today</h4><ul><li><p>A new enum <code>Direction</code></p></li><li><p>Two new constants <code>moveInterval </code>&amp; <code>moveTimer</code></p></li><li><p>Input reading with <code>Input.isKeyPressed</code></p></li><li><p>Movement logic using <code>Timing.delta</code></p></li></ul><div><hr></div><h4>Final code for today</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*

val cellSize     = 20
val columns      = 20
val rows         = 20
val gameW        = columns * cellSize
val gameH        = rows    * cellSize
val scoreBarH    = 60
val moveInterval = 0.15

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

enum Direction:
  case Up, Down, Left, Right

case class Cell(col: Int, row: Int)

def cellToPixel(cell: Cell): (Float, Float) =
  (offsetX.toFloat + cell.col * cellSize, offsetY.toFloat + cell.row * cellSize)

def initialSnake: List[Cell] =
  List(Cell(10, 10), Cell(9, 10), Cell(8, 10))

@main
def main(): Unit =
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  var snake     = initialSnake
  var direction = Direction.Right
  var moveTimer = 0.0

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    if      Input.isKeyPressed(Key.Up)    then direction = Direction.Up
    else if Input.isKeyPressed(Key.Down)  then direction = Direction.Down
    else if Input.isKeyPressed(Key.Left)  then direction = Direction.Left
    else if Input.isKeyPressed(Key.Right) then direction = Direction.Right

    moveTimer += Timing.delta

    if moveTimer &gt;= moveInterval then
      moveTimer = 0.0

      val head = snake.head
      val next = direction match
        case Direction.Up    =&gt; Cell(head.col,     head.row - 1)
        case Direction.Down  =&gt; Cell(head.col,     head.row + 1)
        case Direction.Left  =&gt; Cell(head.col - 1, head.row    )
        case Direction.Right =&gt; Cell(head.col + 1, head.row    )

      snake = next :: snake.init

    drawArena()
    drawSnake(snake)

    Drawing.endFrame()

  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)

def drawSnake(snake: List[Cell]): Unit =
  snake.zipWithIndex.foreach: (cell, index) =&gt;
    val (x, y) = cellToPixel(cell)
    val color  = if index == 0 then Color.Lime else Color.Green
    Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)</code></pre></div><div><hr></div><h4>Direction</h4><p>Our snake can move in four directions, it would be nice if we had a type that represented those directions. <br>For this we will use an enum called &#8220;Direction&#8221;.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">enum Direction:
  case Up, Down, Left, Right</code></pre></div><p>In Scala, an &#8220;enum&#8221; is a data structure that let&#8217;s you define a set of possible values.<br>Direction can only ever be one of these four values (Up, Down, Left or Right).</p><p>We could also use a value with a string like &#8220;up&#8221; or four different integer values like &#8220;0&#8221;, &#8220;1&#8221;, &#8220;2&#8221; and &#8220;3&#8221;, where each of these numbers represents a direction.</p><p>But in our case we already know that our Direction will only have 4 options and we also already know the value of each of those 4 options, so the best option is an enum because Scala automatically returns an error if we try to use a value that is not part of the enum.</p><p>We will use &#8220;Direction&#8221; a lot more in future days when we add collision detection.</p><h4>&#8220;moveInterval&#8221; and &#8220;moveTimer&#8221;</h4><p>We will now add two lines of code.<br>First define this value at the top, below &#8220;scoreBarH&#8221;:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val moveInterval = 0.15</code></pre></div><p>This value is how many seconds our snake waits between  each step. Our snake will move one Cell at a time (one step), so this the interval of seconds between one cell and the next one. At &#8220;0.15&#8221; seconds the snake will take around 6-7 steps per second.</p><p>The next value is this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">var moveTimer = 0.0</code></pre></div><p>Define it below the &#8220;var snake = initialSnake&#8221; line, inside the main function.</p><p>This is a &#8220;var&#8221; so the value can change. Why do we use a &#8220;var&#8221; here? Because &#8220;moveTimer&#8221; is a counter. We will increment the value every frame. When the value of &#8220;moveTimer&#8221; reaches &#8220;moveInterval&#8221;, the snake moves one Cell and we reset the timer to zero.</p><h4>Initial direction</h4><p>When the game starts our snake must know what direction it has to move.<br>We could make a small system that waits for the user to press a key, for example the Left Key, and the snake would move to the left.<br>But in the original snake games, the snake would start moving to a &#8220;default&#8221; direction when the game started.</p><p>In our game we will do the same.</p><p>Add this line below &#8220;var snake = initialSnake&#8221;:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">var direction = Direction.Right</code></pre></div><p>We define a new variable (because the value will change in the future) and we assign a default value.</p><p>As you can see we are using &#8220;Direction.Right&#8221;,  this means we are assigning a value from our enum &#8220;Direction&#8221;.</p><h4>Reading user input</h4><p>Ok we&#8217;ve reached a really interesting part of our game.</p><p>To move the snake around we must somehow take the input from the user&#8217;s keyboard and pass it to our game logic.<br>If the user presses the &#8220;Left key&#8221; it means they want to move the snake to the left.</p><p>How do we do this?</p><p>Well S2D has an &#8220;Input&#8221; package that you can use for this.</p><p>Right after the &#8220;Drawing.clear()&#8221; function add these lines:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">if      Input.isKeyPressed(Key.Up)    then direction = Direction.Up
else if Input.isKeyPressed(Key.Down)  then direction = Direction.Down
else if Input.isKeyPressed(Key.Left)  then direction = Direction.Left
else if Input.isKeyPressed(Key.Right) then direction = Direction.Right</code></pre></div><p>&#8220;Input.isKeyPressed&#8221; returns &#8220;true&#8221; when a key is pressed. It only returns true once, so if you press and hold the key it will only trigger once.<br>In our case we want to use this function because we just want to move our snake once each time we press a key.</p><p>You could also use &#8220;Input.isKeyDown&#8221; which returns true every frame the key is held, but like I said, we don&#8217;t want that for this game.</p><p>One important thing to remember:</p><p><strong>Input must be read after &#8220;Drawing.beginFrame()&#8221;.</strong> This is because &#8220;beginFrame()&#8221; is where S2D polls SDL for new events, such as keyboard input.</p><p>If you added the input logic before beginFrame it would still work, but you would be reading an input state that hasn&#8217;t been updated yet for the current frame.</p><p>For our game the difference won&#8217;t cause any visible problems, but the right way of doing things is always adding the Input logic after &#8220;beginFrame()&#8221;.</p><h4>Movement with Timing.delta</h4><p>Below our Input logic go ahead and add these lines:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">moveTimer += Timing.delta

if moveTimer &gt;= moveInterval then
  moveTimer = 0.0</code></pre></div><p>What is going on here?</p><p>First of all we are adding something to our &#8220;moveTimer&#8221; variable that we defined before. <br>&#8221;Timing.delta&#8221; is a function that returns a &#8220;Double&#8221;. It returns the time in seconds that the last frame took to complete. <br>At 60FPS each frame takes roughly 0.016 seconds. <br>By adding &#8220;Timing.delta&#8221; to &#8220;moveTimer&#8221; every frame, we are counting the real elapsed time between each frame regarding the frame rate.</p><p>This is really important because if you run the game in a computer with a higher frame rate, it would run faster, and it would run slower in a PC with a lower frame rate.<br>We don&#8217;t want that, we want to always maintain the same calculation regardless of the framerate.</p><p>When &#8220;moveTimer&#8221; reaches 0.15 (moveInterval) then we move the snake and reset the timer.</p><h4>Computing the next position</h4><p>Inside the same &#8220;if&#8221; statement from last step, add these lines:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val head = snake.head
val next = direction match
  case Direction.Up    =&gt; Cell(head.col,     head.row - 1)
  case Direction.Down  =&gt; Cell(head.col,     head.row + 1)
  case Direction.Left  =&gt; Cell(head.col - 1, head.row    )
  case Direction.Right =&gt; Cell(head.col + 1, head.row    )</code></pre></div><p>Let&#8217;s go over this line by line:</p><ul><li><p><code>val head = snake.head </code>- &#8220;snake&#8221; is a List. You can use the &#8220;head&#8221; function in Scala to access the <strong>first</strong> element on a list. So &#8220;snake.head&#8221; gives us the first element of the snake List (which in our case it is also the head of the snake).</p></li><li><p><code>val next = direction match</code> - This line starts a &#8220;pattern matching&#8221; statement. Read more about <a href="https://docs.scala-lang.org/tour/pattern-matching.html">Pattern Matching</a> if you don&#8217;t know what it is.<br>We are creating a new value called &#8220;next&#8221; and we will assign a value that depends on what &#8220;direction&#8221; we have at the moment.</p></li><li><p><code>case Direction.Up =&gt; Cell(head.col, head.row - 1)</code> - If we are trying to move Up then it means that we must decrese the row by 1. (because Y increases downward). So basically if the user prompts to move Upwards, we decrease 1 row every frame.</p></li></ul><p>The same idea goes for the other 3 cases (Down, Left and Right). For Left and Right we instead increase or decrease columns, not rows.</p><p>What&#8217;s important to notice here is that we are using the four values from our &#8220;Direction&#8221; enum, we are not leaving a value unused and also we are not using a different value that is not part of the enum. The Scala compiler knows we haven&#8217;t missed any of the predefined values.</p><p>This is important because it will help you understand why we chose an enum over a simple variable.</p><h4>Moving the snake</h4><p>After the pattern matching, inside the if statement, go ahead and add this line:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">snake = next :: snake.init</code></pre></div><p>This right here is the entire movement logic, what actually moves the snake.</p><p>Let&#8217;s break it apart:</p><ul><li><p><code>snake.init</code> - Returns the entire list without its last element. In our case it would drop the &#8220;tail&#8221; of the snake. If we had a snake of three segements (head, body, tail), snake.init would return (head, body), no tail.</p></li><li><p><code>next :: snake.init</code> - this &#8220;pushes&#8221; the &#8220;next&#8221; value (defined on the pattern matching statement) into the List. So the result would be like this (next, head, body). It prepends the new value to that list that has been shortened.</p></li></ul><p>And we assign this value to the &#8220;snake&#8221; variable, which is our actual character on the game.</p><p>This is why we chose a List back in Day 3. Using &#8220;::&#8221; to push a new value to the start of a List is fast and easy to understand. The snake will grow in size from the front and shrink from the back, Lists make these two operations simple.</p><h4>Running the code</h4><p>Open a terminal and run this command</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">scala-cli run .</code></pre></div><p>You should see the snake moving to the Right on its own when the game starts.</p><p>If you press the arrow keys you should be able to change the direction of the snake.</p><p>The snake doesn&#8217;t stop at the walls, it passes through them. That is for later to fix.</p><h4>Recommendations</h4><p>Try changing the &#8220;moveInterval&#8221; value to a different number. You can set it to something like 0.05 to make the snake faster or 0.5 to make it slower.<br>Try making the initialSnake longer and watch how the movement looks.<br>Try changing the initial direction from Right to Left or something different.</p><p>Also take a look at what happens when you go through a wall. Does the snake wrap around or disappears?<br>Think about what check you would need to add to detect that. </p><div><hr></div><p>Thanks a lot for reading and see you on the next Day.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://matiasfinochio.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[S2D Development Series - Snake,Day 3: Drawing our player.]]></title><description><![CDATA[Welcome to Day 3! In Day 2 we drew the arena border. Today we introduce two important building blocks: Cell Type and cellToPixel(). By the end of this post we will see our snake on the screen!]]></description><link>https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-3</link><guid isPermaLink="false">https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-3</guid><dc:creator><![CDATA[Matias Finochio]]></dc:creator><pubDate>Fri, 12 Jun 2026 14:22:23 GMT</pubDate><content:encoded><![CDATA[<h4>Current state</h4><p>The code you should have at the moment should look like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*

val cellSize  = 20
val columns   = 20
val rows      = 20
val gameW     = columns * cellSize
val gameH     = rows    * cellSize
val scoreBarH = 60

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

@main
def main(): Unit =
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    drawArena()

    Drawing.endFrame()

  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)</code></pre></div><p><em><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">If you haven&#8217;t read Day 2, I&#8217;d recommend reading it </mark><a href="https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-2?r=8k6nt1&amp;utm_campaign=post&amp;utm_medium=web&amp;triedRedirect=true"><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">here</mark></a><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">.</mark></em></p><h4>Summary for today</h4><ul><li><p>New type <code>case class Cell</code></p></li><li><p>New functions <code>cellToPixel </code>&amp; <code>drawSnake</code></p></li></ul><h4>Final code for today</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*

val cellSize  = 20
val columns   = 20
val rows      = 20
val gameW     = columns * cellSize
val gameH     = rows    * cellSize
val scoreBarH = 60

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

case class Cell(col: Int, row: Int)

def cellToPixel(cell: Cell): (Float, Float) =
  (offsetX.toFloat + cell.col * cellSize, offsetY.toFloat + cell.row * cellSize)

def initialSnake: List[Cell] =
  List(Cell(10, 10), Cell(9, 10), Cell(8, 10))

@main
def main(): Unit =
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  val snake = initialSnake

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    drawArena()
    drawSnake(snake)

    Drawing.endFrame()

  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)

def drawSnake(snake: List[Cell]): Unit =
  snake.zipWithIndex.foreach: (cell, index) =&gt;
    val (x, y) = cellToPixel(cell)
    val color  = if index == 0 then Color.Lime else Color.Green
    Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)</code></pre></div><h4>Introducing Cell</h4><p>As we discussed in previous days, our game uses a grid.<br>Every position in that grid (where the snake is, where the food is, everything) is described by a column and a row number. <br>We need a way to represent this value and for that we will use &#8220;Cell&#8221;.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">case class Cell(col: Int, row: Int)</code></pre></div><p>In Scala, a case class is a simple data container. You define the fields the class holds and that&#8217;s kinda it.<br>In our case the &#8220;Cell&#8221; case class holds two integers: &#8220;col&#8221; for the column and &#8220;row&#8221; for, you guessed it, the row.<br>In simple words, a &#8220;Cell&#8221; is just a position on our grid which consists in a column number and a row number.</p><p>Why do we use a case class and not two separate variables? Because we want the &#8220;Cell&#8221; to be one thing, one position that holds the two values in the same place.<br>If we want to know where the snake is we need the &#8220;Cell&#8221; where it is positioned, for that we can just retrieve one value (the &#8220;Cell&#8221; value). If we were using two variables we would need to retrieve both values.</p><p>This is a way of keeping related data together which in my opinion is really important in game programming.</p><h4>From grid to pixels</h4><p>Our game logic uses grid coordinates (columns and rows) to perform it&#8217;s calculations.<br>S2D draws stuff using pixel coordinates (x and y on the screen). How do we tell S2D where to draw something if we use a completely different system? We will use something called a &#8220;helper function&#8221;.</p><p>A helper function is a piece of code that has one specific job and aims to make the code easier to read and reuse.</p><p>In our game we will use this helper function:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">def cellToPixel(cell: Cell): (Float, Float) =
  (offsetX.toFloat + cell.col * cellSize, offsetY.toFloat + cell.row * cellSize)</code></pre></div><p>This function takes only one parameter of type &#8220;Cell&#8221;. That means we will pass something like this &#8220;Cell(3, 5)&#8221;.<br>Then it returns two Float values like this (Float, Float). The first Float is the &#8220;x&#8221; position in pixels and the second Float is the &#8220;y&#8221;.</p><p>So in summary this function converts a &#8220;Cell(column, row)&#8221; into &#8220;(x, y)&#8221;.</p><p>Now let&#8217;s go through the calculation:</p><p><code>offsetX.toFloat + cell.col * cellSize</code></p><p>This calculates the &#8220;x&#8221; position. <br>&#8221;offsetX&#8221; is where the arena starts in the X axis (horizontal). We add it because the arena is not drawn directly at the left edfe of the window. If you remember on Day 2 we talked about this.</p><p>Then we take the cell column:</p><p><code>cell.col</code></p><p>an multiply it by the size of an individual cell:</p><p><code>cellSize</code></p><p>For example, if cell.col is 3 and cellSize is 20, then we do &#8220;3 * 20 = 60&#8221;</p><p>This means column 3 starts at 60 pixels from the left side of the arena.</p><p>Now we add the offsetX. If offsetX is 20, then the final position of X would be: &#8220;20 + 60 = 80&#8221;.</p><p>So the final value for column 3 would be 80, meaning it will be drawn at 80 pixels from the left side of the arena.</p><p>We do the same exact thing for y:</p><p><code>offsetY.toFloat + cell.row * cellSize</code></p><p>offsetY is where the arena starts vertically.<br>cell.row tells us the row of the cell.<br>If cell.row is 5 and cellSize is 20 then: 5 * 20 = 100</p><p>If offsetY is 30, then the final y position is: 30 + 100 = 130.</p><p>In simple words, the function &#8220;cellToPixel&#8221; transforms our grid coordinates into screen coordinates so S2D knows where to draw stuff.</p><h4>Initial snake</h4><p>Let&#8217;s look at this part of the code now:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">def initialSnake: List[Cell] =
  List(Cell(10, 10), Cell(9, 10), Cell(8,10))</code></pre></div><p>Our snake is a List of Cells, each Cell is one segment of its body. The first element in the List is always the head of the Snake.</p><p>In this case we start with three segments:</p><ul><li><p>The head at column 10, row 10</p></li><li><p>Two body &#8220;cells&#8221; at column 9, row 10 and column 8, row 10.</p></li></ul><p>We are using a List because it&#8217;s the easiest Scala collection to handle adding new cells to the &#8220;front&#8221; and removing from the &#8220;back&#8221;. This is what we are going to use to move our Snake around in a future Day.</p><p>&#8220;initialSnake()&#8221; is a def because we need to call it when the player restarts the game.<br>Every call gives us a new list, starting at the same position.</p><h4>Drawing the snake</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">def drawSnake(snake: List[Cell]): Unit =
  snake.zipWithIndex.foreach: (cell, index) =&gt;
    val (x, y) = cellToPixel(cell)
    val color  = if index == 0 then Color.Lime else Color.Green
    Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)</code></pre></div><p>Let&#8217;s go through this line by line:</p><ul><li><p><code>snake.zipWithIndex </code>- This may look strange at first, so let&#8217;s try to break it down.<br>&#8221;snake&#8221; (the parameter the function expects) is a List of Cells. Each Cell represents one part of the snake&#8217;s body as discussed above. <br>&#8221;zipWithIndex&#8221; takes each element in the list and pairs it with its position in that list. Our original List looks like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">List(Cell(10, 10), Cell(9, 10), Cell(8, 10))</code></pre></div><p>And after &#8220;zipWithIndex&#8221; it would look like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">List( 
  (Cell(10, 10), 0), 
  (Cell(9, 10), 1), 
  (Cell(8, 10), 2) 
)</code></pre></div><p>As you can see it added an &#8220;index&#8221; to the each element, the first element gets index 0, the second gets 1 and so on.</p></li><li><p><code>.foreach: (cell, index) =&gt;</code> - After &#8220;zipWithIndex&#8221; each item in the List has two parts,  the &#8220;cell&#8221; and the &#8220;index&#8221;, this is called a &#8220;pair&#8221;. For example the head of the snake has this pair: &#8220;(Cell(10, 10), 0)&#8221;, meaning (Cell = 10, 10) and (index = 0)</p><p>&#8220;.foreach&#8221; means repeat this section of code for every item in the list and we can give a name to each element in the pair. We are giving the &#8220;cell&#8221; and &#8220;index&#8221; names, that is why we do &#8220;.foreach: (cell, index)&#8221;. We are telling Scala to iterate over every element on that list and that each pair will have a &#8220;cell&#8221; and an &#8220;index&#8221; inside. These names are just an easier way we can access the elements inside the pairs, we could choose any name we want but it is usually good practice to choose a name that defines the content inside that element.</p></li><li><p><code>val (x, y) = cellToPixel(cell)</code> - Remember that the function &#8220;cellToPixel&#8221; returns a pair (two values). So we create a new value with two names (x, y) and we save the value that &#8220;cellToPixel&#8221; returns. We pass the &#8220;cell&#8221; parameter which in this case would be each individual cell from the List that we iterate with &#8220;.foreach: (cell, index) =&gt;&#8221;. </p></li><li><p><code>val color = if index == 0 then Color.Lime else Color.Green</code> - This is a bit easier, we check if the current index is 0 (meaning if it is the head of the snake), if so we paint it Lime. For the rest of the indices we paint them Green.<br>We save the color in the value named &#8220;color&#8221; (remember that we are iterating the List, so this variable will have a different value for index 0 and index 1)</p></li><li><p><code>Basics.rectangle(x.toInt, y.toInt, cellSize, cellSize, color)</code> - And this final line draws the actual body of the snake. Again, we are iterating the list so it will draw each cell individually. Our list has these coordinates (10, 10), (9, 10) and (8, 10). The variable &#8220;x&#8221; and &#8220;y&#8221; have the screen coordinates for each column because we used the &#8220;cellToPixel&#8221; function. We also use the &#8220;cellSize&#8221; value and the &#8220;color&#8221; value that we calculated above.<br>So let&#8217;s say we are processing the first index (10, 10), which would be the head.</p><p>&#8220;cell&#8221; would be (10, 10) and &#8220;index&#8221; would be 0.<br>(x, y) would be &#8220;cellToPixel(cell)&#8221;, in this case the calculation would be &#8220;10 <em>* 20 + offsetX&#8221; and &#8220;10 * 20 + offsetY&#8221;.  <br></em>&#8221;color&#8221; would be Lime because the index is 0.<br>So &#8220;Basics.rectangle&#8221; uses &#8220;x, y&#8221; for the position, &#8220;cellSize&#8221; for the size of the rectangle and &#8220;color&#8221; for the fill color.</p></li></ul><h4>Updating main</h4><p>After going through every function we now have to add these into &#8220;main&#8221;.</p><p>First we need to create the &#8220;snake&#8221;, for this we will add this line:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val snake = initialSnake</code></pre></div><p>Why are we calling &#8220;initialSnake&#8221; and not &#8220;initialSnake()&#8221; (without the parameters)?<br>Because our &#8220;initialSnake&#8221; doesn&#8217;t take parameters. In Scala, functions with no parameters can be called without the parentheses.</p><p>Then we must call &#8220;drawSnake&#8221; inside the loop, add it after &#8220;drawArena&#8221;.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">drawArena()
drawSnake(snake)</code></pre></div><p>Order matters here.<br>The arena must be before the snake, this way our arena sits behind the snake.<br>If we did it the other way around, the snake would be drawn behind the arena.</p><p>Later on we will draw more stuff and we will talk about the drawing order more in depth.</p><h4>Running the code</h4><p>Open a terminal and run this code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">scala-cli run .</code></pre></div><p>You should see the arena border from Day 2 and also a short green snake in the middle of it.<br>The head should be slightly brighter because we set it to Lime.</p><p>As you can see the snake isn&#8217;t moving yet, we will work on that on Day 4.</p><h4>Recommendations</h4><p>Try messing around with the size of the snake, change the &#8220;initialSnake&#8221; function to add more cells to the body.<br>Try changing the color of other indices, not only the head, for example make half of the body one color and the other half another color.</p><p>Try calling &#8220;drawSnake&#8221; before &#8220;drawArena&#8221; and see what happens too, that way you&#8217;ll get the draw order and how it works.</p><p></p><p>Thanks a lot for reading, see you on Day 4!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://matiasfinochio.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[S2D Development Series - Snake,Day 2: Drawing Things on the Screen.]]></title><description><![CDATA[Welcome to Day 2 of the series! On Day 1 we set up the project and opened a window. Today we will start putting stuff on the screen and learn about the "shapes" package.]]></description><link>https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-2</link><guid isPermaLink="false">https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-2</guid><dc:creator><![CDATA[Matias Finochio]]></dc:creator><pubDate>Wed, 10 Jun 2026 16:22:25 GMT</pubDate><content:encoded><![CDATA[<h4>Current state</h4><p>The code you should have at the moment should look like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:&quot;87d11fe6-157e-4130-815f-777554a874cc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.types.*

@main
def main(): Unit =
  val screenWidth  = 800
  val screenHeight = 450

  Window.create(screenWidth, screenHeight, &#8220;S2D Snake&#8221;)
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)
    Drawing.endFrame()

  Window.close()</code></pre></div><p><em><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">If you haven&#8217;t read Day 1 I&#8217;d recommend reading it </mark><a href="https://substack.com/home/post/p-201230672"><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">here</mark></a><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">.</mark></em></p><h4>Summary for today</h4><ul><li><p>A new package import: <code>s2d.shapes.*</code></p></li><li><p>Three new constants to define the size and position of the game arena.</p></li><li><p>A new function called &#8220;drawArena()&#8221;.</p></li></ul><h4>Final code for today</h4><p>Here is what our code will look like at the end of Day 2. Don&#8217;t worry, we will go over every new line of code.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.shapes.*
import s2d.types.*

val cellSize  = 20
val columns   = 20
val rows      = 20
val gameW     = columns * cellSize
val gameH     = rows    * cellSize
val scoreBarH = 60

def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2

@main
def main(): Unit =
  val screenW = gameW + 40
  val screenH = gameH + scoreBarH + 20

  Window.create(screenW, screenH, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)

    drawArena()

    Drawing.endFrame()

  Window.close()

def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)</code></pre></div><h4>The new import</h4><p>Ok, we are ready to start Day 2. The first thing we will do is to add a new import statement at the top of the file.<br>Go ahead and add this line:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.shapes.*</code></pre></div><p>As we did on Day 1 with the &#8220;core&#8221; and &#8220;types&#8221; packages, we need to import any package that contains functionality we want to use in our game.<br>The &#8220;shapes&#8221; package will let us draw basic shapes on the screen, such as rectangles, circles, triangles, etc.</p><h4>Defining the game area</h4><p>Before drawing something on the screen we must know where and how big the game area is. On Day 1 I mentioned that for this game we will be using a grid-based arena, so let&#8217;s go ahead and define a few values:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val cellSize = 20
val columns = 20
val rows = 20
val gameW = columns * cellSize
val gameH = rows * cellSize
val scoreBarH = 60</code></pre></div><p>I know, that is more than just &#8220;a few values&#8221;, but let&#8217;s go over each line:</p><ul><li><p><code>val cellSize = 20</code> - Defines the size of a single cell (in pixels). What is a cell? Well, our game will have a grid, a cell is one individual square inside that grid.</p></li><li><p><code>val columns = 20</code> - Defines how many cells our grid will have on the X axis (left to right)</p></li><li><p><code>val rows = 20</code> - Defines how many cells our grid will have on the Y axis (top to bottom)<br><em><strong><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">Note: </mark></strong><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">both values (columns and rows) are set to 20, this means we will have a 20x20 (twenty by twenty) cell grid.</mark></em></p></li><li><p><code>val gameW = columns * cellSize </code>&amp; <code>val gameH = rows * cellSize</code> - What&#8217;s going on here? Why are we multiplying values all of a sudden? Let me explain. <br>&#8220;gameW&#8221; and &#8220;gameH&#8221; stand for &#8220;game width&#8221; and &#8220;game height&#8221;, these values are the total dimensions of the game area in pixels.<br>To calculate the total width of the game area we must multiply the number of columns by the size of one individual cell. In our code this would mean &#8220;20 * 20&#8221;, which gives us a total of  &#8220;400 pixels&#8221;. For the height, it is the same. We do &#8220;20 * 20&#8221; and the total is &#8220;400 pixels&#8221;. So our game area has a total of &#8220;400x400 pixels&#8221;.</p></li><li><p><code>val scoreBarH = 60</code> - This defines the height of the area at the top of the window where we will display the score later on in the series. For now we will use it for the layout of the window.</p></li></ul><p>Notice that all of these are defined as &#8220;val&#8221;, meaning we won&#8217;t change their values during the game. Every time you want to store a value that won&#8217;t change during the application runtime, you should use &#8220;val&#8221;.</p><h4>Centering the arena on the screen</h4><p>This is an important part, pay attention to these two lines of code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">def offsetX: Int = (Window.width  - gameW) / 2
def offsetY: Int = (Window.height - gameH) / 2 + scoreBarH / 2</code></pre></div><p>These are &#8220;def&#8221;, not &#8220;val&#8221;. This difference is important and I&#8217;ll explain why.</p><p>A &#8220;val&#8221; is only evaluated once, when it is defined. A &#8220;def&#8221; is evaluated every single time it is called in the code.</p><p>&#8220;Window.width&#8221; and &#8220;Window.height&#8221; (from the core package) return the <strong>current</strong> dimensions of the window. If we resize the window, the values change.<br>By making &#8220;offsetX&#8221; and &#8220;offsetY&#8221; definitions (def), they recalculate every frame using the latest window size. We need this per frame recalculation to keep the arena centered on the window, even when the size of the window changes.</p><p>I know that maybe sounds a bit too complex, so now we will go over what is actually going on in the code.</p><p>&#8220;offsetX&#8221; tells us where the game arena should start on the X axis. To center the arena horizontally (x axis) we use the total window width and subtract the arena width (Window.width - gameW).<br>For example, if the window is 440 pixels wide and the game area is 400 pixels wide: <strong>440 - 400 = 40</strong><br>Good, we calculated the &#8220;extra&#8221; space we have available for our game arena, but we want to center it on the screen. We don&#8217;t want 40 pixels of empty space on the left side of the window, we want to distribute it evenly between the left side and the right side.</p><p>To do that we divide the total (40) by 2, giving us 20 pixels for each side of the window.</p><p>In summary this means the arena should start rendering 20 pixels away from the left side of the window (leaving a 20 pixels gap between the window and the left side of the arena). Since we distributed it evenly the arena will also have a 20 pixels empty space on the right side, centering it on the screen.</p><p>&#8220;offsetY&#8221; now let&#8217;s look at this one. The first part is exactly the same as for X but instead of using Window.width and gameW we use Window.height and gameH.</p><p>But there is an extra operation at the end, &#8220;+ scoreBarH / 2&#8221;.</p><p>Later on in the series we will add a small scoreboard, this extra bit of code pushes the arena down a few pixels to give space for this scoreboard. </p><p>scoreBarH = 60, divided by 2 gives us 30. So we are moving the arena 30 pixels down.</p><p>If this is a bit confusing for you I&#8217;d recommend running the code a few times and changing the values and taking a look at what happens to the arena on the window. <br>In my opinion this is the best way of understanding parts of the code, seeing how it directly affects the arena lets you visualize all the calculations.</p><h4>The &#8220;drawArena()&#8221; function</h4><p>We&#8217;ve reached the most important function today, the one that actually draws something on the screen.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">def drawArena(): Unit =
  Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)</code></pre></div><p>On Day 1 we didn&#8217;t talk about splitting the code into different functions, so we will do that today.<br>As your game grows you might start to get lost if every single line of code is inside the &#8220;main&#8221; function, so it is good practice to split the code into smaller chunks where each of these chunks performs one action. For this we use functions.</p><p>In our case we create a new function called &#8220;drawArena&#8221; that, as the name implies, will draw the arena in the window.</p><p>Inside this function we have one line of code:</p><p><code>Basics.rectangleOutline(offsetX, offsetY, gameW, gameH, Color.White)</code></p><p>We are calling a new class here, the &#8220;Basics&#8221; object from the &#8220;shapes&#8221; package.<br>From the Basics class we are using the &#8220;rectangleOutline&#8221; function.</p><p>This function draws only the border of a rectangle without filling the inside.<br>It takes five parameters: x position, y position, width, height, and a color.</p><p>For the position of the rectangle we will pass the &#8220;offsetX&#8221; and &#8220;offsetY&#8221; definitions.<br>For the size we will use the gameW and gameH values. Remember that this rectangle that we want to draw is the game arena, so we will use the size that we defined in the values gameW and gameH.</p><p>And for the color we will simply use White (you can choose any color that you want)</p><p>With this, we have our function ready to be used in the &#8220;main&#8221; function.</p><h4>Updating the main function</h4><p>Inside the main function from Day 1 we will add a few things.</p><p>First of all, we will update the &#8220;screenWidth&#8221; and &#8220;screenHeight&#8221; values that we had already defined and we are going to do it like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">val screenW = gameW + 40
val screenH = gameH + scoreBarH + 20</code></pre></div><p>What we are doing here is setting the size of the window to be the same size as the arena PLUS a little bit more (I also renamed them to make them shorter, but that&#8217;s a personal choice)</p><p>Again, you can go ahead and remove the 2 additions (+ 40 and + 20) and see what happens. You can also make those values bigger or smaller. <br>Mess around with the code and try to understand what is going on.</p><p>The last update to the main function is to call the &#8220;drawArena&#8221; function somewhere to actually draw it.</p><p>We will place the drawArena function call right between the &#8220;Drawing.clear()&#8221; and &#8220;Drawing.endFrame()&#8221; functions.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">Drawing.clear(Color.Black)

drawArena()

Drawing.endFrame()</code></pre></div><p>Everything that we will draw in the game will go in this place. Order matters, but for now we only draw the arena.</p><h4>Small note on coordinates</h4><p>S2D uses a coordinate system where coordinate (0, 0) is the top-left corner of the window.</p><p>X is increased to the right and Y increases downward.</p><p>This is the standard for most 2D libraries/frameworks but if you come from a math background this is probably going to be a bit confusing.</p><p>As an example try to remember that if anywhere in the code we say &#8220;offsetX = 20 and offsetY = 30&#8221; that means &#8220;20 pixels from the left and 30 from the top&#8221;.</p><h4>Running the code</h4><p>We&#8217;ve done it! We can finally run the code.</p><p>Go ahead and open a terminal (cd to your project) and run this command:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">scala-cli run .</code></pre></div><p>Again you&#8217;ll see a lot of messages on the terminal but after a bit you should see a window opening with a white rectangle, without any fill, centered on the screen.</p><p>Congrats, that&#8217;s your first drawing with S2D!!</p><h4>Recommendations</h4><p>As with Day 1 I&#8217;d recommend messing around with the values we defined today.</p><p>Try changing cellSize to 30 or 40 and see how it affects the arena.<br>Change columns and rows to make it wider or taller.<br>Remove the + 40 and + 20 from the window size and see what happens.</p><p>The more you play with the code the better you&#8217;ll understand how it works.<br>Breaking things on purpose is really important and I can&#8217;t recommend it enough.</p><div><hr></div><p>Thanks everyone for reading,<br>See you on Day 3!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://matiasfinochio.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[S2D Development Series - Snake,Day 1: Opening a Window.]]></title><description><![CDATA[Welcome to the first post of the S2D development series! In this series I will show you how to develop some small games step by step using my library S2D, and the first game in the list is... Snake!!]]></description><link>https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-1</link><guid isPermaLink="false">https://matiasfinochio.substack.com/p/s2d-development-series-snakeday-1</guid><dc:creator><![CDATA[Matias Finochio]]></dc:creator><pubDate>Tue, 09 Jun 2026 18:14:56 GMT</pubDate><content:encoded><![CDATA[<h4>What is S2D?</h4><p>Before we start writing any code we will quickly go over what S2D actually is.</p><p>S2D (Scala 2D) is a simple 2D game programming library for Scala. It was inspired by the simplicity of Raylib, being its main goal to remain as simple/basic as possible while giving you a minimal, direct API so you can focus on developing the game, not the engine.</p><p>There is one important thing that you must know about S2D, it targets Scala Native (Not the JVM or js).<br>In simple terms this means the game you build compiles to a native binary. There is no JVM dependencies, no JVM startup, no LWJGL wrappers. S2D is built directly on top of SDL2 and OpenGL.</p><p>Targetting Scala Native is, for me, what makes S2D special. Games need a language that lets you handle memory efficientely. With Native you have all the expressiveness of Scala while still having control over the Low Level design.</p><h4>Why Snake first?</h4><p>Well&#8230; why not? We&#8217;ve all built snake at least once in our lives haven&#8217;t we?</p><p>In all seriousness, I think Snake is a game that is small enough for you to actually finish building it but complete enough to at least teach you a few important concepts.</p><p>It has things like a Game Loop, User Input, Movement, Collision, States, etc. All of these are fundamental in game development.<br>It is also small enough that you can fit it in one single file which is, in my opinion, better when you are starting out.</p><p>In our case, it is good enough to introduce S2D&#8217;s API piece by piece, in small increments and in an order that makes sense.</p><h4>What you need before we begin</h4><p>To follow this series you will need:</p><ul><li><p>Scala Native up and running on your machine. You should follow the <a href="https://scala-native.org/en/stable/user/setup.html">official</a> guide for this. <em>(For this example we will use Scala 3.8.3 and Scala Native 0.5.12)</em></p></li><li><p>SDL2 and OpenGL binaries. S2D_CLI will take care of these for you.</p></li><li><p>Coursier. Follow the instructions <a href="https://get-coursier.io/docs/cli-installation">here</a> to install it.</p></li><li><p>Scala CLI. Same as Scala Native and Coursier, follow the <a href="https://scala-cli.virtuslab.org/install">official</a> guide.</p></li></ul><h4>Installing S2D CLI</h4><p>S2D uses a small CLI tool called &#8220;S2D-CLI&#8221;. This tool takes care of generating a basic template, downloading the necessary libraries, setting scala-cli up, etc. It basically sets up the environment for you to just open your code editor and start working on your game.</p><p>To install S2D-CLI we will use Coursier. Open a terminal and run this command:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">cs install s2d &#8212;contrib</code></pre></div><p>This command will pull the latest release of the CLI tool and install it on your computer.<br>You can check if the CLI was installed running this on the same terminal window:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">s2d &#8212;help</code></pre></div><p>If everything was installed correctly you should see something like this:</p><p><code>S2D - Scala 2D Native Library CLI Tool</code></p><p><code>Usage:</code></p><p><code>  s2d --generate    Generate a new S2D project template</code></p><p><code>  s2d --help        Show this help message</code></p><h4>Creating the S2D project</h4><p>In the same terminal window (or a new one if you closed the other one) navigate to wherever you want to create your projet folder. In my case I ran this command:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">cd ~/Developments/</code></pre></div><p>Once you are positioned in your folder of choice, run this command:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">s2d &#8212;generate</code></pre></div><p>Now the CLI will start asking you for a few things, don&#8217;t worry, I&#8217;ll guide you.</p><ul><li><p>Project Name: choose whatever name you like, in my case I chose &#8220;snake-game&#8221;.</p></li><li><p>Build System: in this series we will be using scala-cli, but if you are familiar with SBT you can go ahead and choose that instead.</p></li><li><p>Project Path: don&#8217;t type anything here, just press enter.</p></li></ul><p><em><strong><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">Note:</mark></strong><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);"> if you are on Linux the CLI will try to download all the needed libraries using sudo, so you might be prompted for you password. </mark></em></p><p>The CLI will load a few things, show some messages on the terminal and once it stops, you should be able to see a folder with the name of your game and a few files inside.</p><p><code>snake-game/</code></p><p><code>  assets/</code></p><p><code>  project.scala</code></p><p><code>  main.scala</code></p><p><code>  SDL.dll</code></p><p><code>  glew32.dll</code></p><p>We will quickly go over these files:</p><ul><li><p><code>assets</code> this is an empty folder where you can place all of the assets for your game, such as sprites, fonts, audio, etc.</p></li><li><p><code>project.scala</code> this file contains the information for Scala-CLI. You can open it and take a look at what&#8217;s inside, but you won&#8217;t be editing it so don&#8217;t worry too much about it.</p></li><li><p><code>main.scala</code> this is the entry point of the game and the only file we will be using in the series. You can go ahead an open it on your code editor of choice.</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;scala&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-scala">import s2d.core.*
import s2d.types.*

@main
def main(): Unit =
  val screenWidth  = 800
  val screenHeight = 450

  Window.create(screenWidth, screenHeight, "S2D Snake")
  Input.setExitKey(Key.Escape)
  Timing.setTargetFPS(60)

  while Window.isOpen() do
    Drawing.beginFrame()
    Drawing.clear(Color.Black)
    Drawing.endFrame()

  Window.close()</code></pre></div><p>Lets go over this file line by line.</p><ul><li><p><code>import s2d.core.*</code> - This imports the &#8220;core&#8221; package from the S2D library. The core package has really important files inside that let you manage things like Windows, Cursor, User Input, Drawing, etc.</p></li><li><p><code>import s2d.types.*</code> - This imports the &#8220;types&#8221; package from the S2D library. Inside you&#8217;ll see things like &#8220;Vector2&#8221; or &#8220;Color&#8221;, these are data types used by the library and it is important to have them.</p><p><em><strong><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">Note:</mark></strong><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);"> the &#8220;</mark><strong><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">*</mark></strong><mark data-color="#ead1dc" style="background-color: rgb(234, 209, 220); color: rgb(0, 0, 0);">&#8221; after the import means &#8220;import everything that is inside that package&#8221;. You can also choose to import only what you need to use.</mark></em></p></li><li><p><code>@main</code> - This tells the Scala compiler that the function &#8220;main&#8221; below is the entry point of the program.</p></li><li><p><code>def main(): Unit =</code> - This defines a new function called &#8220;main&#8221;. Since it has the <code>@main</code> above it, Scala will use it as the entry point of the program.</p></li><li><p><code>val screenWidth = 800</code> and <code>val screenHeight = 450</code> - These two lines create two values with the names screenWidth and screenHeight. In Scala the &#8220;val&#8221; keyword means these values cannot be changed. When a value cannot be change it is called &#8220;Immutable&#8221;. </p></li><li><p> <code>Window.create(screenWidth, screenHeight, &#8220;Snake&#8221;)</code> - Ok this is the first line where we actually use the library, so what does it do? It opens a window! We use the &#8220;<strong>Window</strong>&#8221; class from the &#8220;<strong>core</strong>&#8221; package we imported earlier. You have to pass some parameters for it to work, following the order of the code above you give it a &#8220;<strong>window width</strong>&#8221;, &#8220;<strong>window height</strong>&#8221; and a &#8220;<strong>window title</strong>&#8221;. It is important to notice that we aren&#8217;t actually displaying anything yet, the window just exists there without an actual purpose.</p></li><li><p><code>Input.setExitKey(Key.Escape)</code> - Here we are using both the &#8220;core&#8221; package and the &#8220;types&#8221; package. We use the &#8220;Input&#8221; class (from core) to call the function &#8220;setExitKey&#8221;. This function needs a parameter of type &#8220;Key&#8221; (from types).<br>So we pass the &#8220;Escape&#8221; key as a parameter. <br>This function tells S2D what key we must press to close the window (and cleanup, stop the game loop, etc).</p></li><li><p><code>Timing.setTargetFPS(60)</code> - This line uses the &#8220;Timing&#8221; class and calls the function &#8220;setTargetFPS&#8221;. In simple terms this tells S2D to cap the the loop speed to 60 frames per second. If this isn&#8217;t set the game would run as fast as your CPU allows, so every computer would run the game at different speeds.</p></li><li><p><code>while Window.isOpen() do</code> - We&#8217;ve reached the &#8220;game loop&#8221;. This while statement uses a S2D function as an expression &#8220;Window.isOpen()&#8221;. This function returns a boolean, true if the window is open, false if it isn&#8217;t. So basically this while loop will run as long as the window stays open.</p></li><li><p><code>Drawing.beginFrame()</code> - The first line on the loop starts a new &#8220;frame&#8221;. What is a frame? Well to keep it as simple as possible, one frame is one complete image of the game, one complete draw. Above we set the loop speed to 60 frames per second, that means we are drawing 60 times every second. So we are calling &#8220;beginFrame&#8221; 60 times, every second. Every frame can be compared to a new &#8220;snapshot&#8221; of the game.</p></li><li><p><code>Drawing.clear(Color.Black)</code> - This line is simple, as its name says it &#8220;clears&#8221; the screen with a given &#8220;Color&#8221;. It paints the entire screen with the color you choose. </p></li><li><p><code>Drawing.endFrame()</code> - We called &#8220;beginFrame()&#8221; and now we need a way to end it. Everything between &#8220;beginFrame&#8221; and &#8220;endFrame&#8221; will be what you will be drawing on the screen for the current frame. By calling &#8220;endFrame&#8221; we tell S2D that there is nothing else to draw on that frame and it is ready to be displayed on the screen.</p></li><li><p><code>Window.close()</code> - We&#8217;ve made it to the end of the file!! The last line closes the window and also takes care of cleaning up all of the SDL2 and OpenGL resources from memory. This line is run once the &#8220;while&#8221; loop is over.</p></li></ul><h4>Running the code</h4><p>Open the terminal again if you closed it. From inside the project folder run this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">scala-cli run .</code></pre></div><p>Scala-CLI will compile the entire project (it can take a moment the first time you run it) and launch the game. <br>If everything is correct you should see an 800x450 black window, with the title &#8220;S2D Snake&#8221;.</p><p>You can go ahead and close it by pressing Escape on your keyboard or the X on the window.</p><p>Congratulations, you just run your first S2D project!</p><h4>Recommendations</h4><p>I would suggest that you take a look at the files in the project, try to understand the code and mess around a bit with it.<br>Do things like changing the window color, the title, dimensions, the closing key, etc.</p><p>Familiarize yourself with the structure of the code and how it works.</p><p>Making games is a long and tough process, it takes time, effort, patience, lots of things. <br>In my opinion it is really important to know the tools that you are using for your project, understand how these tools work and what you can and cannot do with them.</p><h4>Day 2</h4><p>For day 2 we will start drawing things on the screen. </p><p>We will draw a simple game board (a grid) that will act as the &#8220;arena&#8221;.<br>We will use the &#8220;Basics&#8221; package, which includes lots of different shapes that we can use to build games, talk about how coordinates work and explain some more game related contents.</p><p>Hope you liked the post and let me know if there&#8217;s anything that I should work on to make it better for you!</p><p>Thanks and see you on Day 2.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://matiasfinochio.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>