Hnefatafl is an old Nordic two-person chess-like board game in which an attacker must try to capture the defender’s king before the king can escape by reaching a corner of the board.

This tutorial is about building the game with Flutter.

First I develop a class Hnefatafl which represents the game with its board and pieces and which implements the rules for moving and capturing.

which represents the game with its board and pieces and which implements the rules for moving and capturing. Thereafter, I develop a HnefataflView widget which displays the game board and allows the input of moves, that is, allows to play the game.

This way, “model” and “presentation” are clearly separated and I can benefit from Flutter’s functionally reactive approach. The board is displayed, changed and then completely displayed again. Another advantage (which I don’t make use of here) is that the Hnefatafl class is testable independently of the GUI.

For simplicity’s sake, both players share a device and take turns.

The Board

I’m starting by modelling the game board.

It has 11x11 square fields, but there are three different field types. In the middle is the throne field, in the four corners are the castle fields. Together they are called structure fields. Everything else I call a plain field.

My crude impression of the tafl board

In general, each field of the board can contain a maximum of one figure, of which there are again three types: The white king, the 12 white defenders and the 24 black attackers.

…populated with white defenders and black attackers

Only the king may enter structure fields and is safe there from capture.

I could either maintain a list of all 37 pieces with their coordinates and use it to construct the board as needed, or the other way around, maintaining just the board as a 2-dimensional array, using coordinates to address its elements. I refrain from doing both because then I have to synchronize two sources of truth.

Since I have to check neighboring fields for capturing pieces, the second variant seems to be the better one. To make things simpler, I also decide to not directly represent structures but make the game itself know about these special fields by looking at the coordinates.

Coord

Because I want to use the [] operator to access fields of the board (the 2-dimensional array) and because Dart unfortunately doesn’t allow for more than one argument inside those square brackets (like board[x, y] ) I start the modelling with an immutable value class for coordinates. Then I can use board[Coord(x, y)] as a work-around. Sometimes, I miss the elegance and expressive power of Swift…

A Coord is just a pair of x and y values. Because I want coordinates to behave like other value types, I override == and hashCode (the latter only because Dart forces me to do so):

class Coord {

final int x, y; /// Constructs a new [x]/[y] coordinate pair.

const Coord(this.x, this.y); @override

bool operator ==(dynamic c) => x == c.x && y == c.y; @override

int get hashCode => x.hashCode ^ y.hashCode;

}

Board

The Board class represents the board. Because Dart knows only 1-dimensional data structures (called List ), I need to map my coordinates to a flat list. To make the class more abstract and reusable, I don’t restrict it to square sizes and add a generic type P to represent the pieces. Unfortunately, because I want to use an enum type to represent the different kinds of pieces and enum s cannot conform to an interface in Dart, I again envy the expressive power of Swift and have to pass the initial value for all pieces explicitly in the constructor. I don’t want to use null here, hoping for a Dart version that support “non nullable” types arriving any moment now.

class Board<P> {

final int width, height;

final List<P> _pieces; /// Constructs a new empty game board with

/// [width] by [height] fields.

Board(this.width, this.height, P piece) :

_pieces = List.filled(width * height, piece); /// Returns whether the coordinate [c] is inside the board.

bool isValid(Coord c) =>

c.x >= 0 && c.x < width && c.y >= 0 && c.y < height; /// Returns the piece at the coordinate [c] (which must be valid).

P operator [](Coord c) => _pieces[c.x + c.y * width]; /// Updates the board at the coordinate [c] (which must be valid).

void operator []=(Coord c, P piece) =>

_pieces[c.x + c.y * width] = piece;

}

Piece

The following enumeration represents the contents of a field:

enum Piece { none, attacker, defender, king }

The Rules

After this preliminary work I can now implement the actual game logic with the help of the class Hnefatafl . The first task is to setup the board by placing all game pieces:

