Dart Snake Project Part 1: Showing Off Dart (And Learning New Things)

As I highlighted in my teaser post about my “Snake Game Written in Dart in Many Ways” , I’ve successfully written my first game ever, Snake, using just Dart but targeting almost every target you can think of: command line interfaces (CLI), desktop apps on Windows/Mac/Linux, mobile apps on iOS/Android, and websites. With respect to the web deployment this allowed me explore the entire range of how to write a web app with Dart: from hand-coding your own web app in plain old Dart to using the Jaspr framework to have a more Flutter-like experience but using traditional website components, to Flutter Web essentially rasterizing the Flutter app in the browser.

With this series of posts I want to provide a simplified example of how Dart allows for targeting these various systems with maximal code reuse in the “business logic” layers. This could be used by others as an example of how to achieve that. I also want to compare the various web-deployment options for ease of use, development, code size, performance etc. Again this started as “hey I should explore server side Dart”.

What this project is not is an exploration of expert advice on how to write or design a game . I’ve never written one before, although I’ve been writing software professionally since the second half of the 1990s. It is not necessarily the best practices for any of these technologies, especially the Dart Web and Jaspr technologies since I am just learning them myself.

This first post will be about the overall architecture, the game mechanics, and the core game logic library. Future posts will delve into all the details more deeply. The code for this project, as well as screenshots, and other details can be found at the dart-snake GitLab project page . This is open sourced under an Apache 2.0 License.

High Level Architecture

First lets look at the high level architecture of the Dart Snake project. From an architecture perspective we can imagine this in a traditional layered design:

Diagram of the Dart Snake Architecture as described below.

Starting at the bottom we have the Dart standard libraries for doing things like managing Collections, String operations, and that sort of thing. Some of these libraries or runtime environments may be talking down to native libraries as well. This is specifically true for the “ncurses” library for the CLI version of the application. The power of Dart is that we don’t spend a lot of time thinking about those lower level accesses through. Through its Foreign Functions Interface(FFI) it mostly feels like we are working with plain old Dart objects. Similiarly the cross-platform nature of Dart is such that even though we are going to be deploying to such a varied number of environments much of the game logic, the so called “business logic layer” can be shared across them. It is only when we get to the presentation layer that we really have to start specializing our code.

For the different platforms we are trying to target we have a few options. An interactive CLI program is usually written with a library called ncurses . There is a Dart package that has already wrapped that library called dart_ncurses . For targeting a GUI desktop app or mobile there is of course Flutter . For targetting web we can also use Flutter but Flutter isn’t writing web UIs in the traditional sense, with HTML documents and manipulating them programmatically. Instead it is rendering everything like it does with desktop and mobile apps. This is an important difference I hope to explore later. Dart itself has its origins in the web and continues to support writing websites with it. However this is very much a framework-free bare bones web app development experience. The Jaspr Project , founded by Kilian Schulte aims to create a Flutter-like framework experience but using traditional web components.

If we look at the project from a file hierarchy perspective we have a project for each of the target presentation layers technology linking to a shared “snake_lib” Dart library project:

.
├── snake_console
├── snake_flutter
├── snake_jaspr
├── snake_lib
└── snake_web

We will spend the rest of this post talking about the core “business logic” game library and the game mechanics. With each subsequent post we will explore the implementation of the various presentation layer frameworks and deployments.

The Snake Game Mechanics

While the Snake game was pretty ubiquitous in the early mobile phone era, for the sake of those who may not have seen it I am going to go go into how the game works. Below is an animated GIF of some minor game play to go along with it.

Diagram of the Dart Snake Architecture as described below.

The player is a “snake” (the green dot) that is looking to eat “food” (the yellow dot). Each time the snake eats food it grows by one dot in length. The snake is constantly moving “forward” at a constant rate. The player can only control the direction by using arrow keys/buttons. The game ends if the snake loops back on itself, as in the “head” collides with the tail.

Snake Game Core Implementation

├── board_position.dart
├── direction.dart
├── food.dart
├── game_board.dart
├── game_board_extensions.dart
├── game_board_tile.dart
├── game.dart
└── snake.dart

Now that we have how the game works lets look at the major components and implementation. We will be looking solely at the “core” game code that would be in the snake_lib folder common to all. The main over-arching component is of course the game, represented by the Game class. This object keeps track of and manages all the other pieces. There are essentially two “characters” in the game. The first is the food that is being eaten, represented by the Food class, and the snake, represented by the Snake class.

The Game class encapsulates all of the “world” logic of the game. It has the width and height of the game world. It has a master clock called “heartbeat” which ticks time away at an appropriate rate to make game play reasonable. At each time tick it does an “update” of the game if it is still not over. It checks and sees if the snake is up against one of the outside walls and traveling towards it still from the end of the last step. If it is it forces the snake to turn along that wall towards the end it is furthest from. It has the snake “take a step” into this time step. With the new snake position it tests if the snake collided with itself and thus the game is over. Lastly it checks if the head of the snake is on the same point as the food. If it does the snake “eats” the food and a new piece of food is randomly generated somewhere on the board that doesn’t contain any of the snake.

