1..00..1…1.1….0..1..0..11..0…0

Published on October 30, 2020

The article is part of technical blog series on my first game made with Flutter, Bineroo. It is a puzzle game with 3 simple rules to respect in order to fill in a grid. Here the trailer video to get a better idea of the gameplay.



If you are interested in Why Bineroo? Why Flutter? and details on the inception of the project, I encourage you to read the introductory article It all started with a mosquito.


Starting with this article, I will dive a bit deeper into more technical details on the project. Readers will need a bit of understanding of the Flutter framework.


📚 Analysis of the requirements


The core element of the game is a grid.


Bineroo Grid

A grid is composed of N by N elements called tiles (N = 6 on the example above). The first rule of the game is “Equal number of blacks and whites in each column and row”, therefore you guessed that N is always an even number.


Each individual tile can be in one of the following states:

  • 0 (represented by a white circle ⚪️ )

  • 1 (represented by a black circle ⚫️)

  • Empty (represented by an empty circle with a red border)


Now you understand why it is called Bineroo 😉


Each tile can also be mutable or not. This depends on its initial state. If a tile is given a non-empty initial state (i.e. 0 or 1), it is considered immutable. It will keep its initial state during the whole game. If a tile is given an empty initial state, then it is considered mutable.


From a UX perspective, I decided that tapping on a mutable tile would make it shift from one state to another in the following order:

empty ➡ 0 ➡ 1 ➡ empty and so on.


In order to represent the state of the whole grid at any moment, I chose to use a string of N*N characters composed of 0’s, 1’s, and dots (dot representing an empty element). The title of this article should be a bit less confusing now 😄.


Tapping on a mutable tile will make it shift its state (as explained above) and consequently will also change the state of the whole grid. At every change, we need to check if the grid has been totally and successfully completed. If it is a success, the user is notified and the game stops. If there are still some empty elements or errors, nothing happens.


If we wanted to be totally purist, verifying the current state would require to check the respect of all 3 game rules in order to decide if the completed grid is correct or not.


This is not a difficult exercise but for the sake of simplifying this article and focusing more on widgets rather than on algorithm, I took the liberty to add a solution string. Checking the correctness of a grid is simple as checking if the current state of the grid represented by its string variable is equal to the solution string. Voila!


Once again, I know that for a given initial grid state, there may be multiple possible correct answers. I have a cool exercise in mind, purely algorithmic around that subject. I promise I will come back to it in a future post but for the moment, please stick to my simplistic option.


At that point, I believe everything is crystal clear and we can head to our favorite IDE (VS Code for me) to start the fun part: coding.


👨‍💻 Let’s code


First, start by creating a new Flutter project

> flutter create project_name


Let’s start with the Tile widget by creating a new file named tile.dart in the lib folder.


As mentioned above in the requirements, a tile may have a changing state if it is mutable. Consequently, the Tile widget extends the StatefulWidget class and we also need a _TileState class that extends State<Tile> class.


In terms of properties, a tile will have:

  • A value represented by a String of a unique character with one of the following possibilities “0”, “1”, “.”

  • A mutable state represented by a bool

  • A position represented by an int equals the index of the tile within the grid String

  • A callback function represented by a dynamic type *var *that will be called every time a mutable tile is changed


import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

class Tile extends StatefulWidget {
  int _pos;
  String _value;
  bool _isMutable;
  var _onTapped;

  String get value {
    return _value;
  }

  int get pos {
    return _pos;
  }

  Tile(this._pos,this._value, this._onTapped){
    this._isMutable = (_value !='0') && (_value !='1');
  }

  @override
  State<StatefulWidget> createState() => _TileState();
}

class _TileState extends State<Tile> {
  @override
  Widget build(BuildContext context) {
    //TODO
  }
}


The build method of the TileState class is pretty straightforward as the widget is very simple: A Container with a circle shape encapsulated within a GestureDetector to handle the tap events. No need to comment on this simple code.



class _TileState extends State<Tile> {
  Color _getTileColor() {
    if (widget._value == '0') {
      return Colors.white;
    } else if (widget._value == '1') {
      return Colors.black;
    } else {
      return Colors.transparent;
    }
  }