class Hnefatafl {

static const size = 11; final Board<Piece> board = Board(size, size, Piece.none); /// Resets the board and puts all pieces on the board.

void setup() {

// clear the board

for (int y = 0; y < size; y++) {

for (int x = 0; x < size; x++) {

board[Coord(x, y)] = Piece.none;

}

} final center = size ~/ 2; // place 24 attackers at 4 edges

for (int i = center - 2; i < center + 3; i++) {

board[Coord(i, 0)] = Piece.attacker;

board[Coord(0, i)] = Piece.attacker;

board[Coord(i, size - 1)] = Piece.attacker;

board[Coord(size - 1, i)] = Piece.attacker;

}

board[Coord(center, 1)] = Piece.attacker;

board[Coord(1, center)] = Piece.attacker;

board[Coord(center, size - 2)] = Piece.attacker;

board[Coord(size - 2, center)] = Piece.attacker; // place 12 defenders around the center

for (int y = -2; y < 3; y++) {

final o = 2 + (y < 0 ? y : -y);

for (int x = center - o; x < center + o + 1; x++) {

board[Coord(x, center + y)] = Piece.defender;

}

} // place 1 king at the center

board[Coord(center, center)] = Piece.king;

} ...

}

At this point it might be helpful to define the rules:

The two sides are called attacker and defender.

Attacker and defender take turns moving one piece until one side has won.

The attacker begins.

A piece can be moved one or more empty spaces either horizontally or vertically but not diagonally.

Only the king can enter the throne or a castle field.

Other pieces may skip the empty throne while moving.

If a move causes an opponent’s piece (except the king) to stand between the piece just moved and another piece of the side that is currently moving (including the king in case of the defender) or an empty throne or castle, it is captured and immediately removed from the board.

The defender wins if the king is standing on a castle field.

The attacker wins if the king is surrounded on all four sides by either an attacker or the empty throne.

As mentioned above, I don’t store the field type as part of the Board class. Therefore I need methods isThrone and isCastle to check whether a field at the given coordinate is such a structure:

... /// Returns whether the valid coordinate [c] is a throne.

bool isThrone(Coord c) => c.x == size ~/ 2 && c.y == size ~/ 2; /// Returns whether the valid coordinate [c] is a castle.

bool isCastle(Coord c) =>

(c.x == 0 && c.y == 0) ||

(c.x == 0 && c.y == size - 1) ||

(c.x == size - 1 && c.y == 0) ||

(c.x == size - 1 && c.y == size - 1); ...

Another helper method returns the coordinate of the king. Hopefully, there is one (and only one) on the board:

... /// Returns the coordinate of the only king of the board.

Coord get kingCoord {

for (int y = 0; y < size; y++) {

for (int x = 0; x < size; x++) {

if (board[Coord(x, y)] == Piece.king) return Coord(x, y);

}

}

throw AssertionError();

} ...

Next are two methods that can check whether the attacker or the defender has won according to the above rules.

Whether the defender has won is easy to determine — the king must stand on one of the four corner squares:

... /// Returns whether the defender has won

/// by moving the king onto a castle.

bool get defenderWon => isCastle(kingCoord); ...

Whether the attacker has won or not looks a bit more complicated. For all neighbors I check whether such a field exists, whether it contains an attacker or the throne. Note, I don’t have to check whether the throne is empty because I assume there’s only one king and the king is the only piece that may occupy this field:

... /// Returns whether the attacker has won by trapping the king.

bool get attackerWon {

final c = kingCoord;

return const [Coord(-1, 0), Coord(1, 0),

Coord(0, -1), Coord(0, 1)]

.map((d) => Coord(c.x + d.x, c.y + d.y))

.every((c1) => board.isValid(c1) &&

(board[c1] == Piece.attacker || isThrone(c1)));

} ...

Another method is to enumerate all possible target fields valid according to rules to move a piece to. I will use this method to highlight all fields the user may choose after selecting a piece to move.

... /// Returns all coordinates the piece at the valid coordinate [c]

/// can move to.

Iterable<Coord> validMoves(Coord c) sync* {

final piece = board[c];

if (piece == Piece.none) return;

bool shouldStop(Coord c) => board[c] != Piece.none;

bool canEnter(Coord c) =>

piece == Piece.king || !(isThrone(c) || isCastle(c));

// up

for (int y = c.y - 1; y >= 0; y--) {

final c1 = Coord(c.x, y);

if (shouldStop(c1)) break;

if (canEnter(c1)) yield c1;

}

// down

for (int y = c.y + 1; y < size; y++) {

final c1 = Coord(c.x, y);

if (shouldStop(c1)) break;

if (canEnter(c1)) yield c1;

}

// left

for (int x = c.x - 1; x >= 0; x--) {

final c1 = Coord(x, c.y);

if (shouldStop(c1)) break;

if (canEnter(c1)) yield c1;

}

// right

for (int x = c.x + 1; x < size; x++) {

final c1 = Coord(x, c.y);

if (shouldStop(c1)) break;

if (canEnter(c1)) yield c1;

}

}