The Food class doesn’t have anything to do except take into account its location. The Snake class has a bit more to keep up with. The first thing the snake class needs to do is keep track of where the snake is heading. When it is told to take a step it advances one unit in the heading direction, where heading is: up, down, left, or right. The snake also has to keep track of all of its pieces too. Its “body” therefore isn’t just one thing but a list of “SnakeSegments” where each segment has its location on the board. Technically as a snake moves each of the segments will move forward in a comparable direction. However since we are not drawing any of that motion the snake really only keeps track of its current “footprint” on the board. Therefore with each step it creates a new element to mark the new head’s position and it removes the last element of the tail. The only time it doesn’t trim the last element is if it grew in the last step and therefore it needs to keep the one extra square to be the proper length moving forward.

In terms of game state the Game Object has everything it needs to do all the bookkeeping on time, characters, etc. However we are going to want to interact with the world, specifically view it as the game progresses. It therefore has hooks for various game events which can then be used for updating the “presentation” of the game:

  • beforeStep handler is called at the beginning of the “update” method before any updates to game state are made
  • afterStep handler is called after all the game state updates are made in the update step
  • onGameOver is called when the game is over because of the snake collision
  • onStopRunning is called if the user quits the game.

The GameBoard class is used to create an abstract representation of the state of the game world for visualization. It too has a height and width like the Game class. However all it is managing are the GameBoardTile objects. It initializes its view of the world with all tiles either being a background tile or a border tile. It implements a “beforeUpdate” and “afterUpdate” handler for updating where the food and snake pieces are on the board in any given time step. It has mechanisms for letting the outside world get the tiles as well.

The GameBoardTile object has only two pieces of information: its position on the board and its current state. A game tile will be representing a tile that either has: the border, the background, food, the snake head, or the snake body. This is a mutable class that allows the state of the tile to change but not the position. If we look at the way it is coded something may seem peculiar:

class GameBoardTile extends StateNotifier<TileState> {
 final BoardPosition position;

 GameBoardTile(super.state, {required int x, required int y})
     : position = BoardPosition(x: x, y: y);

 void update(TileState newState) {
   state = newState;
 }
}

While the position has an explicit member variable the state seems to have come out of some place else. It comes frome the StateNotifier class that we are extending here. StateNotifier is not part of standard Dart. It is part of a state management framework called Riverpod . A lot of the “magic” of Flutter comes from its Declarative UI pattern. It is the same pattern that is used in SwiftUI, React, and other frameworks. Essentially when you lay out your user interface components you don’t worry about updating them per-se. You design it to draw the current state of the data. The framework is responsible for determining if state has changed and generating a new version of your component with the new state. In order to do that the framework needs to know that the state of something changed. That’s where things like the StateNotifier comes in. In the update method it looks like we are just assigning a new value to the GameBoardTile instance’s state variable. However because we are extending StateNotifier it is doing more than that. It tracks the fact that something changes and notifies any objects that are listening for that change. The frameworks can take this information and make some smart decisions about what needs regenerating and what does not to efficiently re-render the UI with the updated state information.

The last aspect of the GameBoard we want to cover is how the updates will get initiated. Essentially we want the game board state to do any necessary cleanup before the update and any necessary creations after the update. This is the part where it erases the tail of the snake, if necessary, or moves the food. This data is all leveraging information and functionality about the Snake, SnakeSegments, and Food. In many languages we’d put that in the classes themselves. Or perhaps we’d extend those classes or wrap them in other classes. In Dart we will instead use extension methods . Extension methods are a mechanism that allows us to add new functionality to a class only where we need it. So some generic user of Snake won’t see any of the GameBoard Snake methods but these allow GameBoard to operate on the Snake directly with these methods. An example of an extension would be on the SnakeSegments class:

extension SnakeSegmentExtensions on SnakeSegment {
  void draw(GameBoard gameBoard, {TileState state = TileState.body}) {
    gameBoard.updateTile(x, y, state);
  }

  void clear(GameBoard gameBoard) {
    draw(gameBoard, state: TileState.background);
  }
}

This adds a draw and clear method which takes a GameBoard object and updates the tile in that location using the GameBoard’s update method. So any code that has important these extensions can do snakeSemgent.clear(), for example. You can see the rest of the methods that achieve comparable thing for Snake and Food. With this last piece we’ve tied together the Game itself with a representation of the game state ready for use in all of our various presentation layers. We will leverage the frameworks themselves, the state notification system, and more extension methods to easily use this core game logic in each of our presentation layer frameworks.

Conclusion

With all of the above we have seen the overall architecture and how we will be using various Dart-based technologies to reach our individual targets. We have seen how all of the game logic is coded up in this business logic layer that can be used by all. Next up is exploring each of these deployment target implementations.