  @override
  Widget build(BuildContext context) {
    Color _tileColor = _getTileColor();

    return GestureDetector(
      child: Container(
        height: 100,
        width: 100,
        margin: EdgeInsets.all(5.0),
        decoration: BoxDecoration(
          color: _tileColor,
          shape: BoxShape.circle,
          border: Border.all(
              color: _tileColor == Colors.transparent ? Colors.red : _tileColor,
              width: 1.0,
              style: BorderStyle.solid),
        ),
      ),
      onTap: (){
        if(widget._isMutable){
          setState((){
            if (widget._value == '0') {
              widget._value = '1';
            } else if (widget._value == '1') {
              widget._value = '.';
            } else {
              widget._value = '0';
            }
          });
          widget._onTapped(context, widget);
        }
        // Do nothing if it is immutable
      },
    );
  }
}


Let’s move on to the Grid widget.


Create a grid.dart file in the lib folder


First question: Is Grid a Stateless or Stateful widget? From the first point of view, one would think of a Stateful widget as the grid state is changing over time. The answer is not obvious and I chose to extend the StatelessWidget class. So why?


Making the grid stateful would mean that every time the state is changed, the whole has to be redrawn. In our case, the grid state is changed every time a mutable tile is tapped. But we only need to redraw the tapped tile and not the whole grid. That would be a waste of performance. Thanks to the callback function, the tapped tile can inform the grid of its change so this latter can take the appropriate action (here: check for success). Each tile is already a stateful widget, therefore the whole grid does not need to be stateful.


There are always multiple solutions to the same problem but as a rule of thumbs, I always start by making my widgets stateless and, when the need for redrawing comes, I ask myself what part of the screen needs to be changed and I make the smallest component Stateful to fit that requirement.


🌟 Bonus: How to convert a stateless widget to stateful with key shortcuts


  1. Click on the stateless class you want to convert
  2. Tap Ctrl + Shift + . (this is a dot)
  3. Select to Convert to StatefulWidget from the drop-down


This Ctrl + Shift + . has a lot of other possibilities to help you refactor you code. It works very well. I recommend you have a closer look at it to save some time.


The grid has 3 properties:

  • its initial value

  • its current value

  • its solution


All properties are represented by a String of N*N characters.


The build method of the Grid widget is again very simple. It is just a GridView with a list of Tile as children. I used the GridView.count constructor directly inspired by the Flutter cookbook.



import 'dart:math';
import 'package:flutter/material.dart';

import 'tile.dart';

class Grid extends StatelessWidget {

  String _init;
  String _value;
  String _solution;

  Grid(this._init,this._solution){
    _value = _init;
  }

  @override
  Widget build(BuildContext context) {
    return GridView.count(
        crossAxisCount: sqrt(this._init.length).toInt(),
        children: List.generate(_init.length, (pos){
          return Tile(pos,_init[pos],onTapped);
        }
      )
    );
  }

  void onTapped(BuildContext context, Tile tile){
      // TODO
  }
}


The onTapped method is passed as a parameter to every instantiated tile and is called every time a mutable tile is changed.


A simple operation on the current String value of the grid is necessary to update it (NB: the position parameter of the tapped Tile is here necessary to know the index of character of the grid value to modify).


Once updated, we just check if the new value is equal to the solution or not. If it is the case, then we show an AlertDialog to inform the user of success (NB: showDialog method requires a BuildContext. *This is the reason why we pass a BuildContext parameter via the onTapped method).


void onTapped(BuildContext context, Tile tile){
    print("Tile ${tile.pos} tapped - value: ${tile.value}");

    //update grid value
    _value = _value.replaceRange(tile.pos, tile.pos+1, tile.value);
    print("New grid value: $_value");

    if (_value == _solution){
      print("Success");
      showDialog(
        context: context,
        builder: (BuildContext context) => AlertDialog(
          title: Text("Congratulations"),
          content: Text("You completed the grid with success"),
          actions: [
            FlatButton(
              child: Text("Close"),
              onPressed: (){
                Navigator.of(context).pop();
              }
            )
          ],
        )
      );
    }

  }

At this point, the coding can be considered as completed. At least for a first iteration. We can move to the next phase and see what’s missing and/or not working as expected


🔧 Test and Fix