While I often miss Swift when developing with Flutter, I must confess that iterables with yield are a great way to simpify complex algorithms. Instead of using a temporary list of results I can yield coordinates as I’m computing them.

If the given coordinate contains a piece (otherwise the method was called when it shouldn’t) I look at all four directions, enumerating the coordinates of all fields that are empty (see shouldStop ). Because attackers and defenders cannot enter the throne field, but can skip over it, the logic is a bit more complex.

Finally, I need a method that lists all squares on which pieces are caught after a piece has moved. I can then remove them before checking if there is a winner which finishes a move.

... /// Returns all coordinates of pieces which are trapped by the

/// piece at the coordinate [c].

Iterable<Coord> trap(Coord c) sync* {

final piece = board[c];

if (piece == Piece.none) return;

bool isEmptyStructure(Coord c) =>

board[c] == Piece.none && (isThrone(c) || isCastle(c));

for (Coord d in const [Coord(-1, 0), Coord(1, 0),

Coord(0, -1), Coord(0, 1)]) {

final c1 = Coord(c.x + d.x, c.y + d.y);

final c2 = Coord(c.x + d.x * 2, c.y + d.y * 2);

if (board.isValid(c1) && board.isValid(c2)) {

if (piece == Piece.attacker) {

if (board[c1] == Piece.defender &&

(board[c2] == Piece.attacker || isEmptyStructure(c2)))

yield c1;

} else {

if (board[c1] == Piece.attacker &&

(board[c2] == Piece.defender ||

board[c2] == Piece.king || isEmptyStructure(c2)))

yield c1;

}

}

}

} ...

This is the most complicated method of Board . Starting from a given piece which is assumed to have just moved there, I check all neighboring fields. One side must enclose the other side with two of its own pieces or a piece and a castle (the castle is always empty because otherwise, the defender already won) or the empty throne, or if the active side is the defender, the king can be used instead of a normal piece.

The move method makes it easy to perform moves:

... /// Moves a piece from valid coordinate [from] to valid coordinate

/// [to], removing all captured pieces from the board. There must

/// be a piece at [from] and there must be no piece at [to]. The

/// piece must be allowed to move to [to].

void move(Coord from, Coord to) {

assert(board.isValid(from) &&

board.isValid(to) &&

board[from] != Piece.none &&

board[to] == Piece.none &&

validMoves(from).contains(to));

final piece = board[from];

board[from] = Piece.none;

board[to] = piece;

trap(to).forEach((c) => board[c] = Piece.none);

}

}

This was the complete game logic in ~160 lines.

We could “play” the game now…

final game = Hnefatafl();

final from = Coord(0, 3);

final to = Coord(3, 3);

game.move(from, to);

The UI

Let’s start with a minimal Material app widget:

class MyApp extends StatelessWidget {

@override

Widget build(BuildContext context) {

return MaterialApp(

title: 'Hnefatafl',

theme: ThemeData.dark(),

home: HnefataflView(),

);

}

}

The HnefatalflView is a stateful widget that contains an instance of Hnefatafl . Later, we’ll extend it to keep track of the game state, that is the side that may move, and the currently selected field while moving. Right now, it’s just a Scaffold , using a BoardView to display the board:

class HnefataflView extends StatefulWidget {

@override

_HnefataflViewState createState() => _HnefataflViewState();

} class _HnefataflViewState extends State<HnefataflView> {

final _game = Hnefatafl()..setup(); @override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text('Hnefatafl'),

),

body: Center(

child: BoardView(

board: _game.board,

builder: (context, coord, piece) {

return Container();

},

),

),

);

}

}

To display the board, I could have used a column of rows, a layout builder or a multi widget layout thingy, a stack with cleverly positioned children or even my own RenderObject -based layout, but I’m using a Table here.

The Board<P> delegates creating fields to a builder function called FieldBuilder<P> . Its only concern is to layout width x height square fields and drawing some borders.

I confess, by making Board generic, I made stuff more difficult than needed.

typedef FieldBuilder<P> =

Widget Function(BuildContext context, Coord coord, P piece); class BoardView<P> extends StatelessWidget {

final Board<P> board;

final FieldBuilder<P> builder; const BoardView({

Key key,

@required this.board,

@required this.builder,

}) : super(key: key); @override

Widget build(BuildContext context) {

List<TableRow> rows = [];

for (int y = 0; y < board.height; y++) {

List<Widget> fields = [];

for (int x = 0; x < board.width; x++) {

final c = Coord(x, y);

fields.add(

AspectRatio(

aspectRatio: 1,

child: builder(context, c, board[c]),

),

);

}

rows.add(TableRow(children: fields));

}

final side = BorderSide(color: Colors.brown);

return Container(

decoration: BoxDecoration(

border: Border.all(

color: Colors.brown,

width: 3,

),

),

padding: EdgeInsets.all(4),

child: Table(

border: TableBorder(

left: side,

right: side,

top: side,

bottom: side,

horizontalInside: side,

verticalInside: side,

),

children: rows,

),

);

}

}

If you’re following along, this is how your app should look now:

everything is better with dark mode

Next, I will mark the structure fields by creating a custom Field widget that uses a Decoration to display a cross (feel free to create more elaborate art here). I want my fields to be scalable, so I don’t want to use a bitmap image. Therefore, I’m using some kind of CustomPainter here.

Because of my earlier design decision to make the Board not know the field type, I had to pass the Coord to the FieldBuilder so that this function can use the game to determine whether the field should be displayed decorated or not.

Creating custom decorations is possible, but not that common. I have to subclass Decoration and provide my own BoxPainter subclass:

class _FieldDecoration extends Decoration {

final Color color; const _FieldDecoration(this.color); @override

BoxPainter createBoxPainter([onChanged]) {

return _FieldPainter(color, onChanged);

}

}

The BoxPainter works similar to a CustomPainter . And no, don’t ask me about the onChanged callback. I just copied this from other decoration subclasses of the framework. I haven’t fully understood it. The painter’s paint method is straight forward, though. Inset by 3 points, I draw the cross by drawing two lines on the provided canvas, using the provided size.

class _FieldPainter extends BoxPainter {

final Paint _paint; _FieldPainter(Color color, VoidCallback onChanged)

: _paint = Paint()

..style = PaintingStyle.stroke

..color = color

..strokeWidth = 1,

super(onChanged); @override

void paint(Canvas canvas, Offset offset,

ImageConfiguration configuration) {

var rect = (offset & configuration.size).deflate(3);

canvas.drawLine(rect.topLeft, rect.bottomRight, _paint);

canvas.drawLine(rect.topRight, rect.bottomLeft, _paint);

}

}

Here is a Field widget that uses the new decoration:

class Field extends StatelessWidget {

final Piece piece;

final bool isDecorated; const Field({

Key key,

@required this.piece,

this.isDecorated = false,

}) : assert(isDecorated != null),

super(key: key); @override

Widget build(BuildContext context) {

return Container(

decoration: isDecorated ? _FieldDecoration(Colors.brown)

: null,

);

}

}

Now let’s use the Field widget in the FieldBuilder function in the build method of the _HnefataflViewState class:

...

builder: (context, coord, piece) {

return Field(

piece: piece,

isDecorated: _game.isThrone(coord) || _game.isCastle(coord),

);

},

...

don’t mistake the throne for a castle

Next, I will display attackers, defenders, and the king. To make things simple, attackers are drawn as brown (which obviously is the new black) circles, defenders as white circles, and the king will be a white square. Feel free to use more sophisticated graphics in your own implementation.

Here is mine as part of the Field widget:

class Field extends StatelessWidget {

... @override

Widget build(BuildContext context) {

return Container(

decoration: isDecorated ? _FieldDecoration(Colors.brown)

: null,

padding: EdgeInsets.all(4),

child: _buildPiece(),

);

} Widget _buildPiece() {

switch (piece) {

case Piece.none:

return null;

case Piece.attacker:

return Container(

decoration: BoxDecoration(

shape: BoxShape.circle,

color: Colors.brown,

border: Border.all(color: Colors.brown.shade300),

),

);

case Piece.defender:

return Container(

decoration: BoxDecoration(

shape: BoxShape.circle,

color: Colors.white,

border: Border.all(color: Colors.brown.shade300),

),

);

case Piece.king:

return Container(

decoration: BoxDecoration(

color: Colors.white,

border: Border.all(color: Colors.brown.shade300),

),

);

default:

throw AssertionError();

}

}

}

It’s starting to look like the real game:

the battle is about to begin…

To move pieces, the user taps the piece to move and then the field the piece shall move to. After the first tap, all possible target fields are highlighted. A tap on any other field but one of the highlighted once will cancel the move.