Flutter and all its tooling ecosystem are very graphical so when it comes to testing, I always favor building a simple app to embed my widget and see how they behave rather than using unit test code. I know it is not great for automated CI/CD pipeline but for the time being, it will be just fine.


So, delete all code from the generated file main.dart in the lib folder and replace it with the following code:


import 'package:flutter/material.dart';
import 'package:playground/grid.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Container(
          color: Colors.blue,
          padding:EdgeInsets.all(10),
          child: Center(
            child: Grid(
              '1..00..1...1.1....0..1..0..11..0...0',
              '101001010101110010001101010110101010'
            )
          )
        ),
      ),
    );
  }
}

The app uses the Material design. The main widget is a Container with a blue background and a Grid at its center (NB: Grid is encapsulated in a Center widget to place the grid at the center of the Container that automatically takes the whole available space).


Run the code on a local emulator and Bingo


Grid not centered

Well …. not so Bingo


How come the grid is stuck at the top of the screen?


This is where the DevTools become very handy.


DevTools

Your widget tree should look like this


Widget Tree

The problem comes from the fact that most of the widgets of the tree have no imposed constraints from their parents.


If there is one rule to understand and keep in mind is this one:


Constraints go down. Sizes go up. Parent sets position.


More details here


In our case, MaterialApp imposes no constraints on Scaffold that imposes no constraints on Container that imposes …. Down to the Tile. The tile is the only widget with imposed constraints. Why? The GridView is taking the whole available space (no constraints and no sizes). But we specified in the GridView constructor that we wanted N elements on the cross axis (here the horizontal). Therefore the framework will divide the screen widget in N and assign this value as the width of each tile. As tile are squared, their height is equal to their width. The tile has constraints both in width and height.


So how to fix this problem? As the rule states, you can set sizes. The problem here is that the grid is taking the whole screen while we wanted it to be a square of size equals to the screen width. Let’s do that.


Wrap the GridView inside a Container and set the container height to the screen width (using MediaQuery widget). NB: Use the Ctrl + Shift + . to do the wrapping 😉


@override
Widget build(BuildContext context) {
    return Container(
        height: MediaQuery.of(context).size.width,
        child: GridView.count(
            crossAxisCount: sqrt(this._init.length).toInt(),
            children: List.generate(_init.length, (pos){
            return Tile(pos,_init[pos],onTapped);
            }
        )
        ),
    );
}

Re-run and now it should be all fine.


Yes

Tap the tile and make sure the immutable ones are not changing and the mutable ones are correctly moving from one state to the next one.


Let me help you to test the success verification by showing you the solution for the test grid


Grid centered

As expected the code is working for any grid size. Here are a few examples:

N = 8 / Initial value = 1.0..0.....1.0...0....1...1....00..0..1..10...101.0....0...1.1..

N = 10 / Initial value =

.1......1.0.0...0.110..1..0.....1..........0...1..1......1.1.0.1....0..0............00........1..0..

N = 12 / Initial value = 0.......1..000.00.1.11.....0.1.0..1.0.................1..0.0.0..1.11...01.1..0..0......0.....1.1.1.....00.......0.1....11.........1..00.0.0.0..1

N = 14 / Initial value = 00.....1..1..00..1...11....1..11......1...00..0..0..1.110..0..1....1....0....0..1..1.1..1.1.1..00.......1....0...1..0..1.1...1..0..1.....00.11.0..0........1...1.0...0..1.1......0..1.11..01....00.0


I won't give you the answers. You need to figure it out by yourself. Code a solving algorithm. Solve it on paper. Do as you want. I will be happy to get your answers as comments on this article.


This is the end of my first technical article on Bineroo. I hope you enjoyed it. Feel free to react and leave any comments. Always good to get some feedback.


The next article will be about persistence. How to save and resume a grid and having the possibility to play multiple grids in parallel without having to restore from the beginning.


Once again, this blog is meant to be a living thing. If you have any topics you want me to talk about, please leave a comment with your request.




PS: I am a great fan of Gitlab but too bad it is impossible to embed Gitlab snippets in Medium (Open issue here https://gitlab.com/gitlab-org/gitlab/-/issues/26936) and I find codepen.io, not that convenient. Had to put snippets on Github 😐

Bineroo Logo

Bineroo

Play 1600 Takuzu grids for free, and challenge other players on daily challenges and duels

DARGIL copyright