To implement this, I need to keep track of the currently selected field, that is, its coordinate. Once a field is selected, I compute a set of target fields and use it inside the field builder function to highlight them. For this, I will use two shades of white.

Here are the changes to _HnefataflViewState :

class _HnefataflViewState extends State<HnefataflView> {

final _game = Hnefatafl()..setup(); Coord _selected; @override

Widget build(BuildContext context) {

final targets = Set.of(_selected != null

? _game.validMoves(_selected)

: []); ... builder: (context, coord, piece) {

return GestureDetector(

onTap: () => _onSelected(coord),

child: Field(

piece: piece,

isDecorated: _game.isThrone(coord) ||

_game.isCastle(coord),

isSelected: _selected == coord,

isTarget: targets.contains(coord),

),

);

},

...

} void _onSelected(Coord coord) {

setState(() {

if (_selected == null) {

if (_game.board[coord] != Piece.none) {

_selected = coord;

} else {

_selected = null;

}

} else {

if (_game.validMoves(_selected).contains(coord)) {

_game.move(_selected, coord);

}

_selected = null;

}

});

}

}

And here is the next version of my Field widget which supports two new properties isSelected and isTarget :

class Field extends StatelessWidget {

final Piece piece;

final bool isDecorated;

final bool isSelected;

final bool isTarget; const Field({

Key key,

@required this.piece,

this.isDecorated = false,

this.isSelected = false,

this.isTarget = false,

}) : assert(isDecorated != null),

assert(isSelected != null),

assert(isTarget != null),

super(key: key); @override

Widget build(BuildContext context) {

return Container(

color: isSelected

? Colors.white24

: isTarget ? Colors.white10 : Colors.transparent,

child: ...

);

} ...

}

Tapping any piece will now highlight all valid target fields. Tapping an empty field will remove the highlight again. Tapping a target field will move the piece and remove all captured opponents.

before and after the move of a white defender…

All that is left are some finishing touches.

I add a reset button to the app bar to reset the game. I also add a message that informs about the active player and whether a player has won. For this, I wrap the board view into a column and add a text widget. And I add a variable _side to keep track of the active player and change _onSelected once again to make sure that only the active play may move a piece (misusing Piece here instead of creating a new enumeration Side was a bad decision).

Here is the source code:

class _HnefataflViewState extends State<HnefataflView> {

final _game = Hnefatafl(); Piece _side;

Coord _selected; @override

void initState() {

super.initState();

_onReset();

} @override

Widget build(BuildContext context) {

final targets = Set.of(_selected == null

? []

: _game.validMoves(_selected));

return Scaffold(

appBar: AppBar(

title: Text('Hnefatafl'),

actions: <Widget>[

IconButton(

onPressed: _onReset,

icon: Icon(Icons.refresh),

),

],

),

body: Column(

mainAxisAlignment: MainAxisAlignment.center,

children: [

Text(_message),

SizedBox(height: 16),

BoardView(

board: _game.board,

builder: (context, coord, piece) {

return GestureDetector(

onTap: () => _onSelected(coord),

child: Field(

piece: piece,

isDecorated: _game.isThrone(coord) ||

_game.isCastle(coord),

isSelected: _selected == coord,

isTarget: targets.contains(coord),

),

);

},

),

],

),

);

} void _onReset() {

setState(() {

_game.setup();

_side = Piece.attacker;

_selected = null;

});

} String get _message {

if (_game.defenderWon) return 'The defender has won!';

if (_game.attackerWon) return 'The attacker has won!';

return 'Your turn, ${_side == Piece.attacker

? 'attacker'

: 'defender'}.';

} void _onSelected(Coord coord) {

setState(() {

if (_selected == null) {

if (_game.board[coord] == _side ||

_game.board[coord] == Piece.king &&

_side == Piece.defender) {

_selected = coord;

} else {

_selected = null;

}

} else {

if (_game.validMoves(_selected).contains(coord)) {

_game.move(_selected, coord);

_side = _side == Piece.attacker

? Piece.defender

: Piece.attacker;

}

_selected = null;

}

});

}

}

And there you have it: A complete board game written in Flutter, using about 170 lines of code for the game logic and an additional 230 lines of code for the presentation (11KB of formatted source code).

an easy victory for “black” — and a rather confused “white” side

Unfortunately, it’s not as easy to master the game as to implement it. I’ll make the full source code available in a few days – and update this article if done so.

Thanks for reading this article and please check out my other articles, too.