DEV Community

Cover image for Introduction to Mobile Game Dev: How to Build a Basic Chess Game on Mobile in Flutter

Introduction to Mobile Game Dev: How to Build a Basic Chess Game on Mobile in Flutter

This article is co-authored by Ryan Lucas Canama

"I want to make a game on my phone, but where do i start?"

Whether you mentally said yes or even if you don’t even relate, read on to learn how to make a chess game using Flutter.

What is Game Development?

Game development is the process of creating a video game from an initial idea all the way to a finished, playable product. Game development is one of the best environments for learning programming and systems thinking because feedback is immediate and tangible. You write code, run the game, and see exactly what syour logic does in motion and in real time.

It's also a fun way to learn programming because you're making a game — since when is making games ever not fun?!

Introduction to Flutter and Virtual Machines

Flutter is an open source UI toolkit by Google where a single codebase can be used for cross-platform applications such as for mobile, web and desktop. It has many advantages such as Hot Reload where you can instantly see the changes you make, the dart language that is strongly typed and fast, and as previously mentioned, its cross-platform application.

In order to run games we need a virtual machine, Android Studio both developed by Google, goes on hand in hand with Flutter. It provides the ability to create emulators for multiple devices in order to simulate how an application runs on its intended environment with the luxury of being able to edit and run your changes in real time.

Why use Flutter to start Mobile Game Development?

Apart from being mainly used as a UI toolkit throughout its release, flutter has become a serious contender for mobile game development—specifically for indie developers and logic-based projects.

The average road map on being a game developer usually goes from mastering 2D "logic based" and "Game loop" focused games then pivot more on to 3D games using tools like Unity, Godot, and Unreal Engine. Flutter can be helpful as a starting point to mobile game dev because of it's beginner friendly dart programming language and acts as a gentler entry into the workspace.

If your goal is to go professional or build complex games, treat Flutter as a stepping stone and plan to move to Unity or Godot eventually.

Why Build A Chess Game?

Chess is a surprisingly excellent first "real" project — not because it's easy (it isn't), but because of what it forces you to learn.

You'll implement an 8x8 board using a 2D array and flat arrays. This teaches you coordinate systems, indexing, and how games represent spatial data — foundational for almost every game type.

it also teaches you on Object-Oriented Design by modeling pieces using classes and objects.

Let's Start!

Be careful that this tutorial assumes that you have already installed the pre-requisite softwares needed to run a mobile android dev environment!

Required pre-requisites:

  1. In case you don't have Flutter installed yet, click here.
  2. For the Android emulator, if you haven't installed it yet, click here.
  3. For setting up a Virtual Device on Android Studio, click here

Setting up a new project

Assuming that you have downloaded the needed prerequisites (Install Flutter, Flutter Extension on VSCode, Android emulator on Android Studio), start a new project on VSCode by simply heading to the command palette by clicking on View>command palette>Flutter: new project or use Ctrl+Shift+p, then click on the new projec,t then click Application. It will act as a template for our game.

VSCode

Command Palette

Once clicked and a folder has been created, name your project as you please, but follow Flutter’s project naming rule:

Wrong naming of project

Correct naming of project

just wait for a few minutes until flutter has fully created the config files. The terminal on Vs code will display this output once the config files are finished:

//Terminal
[chess_game] flutter create --template app --overwrite .
Creating project ....
Resolving dependencies...
Downloading packages...
Got dependencies.
Wrote 130 files.


All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev


In order to run your application, type:


  $ flutter run


Your application code is in .\lib\main.dart.


exit code 0

Enter fullscreen mode Exit fullscreen mode

Now we can start setting up our game!

Set up Game Board and Environment

Prepare Emulator on VScode

Ater finishing the installation of the config files, set up the virtual machine that will be needed to emulate the game.
By clicking the No device button at the bottom of VSCode, Select the android emulator that you have set up on Android Studio to boot it up.

It is recommended to boot up the emulator before running the flutter project because it takes more time to run than the project itself.
-Author

No Device Button

Available Emulators

You can stop, restart and hot reload the emulator with the control panel

Emulator control panel

Set up main.dart

Next, head to the lib folder and click on the main.dart file

Sidebar Opened Lib

After taking a look at the main.dart pre-built code, delete everything inside the stateless widget because we don't need them for our game.

import 'package:flutter/material.dart';


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


class MyApp extends StatelessWidget {
  const MyApp({super.key});


  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: GameBoard(


      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Create game_board.dart file

After cleaning the main.dart, create a new file on the lib folder and name it game_board.dart,
This is where we’ll be making all the main features of our chess game including initializing the pieces and the moves of individual pieces. After creating the file create a stateless widget:

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


class Game_board extends StatelessWidget {
  const Game_board({super.key});


  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}
Enter fullscreen mode Exit fullscreen mode

Then replace the const Placeholder with a scaffolding widget and add a gridview.builder as body of the widget. The gridview.builder along with the itemBuilder callback function creates a scrollable grid over the entire screen which will act as a basis on our game board.

//game_board.dart
class Game_board extends StatelessWidget {
  const Game_board({super.key});


  @override
  Widget build(BuildContext context) {
    return Scaffold(


      body: GridView.builder(gridDelegate:
       const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 8),
       itemBuilder: (context, index) => Text(index.toString()),

    ));
  }
}
Enter fullscreen mode Exit fullscreen mode

Board with many numbers

Since this is not the output that we want, let’s adjust the gridview a little bit by adding an item count to limit the numbers on the screen:

//game_board.dart
class Game_board extends StatelessWidget {
  const Game_board({super.key});


  @override
  Widget build(BuildContext context) {
    return Scaffold(


      body: GridView.builder(
      physics: const NeverScrollableScrollPhysics(),
      gridDelegate:
      const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 8),
      itemCount: 8*8,
      itemBuilder: (context, index) => Center//puts the number in the center of the square
      (child: Text(index.toString())),

    ));
  }
}
Enter fullscreen mode Exit fullscreen mode

But even after removing the excess squares, it would still be scrollable so we would need to put physics: const NeverScrollableScrollPhysics(), as shown above

8*8 Board

To make actual tiles without numbers, make a folder within lib named “components” and within that, make a file called “squares_gb.dart”. inside the file, we make a boolean named isWhite and initialize it to the constructor, then return a container widget and a calling method color in order for the squares to turn light green and if not, it turns darker green.

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




class SquaresGb extends StatelessWidget {
  final bool isWhite;
  const SquaresGb({super.key, required this.isWhite});




  @override
  Widget build(BuildContext context) {
    return Container(
      color: isWhite ? Colors.green[200] : Colors.green[700],


    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Then, create a folder inside the lib folder and name it helper_methods, that way, you can refer to it later on and make the code cleaner by just referring to the helper. Now, inside the recently made folder, we insert the code below so that we can make a checkered pattern.

//helper_methods.dart
bool isWhite(int index){
int x = index ~/ 8;// for Row
       int y = index % 8; // for Column


       bool isWhite =(x + y) % 2 == 0;//Alternates colors
       return isWhite;
}
Enter fullscreen mode Exit fullscreen mode

Set up Images on pubspec.yaml

Now that you have made your game board, let's set up the pieces! What you have to do first is gather all the needed images into one folder.

And make sure to use .png images that are color black so it can be customized later!
-Author

Pieces Images

Now drag the folder into your lib folder along with the dart files that belong on your code.

Image of folder location

Now head back to VSCode and head to the pubspec.yaml file and "uncomment the assets section”.

From:

#pubspec.yaml
flutter:


  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true


  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg
Enter fullscreen mode Exit fullscreen mode

To:

#pubspec.yaml
flutter:


  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true


  # To add assets to your application, add an assets section, like this:
  assets:
  - lib/images/
  #   - images/a_dot_ham.jpeg
Enter fullscreen mode Exit fullscreen mode

Then run on the terminal

#Terminal
flutter pub get
Enter fullscreen mode Exit fullscreen mode

This should be the output

#Terminal
Resolving dependencies... 
Downloading packages... 
  characters 1.4.0 (1.4.1 available)
  matcher 0.12.17 (0.12.18 available)
  material_color_utilities 0.11.1 (0.13.0 available)
  meta 1.17.0 (1.18.1 available)
  test_api 0.7.7 (0.7.9 available)
Got dependencies!
5 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.
Enter fullscreen mode Exit fullscreen mode

Now the game can access your images folder when needed!

Creation of pieces and selection function

Now that we have set our images, create a new file named chess_ pieces.dart in here, we can put the specifications of our different chess types:

//chess_pieces.dart
enum ChessPieceType {pawn, rook, knight, bishop, king, queen,}


class ChessPieces {
  final ChessPieceType type;
  final bool isWhite;
  final String imagePath;


  ChessPieces({
    required this.type,
    required this.isWhite,
    required this.imagePath,});


}

Enter fullscreen mode Exit fullscreen mode

We clarified there on the constructor that in order to create a ChessPieces object, we need to pass an image, a color, and a piece type.

Set that aside and create a folder named “values” and a file called “color_values.dart”, this will be for our background and foreground color. We separate this from the code above so our code would look cleaner and would not overlap with other functions.

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


var backgroundClr =  const Color.fromARGB(255, 1, 99, 1);
var foregroundClr = const Color.fromARGB(201, 142, 228, 120);
Enter fullscreen mode Exit fullscreen mode

Now we can go back to the squares_gb.dart and add the ChessPieces boolean to the squares:

//squares_gb.dart
import 'package:chess_sparcs/components/chess_pieces.dart';
import 'package:flutter/material.dart';


class SquaresGb extends StatelessWidget {
  final bool isWhite;//decides the color of the pieces
  final ChessPieces? pieces; //add Chess Pieces to squares
const SquaresGb({
    super.key,
    required this.isWhite,
    required this.pieces,
 });


  @override
  Widget build(BuildContext context) {
 return Container(
       child: pieces != null ? Image.asset(pieces!.imagePath,
        color: pieces!.isWhite ? Colors.white : const Color.fromARGB(255, 105, 1, 1),
        ) : null, //returns the image and color of the piece otherwise, none will appear
   }
}   
Enter fullscreen mode Exit fullscreen mode

Let us also return to our game_board.dart since we added the ChessPieces object into the square’s constructor, we might as well create a 2D List of all the positions in the board that will have a piece on.

//game_board.dart
class GameBoard extends StatefulWidget {
  const GameBoard({super.key});


  @override
  State<GameBoard> createState() => _GameBoardState();
}


class _GameBoardState extends State<GameBoard> {
  // 2D list representing the ChessBoard
    late List<List<ChessPieces?>> board; // we use late to  declares a chess board that will be set up later.
  //...the rest of your code in game_board
Enter fullscreen mode Exit fullscreen mode

Now, before we initialize our 2D list lets create a void function to initialize our game board pieces entirely:

  //game_board.dart
  @override
  void initState() {
    super.initState();
    _initalizeBoard();
  }
 void _initalizeBoard() {
    //initialize the board withe nulls, no pieces in those positions
    List<List<ChessPieces?>> Newboard = List.generate(
      8,
      (index) => List.generate(8, (index) => null),
    );
}
Enter fullscreen mode Exit fullscreen mode

Now, inside our void function, we will input the initial positions of all of our chess pieces, starting with the pawns:

//game_board.dart
//place pawns
    for (int i = 0; i < 8; i++) {
      Newboard[1][i] = ChessPieces(
        //black pawn
        type: ChessPieceType.pawn,
        isWhite: false,
        imagePath: 'lib/images/pawn_transparent.png',
      );
      Newboard[6][i] = ChessPieces(
        //white pawn
        type: ChessPieceType.pawn,
        isWhite: true,
        imagePath: 'lib/images/pawn_transparent.png',
      );
    }
Enter fullscreen mode Exit fullscreen mode

Now, as you can see, the boolean isWhite is now a requirement in all pieces, because it is now the one that decides if it's a white or black piece.

For Rooks:

//game_board.dart
//place rooks
    Newboard[0][0] = ChessPieces(
      //black rook
      type: ChessPieceType.rook,
      isWhite: false,
      imagePath: 'lib/images/Rook_transparent.png',
    );
    Newboard[0][7] = ChessPieces(
      //black rook
      type: ChessPieceType.rook,
      isWhite: false,
      imagePath: 'lib/images/Rook_transparent.png',
    );
    Newboard[7][0] = ChessPieces(
      //black rook
      type: ChessPieceType.rook,
      isWhite: true,
      imagePath: 'lib/images/Rook_transparent.png',
    );
    Newboard[7][7] = ChessPieces(
      //black rook
      type: ChessPieceType.rook,
      isWhite: true,
      imagePath: 'lib/images/Rook_transparent.png',
    );
Enter fullscreen mode Exit fullscreen mode

For Knights:

//game_board.dart
//place knights
    Newboard[0][1] = ChessPieces(
      //black knight
      type: ChessPieceType.knight,
      isWhite: false,
      imagePath: 'lib/images/knight_transparent.png',
    );
    Newboard[0][6] = ChessPieces(
      //black knight
      type: ChessPieceType.knight,
      isWhite: false,
      imagePath: 'lib/images/knight_transparent.png',
    );
    Newboard[7][1] = ChessPieces(
      //white knight
      type: ChessPieceType.knight,
      isWhite: true,
      imagePath: 'lib/images/knight_transparent.png',
    );
    Newboard[7][6] = ChessPieces(
      //white knight
      type: ChessPieceType.knight,
      isWhite: true,
      imagePath: 'lib/images/knight_transparent.png',
    );
Enter fullscreen mode Exit fullscreen mode

For Bishops:

//game_board.dart
//place bishops
    Newboard[0][2] = ChessPieces(
      type: ChessPieceType.bishop,
      isWhite: false,
      imagePath: 'lib/images/Bishop_transparent.png',
    );
    Newboard[0][5] = ChessPieces(
      type: ChessPieceType.bishop,
      isWhite: false,
      imagePath: 'lib/images/Bishop_transparent.png',
    );
    Newboard[7][2] = ChessPieces(
      type: ChessPieceType.bishop,
      isWhite: true,
      imagePath: 'lib/images/Bishop_transparent.png',
    );
    Newboard[7][5] = ChessPieces(
      type: ChessPieceType.bishop,
      isWhite: true,
      imagePath: 'lib/images/Bishop_transparent.png',
    );
Enter fullscreen mode Exit fullscreen mode

For Queens:

//game_board.dart
//place queens
    Newboard[0][3] = ChessPieces(
      type: ChessPieceType.queen,
      isWhite: false,
      imagePath: 'lib/images/queen_transparent.png',
    );
    Newboard[7][3] = ChessPieces(
      type: ChessPieceType.queen,
      isWhite: true,
      imagePath: 'lib/images/queen_transparent.png',
    );
Enter fullscreen mode Exit fullscreen mode

For Kings:

//game_board.dart
 //place kings
    Newboard[0][4] = ChessPieces(
      type: ChessPieceType.king,
      isWhite: false,
      imagePath: 'lib/images/king_transparent.png',
    );
    Newboard[7][4] = ChessPieces(
      type: ChessPieceType.king,
      isWhite: true,
      imagePath: 'lib/images/king_transparent.png',
    );
Enter fullscreen mode Exit fullscreen mode

And for the pieces to be shown, we must return the board to Newboard:

//game_board.dart
//..Chesspieces initial position
 board = Newboard;
//..End of void function
}
Enter fullscreen mode Exit fullscreen mode

As we are done with the pieces, we can head back to the GridView.builder and pass the requirements that we added to the constructor earlier, as well as the row and column of the squares.

//game_board.dart
//..the rest of your code
 @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: backgroundClr,//bg color from color_values.dart
body:GridView.builder(
              itemCount: 8 * 8,
              physics: const NeverScrollableScrollPhysics(),
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 8,
              ),
              itemBuilder: (context, index) {
                //gets row and col position from square
                int row = index ~/ 8;
                int col = index % 8;


                return SquaresGb(
                  isWhite: isWhite(index),
                  pieces: board[row][col],
                );
              },
            ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

It would likely look more like this:

Board with complete Pieces

Now that we have placed our chess pieces, let's try to select them!
First, we’ll have to go back to our square_gb.dart and add a new boolean called isSelected in the class and its constructor:

//square_gb.dart
class SquaresGb extends StatelessWidget {
  final bool isWhite;
  final ChessPieces? pieces;
  final bool isSelected;


  const SquaresGb({
    super.key,
    required this.isWhite,
    required this.pieces,
    required this.isSelected,
    });
Enter fullscreen mode Exit fullscreen mode

Then let’s make the square change its color depending on whether it has been selected or not.

//square_gb.dart
//..constructor
@override
  Widget build(BuildContext context) {


    Color? squareColor;
    //if selected square is amber
    if(isSelected){
        squareColor = Colors.amberAccent;
    }
    //if not, square is green or light green
    else{
      squareColor = isWhite ? foregroundClr : backgroundClr;
    }
  }
Enter fullscreen mode Exit fullscreen mode

After that, we head back to the game_board.dart again to create a variable for the selected piece.

//game_board.dart
class _GameBoardState extends State<GameBoard> {
  // 2D list representing the ChessBoard
  late List<List<ChessPieces?>> board;

  //current piece being selected on board
  //if no piece then it is null
  ChessPieces? selectedPiece;
}


//..the rest of your code
Enter fullscreen mode Exit fullscreen mode

Then let's also add the column index of the selected piece:

//game_board.dart
class _GameBoardState extends State<GameBoard> {
  // 2D list representing the ChessBoard
  late List<List<ChessPieces?>> board;

  //current piece being selected on board
  //if no piece then it is null
  ChessPieces? selectedPiece;
  //row index of selected piece
  // default value (-1) inidicates no pieces have been selected
  int selectedRow = -1;


  //col index of selected piece
  // default value (-1) inidicates no pieces have been selected
  int selectedCol = -1;
}
Enter fullscreen mode Exit fullscreen mode

After that, create a void function that initializes when a user selects a piece.

//game_board.dart
 void piecesSelected(int row, int col) {
    setState(() {
      //No piece has been selected yet, this is the first selection
     if (board[row][col]!= null) {
          selectedPiece = board[row][col];
          selectedRow = row;
          selectedCol = col;
        }
    });}
Enter fullscreen mode Exit fullscreen mode

Then head down to create a checker if the square has been selected or not and return it:

//game_board.dart
 //..GridView.Builder
//checker if seleceted or not
        bool isSelected = selectedRow == row && selectedCol == col;

return SquaresGb(
                  isWhite: isWhite(index),
                  pieces: board[row][col],
                  isSelected: isSelected,
            );
Enter fullscreen mode Exit fullscreen mode

Select chess pieces

We have implemented the logic for selecting but there is something wrong... We can't touch it! In order to fix that we must head to the squares_gb.dart to add a GestureDetector to detect our taps.

//squares_gb.dart
return GestureDetector(
    onTap: onTap,
      child: Container(
        color: squareColor,
        child: pieces != null ?
        Image.asset(
        pieces!.imagePath,
        color: pieces!.isWhite ? Colors.white : const Color.fromARGB(255, 105, 1, 1),
        ) : null,
      )
)
Enter fullscreen mode Exit fullscreen mode

But in order to continue, the Gesture detector states that it needs to initalize a void function declared and initialized at the constructor in order to work functionally.

GestureDetector requirements

So let’s add that

//squares_gb.dart
class SquaresGb extends StatelessWidget {
  final bool isWhite;
  final ChessPieces? pieces;
  final bool isSelected;
  final void Function()? onTap;


  const SquaresGb({
    super.key,
    required this.isWhite,
    required this.pieces,
    required this.isSelected,
    required this.onTap,
    });
}
Enter fullscreen mode Exit fullscreen mode
//squares_gb.dart
return GestureDetector(
      onTap:  onTap,//GestureDetector now works
      child: Container(
        color: squareColor,
        child: pieces != null ?
        Image.asset(
        pieces!.imagePath,
        color: pieces!.isWhite ? Colors.white : const Color.fromARGB(255, 105, 1, 1),
        ) : null,

      ),
    );
Enter fullscreen mode Exit fullscreen mode

Once added we head back to the game_board.dart to pass the newly required onTap on the SquaresGb.

//game_board.dart
return SquaresGb(
                  isWhite: isWhite(index),
                  pieces: board[row][col],
                  isSelected: isSelected,
                  onTap: () => piecesSelected(row, col),
                );
Enter fullscreen mode Exit fullscreen mode

Now restart your emulator and try to select one of the pieces. It would most likely look like this:

Selection of pieces demo

Raw Valid Moves

To know what valid moves each piece has, we implement the following:

At game_board.dart, we add this:

//game_board.dart
  //A list of valid moves  for the currently selected pieces
  //each move is represented as a list with 2 elements: row and col
  List<List<int>> validMoves = [];
Enter fullscreen mode Exit fullscreen mode
//game_board.dart
// if a  piece is selected, calculate it’s valid moves
validMoves = calculateRawValidMoves(selectedRow, selectedCol, selectedPiece);
Enter fullscreen mode Exit fullscreen mode

(but there is a difference between raw and real valid moves which we will get to later!)

and then, we make a List called candidateMoves.

//game_board.dart
 //Calculate Raw Valid Moves
  List<List<int>> calculatedRawValidMoves(
    int row,
    int col,
    ChessPieces? pieces,
  ) {
    List<List<int>> candidateMoves = [];


//different directions based on their color
int direction = pieces!.isWhite ? -1 : 1; //up or down moves
Enter fullscreen mode Exit fullscreen mode

After that, we should make another helper method in our helper_methods.dart file since this method would be used a lot and it is to check if a specific row and column is in the board.

//helper_methods.dart
bool isInBoard(int row, int col){
  return row >= 0 && row < 8 && col >= 0 && col < 8;
}
Enter fullscreen mode Exit fullscreen mode

Declaration of Individual moves

Now, we make different cases based on different pieces. The process is basically similar for all pieces with all unique moves added to the List<List<int>> candidateMoves.

For Pawns:

//game_board.dart
switch (pieces.type) {
      case ChessPieceType.pawn:
        // Move forward one square if not blocked
        if (isInBoard(row + direction, col) &&
            board[row + direction][col] == null) {
          candidateMoves.add([row + direction, col]);
        }
        // Move forward two squares from starting position if both squares are empty
        if ((row == 1 && !pieces.isWhite) || (row == 6 && pieces.isWhite)) {
          if (isInBoard(row + 2 * direction, col) &&
              board[row + 2 * direction][col] == null &&
              board[row + direction][col] == null) {
            candidateMoves.add([row + 2 * direction, col]);
          }
        }
        // Capture diagonally to the left if an enemy piece is there
        if (isInBoard(row + direction, col - 1) &&
            board[row + direction][col - 1] != null &&
            board[row + direction][col - 1]!.isWhite != pieces.isWhite) {
          candidateMoves.add([row + direction, col - 1]);
        }
// Capture diagonally to the right if an enemy piece is there
        if (isInBoard(row + direction, col + 1) &&
            board[row + direction][col + 1] != null &&
            board[row + direction][col + 1]!.isWhite != pieces.isWhite) {
          candidateMoves.add([row + direction, col + 1]);
        }
        break;
Enter fullscreen mode Exit fullscreen mode

now, we add this line at the end so it returns all the moves that was added in candidateMoves.

//game_board.dart
return candidateMoves; 
Enter fullscreen mode Exit fullscreen mode

but to return it, we go back in the squares_gb.dart , we add bool isValidmove to indicate if it is a valid move or not.

//squares_gb.dart
 class SquaresGb extends StatelessWidget {
  final bool isWhite;
  final ChessPieces? pieces;
  final bool isSelected;
  final bool isValidmove;
Enter fullscreen mode Exit fullscreen mode

we also add this requirement to the constructor

//squares_gb.dart
required this.isValidmove,
Enter fullscreen mode Exit fullscreen mode

then, to have a separate color for when its a valid move, we add an else if under the squareColor if statements:

//squares_gb.dart
    else if(isValidmove){
      squareColor = Colors.blueAccent;
    }
Enter fullscreen mode Exit fullscreen mode

then we go back to the game_board and add the following line for the scaffold:

//game_board.dart
                //CHECK if valid move
          bool isValidmove = false;
                for (var position in validMoves) {
                  //compare row and columns
                  if (position[0] == row && position[1] == col) {
                    isValidmove = true;
                  }
                }
Enter fullscreen mode Exit fullscreen mode

Now we do one for Rooks:

//game_board.dart
 case ChessPieceType.rook:
       // Rooks move vertically and horizontally until blocked by an ally, an enemy, or the board edge
        var directions = [
          [-1, 0], //up
          [1, 0], //down
          [0, -1], //left
          [0, 1], //right
        ];
        for (var direction in directions) {
          var i = 1;
          while (true) {
    // Calculate next square in current direction
            var newRow = row + i * direction[0];
            var newCol = col + i * direction[1];
    // Stop if out of board bounds   
          if (!isInBoard(newRow, newCol)) {
              break;
            }
            if (board[newRow][newCol] != null) {
// If enemy piece, add as valid capture move then stop 
        if (board[newRow][newCol]!.isWhite != pieces.isWhite) {
                candidateMoves.add([newRow, newCol]); //kill as rook
              }
    // Stop regardless — blocked by ally or after capture
              break; 
            }
// Square is empty, rook can move here, continue in same direction
           candidateMoves.add([newRow, newCol]);
            i++;
          }
        }
        Break;
Enter fullscreen mode Exit fullscreen mode

Now for Knights:

//game_board.dart
 case ChessPieceType.knight:
        // Knights move in an L-shape: 2 squares in one direction, 1 in the other
          var knightMoves = [
          [-2, -1], //up 2 left 1
          [-2, 1], //up 2 right 1
          [-1, -2], //up 1 left 2
          [-1, 2], //up 1 right 2
          [1, -2], //down 1 left 2
          [1, 2], //down 1 right 2
          [2, -1], //down 2 left 1
          [2, 1], //down 2 right 1
        ];
        for (var move in knightMoves) {
          var newRow = row + move[0];
          var newCol = col + move[1];
    // Skip if move lands outside the board
          if (!isInBoard(newRow, newCol)) {
            continue;
          }
          if (board[newRow][newCol] != null) {
       // If enemy piece, add as valid capture then skip to next move
            if (board[newRow][newCol]!.isWhite != pieces.isWhite) {
              candidateMoves.add([newRow, newCol]); 
            }
 // Skip if blocked by ally
            continue; 
          }
          candidateMoves.add([newRow, newCol]);
        }
        break;
Enter fullscreen mode Exit fullscreen mode

for Bishops:

//game_board.dart
 case ChessPieceType.bishop:
        // Bishops move diagonally in 4 directions until blocked by an ally, an enemy, or the board edge


        var directions = [
          [-1, -1], //up left
          [-1, 1], //up right
          [1, -1], // down left
          [1, 1], // down right 
        ];


        for (var direction in directions) {
          var i = 1;
          while (true) {
    // Calculate next square in current diagonal direction 
            var newRow = row + i * direction[0];
            var newCol = col + i * direction[1];
    // Stop if out of board bounds
            if (!isInBoard(newRow, newCol)) {
              break;
            }
            if (board[newRow][newCol] != null) {
    // If enemy piece, add as valid capture then stop 
              if (board[newRow][newCol]!.isWhite != pieces.isWhite) {
                candidateMoves.add([newRow, newCol]); 
              }
// Stop regardless — blocked by ally or after capture
              break;
            }
// Square is empty, bishop can move here, continue diagonally
            candidateMoves.add([newRow, newCol]);
            i++;
          }
        }
        break;
Enter fullscreen mode Exit fullscreen mode

For Queens:

//game_board.dart
  case ChessPieceType.queen:
        // Queen combines rook + bishop: slides in all 8 directions until blocked by an ally, an enemy, or the board edge         
var directions = [
          [-1, 0], //up
          [1, 0], // down
          [0, -1], // left
          [0, 1], //right
          [-1, -1], // up left
          [-1, 1], //up right
          [1, -1], //down left
          [1, 1], //down right
        ];
        for (var direction in directions) {
          var i = 1;
          while (true) {
    // Calculate next square in current direction
            var newRow = row + i * direction[0];
            var newCol = col + i * direction[1];
// Stop if out of board bounds
            if (!isInBoard(newRow, newCol)) {
              break;
            }


            if (board[newRow][newCol] != null) {


// If enemy piece, add as valid capture then stop
              if (board[newRow][newCol]!.isWhite != pieces.isWhite) {
                candidateMoves.add([newRow, newCol]);
              }
// Stop regardless — blocked by ally or after capture
            break;  
           }


// Square is empty, queen can move here, continue in same direction
            candidateMoves.add([newRow, newCol]);
            i++;
          }
        }
        break;
Enter fullscreen mode Exit fullscreen mode

And lastly for the Kings:

//game_board.dart
case ChessPieceType.king:
        //King moves in all 8 directions but only one square at a time
        var directions = [
          [-1, 0], //up
          [1, 0], // down
          [0, -1], // left
          [0, 1], //right
          [-1, -1], // up left
          [-1, 1], //up right
          [1, -1], //down left
          [1, 1], //down right
        ];
        for (var direction in directions) {
// Calculate the single square in current direction
          var newRow = row + direction[0];
          var newCol = col + direction[1];


    // Skip if out of board bounds
          if (!isInBoard(newRow, newCol)) {
            continue;
          }
          if (board[newRow][newCol] != null) {
      // If enemy piece, add as valid capture then skip to next direction
            if (board[newRow][newCol]!.isWhite != pieces.isWhite) {
              candidateMoves.add([newRow, newCol]); //Kill as King
            }
    // Skip if blocked by ally
            continue; 
          }
// Square is empty, king can move here
          candidateMoves.add([newRow, newCol]);
        }
        break;
    }
// Return all valid candidate moves for the piece   
    return candidateMoves;
  }
Enter fullscreen mode Exit fullscreen mode

if you want to test it fast, you can add the following line of code in the board initialization to put a piece in the middle and see if it moves correctly.

//game_board.dart
newBoard[3][3] = chessPiece(
  type: ChessPieceType.queen,
  isWhite: true,
  imagePath: 'lib/images/queen.png',
);
Enter fullscreen mode Exit fullscreen mode

Moves Demo Queen

Moving of pieces

Now that you have made the possible moves lets actually set the pieces in motion! In order
to move the pieces create a void function movePieces within the game_board.dart to get started and add the following:

//game_board.dart
//MOVE PIECES
  void movePiece(int newRow, int newCol) {
    //move the piece and clear the old spot
    board[newRow][newCol] = selectedPiece;
    board[selectedRow][selectedCol] = null;

    //clear selection
    setState(() {
      selectedPiece = null;
      selectedRow = -1;
      selectedCol = -1;
      validMoves = [];
    });
Enter fullscreen mode Exit fullscreen mode

and we would call it when we select a piece so inside the pieceSelected, we add the following:

//game_board.dart
      //if there is a piece selected and user taps on valid square that is a valid move, then move there
      else if (selectedPiece != null &&
          validMoves.any((element) => element[0] == row && element[1] == col)) {
        movePieces(row, col);
      }
Enter fullscreen mode Exit fullscreen mode

An optional minor visual change is to change the margins of the valid moves’ square color. If you want to do that, you can put the following code in the squares_gb.dart file and within the GestureDetector: (feel free to tweak it to your own tastes)

//squares_gb.dart file
        margin: EdgeInsets.all(isValidmove ?  6 : 0),
Enter fullscreen mode Exit fullscreen mode

Meanwhile, in the pieceSelected, we would need to tweak it since if we try to capture an opponents piece, it will just assume that we are selecting it instead. so to fix that, we would need to input:

//game_board.dart
// If no piece is currently selected AND the tapped cell has a piece on it
if (selectedPiece == null && board[row][col] != null) {
  // Only allow selection if the piece belongs to the current player
  if (board[row][col]!.isWhite == isWhiteTurn) {
   // Select the piece and store its position
    selectedPiece = board[row][col];
    selectedRow = row;
    selectedCol = col;
  }
}
// If a piece is already selected AND the newly tapped cell also has a piece
// that belongs to the same player (i.e. not an enemy piece)
else if (board[row][col] != null &&
    board[row][col]!.isWhite == selectedPiece!.isWhite) {
  // Switch the selection to the newly tapped friendly piece
  selectedPiece = board[row][col];
  selectedRow = row;
  selectedCol = col;
}
Enter fullscreen mode Exit fullscreen mode

To implement checks and checkmates:

We first initialize the kings position before the code to initialize the board

//game_board.dart
  //initial position of kings
  List<int> whiteKingPosition = [7, 4];
  List<int> blackKingPosition = [0, 4];
  bool checkStatus = false;
Enter fullscreen mode Exit fullscreen mode

next, we make an isKinginCheck method to determine if the current player’s king is in check

//game_board.dart
 // Determine if the given player's king is currently in check
bool isKingInCheck(bool isWhiteKing) {
  // Get the current position of the king we're checking
  List<int> kingPosition = isWhiteKing
      ? whiteKingPosition
      : blackKingPosition;
}
Enter fullscreen mode Exit fullscreen mode

then, we apply so that every time we move a piece, we first check the checkmate before the check and we will also add an action TextButton to start over the game when someone has been checkmated. So this snippet of code would go to the last part of movePieces:

//game_board.dart
// Determine game state after the move: evaluate checkmate before check
// since checkmate takes priority over simply being in check
bool mated = isCheckmate(!isWhiteTurn);
bool checked = isKingInCheck(!isWhiteTurn);



if (mated) {
  // Checkmate — suppress the check indicator and prompt the end game dialog
  checkStatus = false;
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('CHECK MATE!'),
      actions: [
        TextButton(onPressed: resetGame, child: const Text('Play Again?'))
      ],
    ),
  );
} else if (checked) {
  // The king is in check but still has valid moves — flag it for the UI
  checkStatus = true;
} else {
  // No check or checkmate — clear any existing check indicator
  checkStatus = false;
}


// Hand control to the other player
isWhiteTurn = !isWhiteTurn;
Enter fullscreen mode Exit fullscreen mode

then directly afterwards,

//game_board.dart
//Check if enemy pieces can attack the king
      for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
          //skip empty square and pieces of  the same color of king
          if (board[i][j] == null || board[i][j]!.isWhite == iswhiteKng) {
            continue;
          }
          List<List<int>> pieceValidmoves = calculatedRealValidMoves(
            i,
            j,
            board[i][j],false
          );
          //check if the king's position is in this piece's valid moves
          if (pieceValidmoves.any(
            (move) => move[0] == kingPosition[0] && move[1] == kingPosition[1],
          )) {
            return true;
          }
        }
      }
      return false;
    }
Enter fullscreen mode Exit fullscreen mode

Add this in game_board.dart as an indicator that a king is being checked

//game_board.dart
          //GAME STATUS
          Text(checkStatus ? "CHECK!" : ""),
Enter fullscreen mode Exit fullscreen mode

Since we have called a function earlier named resetGame let's build the method now

//game_board.dart
//Reset to new Game
void resetGame(){

  Navigator.pop(context);
  _initalizeBoard();
  checkStatus = false;
  WhitePiecesKilled.clear();
  BlackPiecesKilled.clear();
  whiteKingPosition = [7, 4];
  blackKingPosition = [0, 4];
  setState(() {});
  isWhiteTurn = true;
}
Enter fullscreen mode Exit fullscreen mode

Now we can reset the game if someone is being checkmated!

Checkmate reset button

Real Valid Moves

Up until now, we were only counting the raw valid moves but not all raw valid moves are real valid moves such as raw valid moves counting the ability to put your king in check when in reality, that is not allowed.

To count the real valid moves we would need to make another method to calculate and simulate the real valid moves of the pieces

//game_board.dart
 // Calculate REAL Valid Moves (filters out moves that would leave your king in check)
List<List<int>> calculatedRealValidMoves(
  int row,
  int col,
  ChessPieces? pieces,
  bool checkSimulation,
) {
  List<List<int>> realValidMoves = [];


  // First, get all technically possible moves without any check-safety filtering
  List<List<int>> candidateMoves = calculatedRawValidMoves(row, col, pieces);


  // If check simulation is enabled, we need to verify each move is safe
  if (checkSimulation) {
    for (var move in candidateMoves) {
      int endRow = move[0];
      int endCol = move[1];


      // Simulate the move on a copy of the board and check if our king
      // would still be in check after it — only add the move if it's safe
      if (safeSimulatedMove(pieces!, row, col, endRow, endCol)) {
        realValidMoves.add(move);
      }
    }
  } else {
    // Skip check simulation (used internally to avoid infinite recursion
    // when safeSimulatedMove itself calls this function)
    realValidMoves = candidateMoves;
    }
  return realValidMoves;
}

Enter fullscreen mode Exit fullscreen mode

And then we add this to temporarily apply a move on the board to determine whether it would leave the current player's king in check.

//game_board.dart
// Simulate a future move to check if it would leave our king in check
bool safeSimulatedMove(ChessPieces pieces, int startRow, int startCol, int endRow, int endCol) {

  // Save the piece currently at the destination so we can restore it later
  ChessPieces? originalDestinationPiece = board[endRow][endCol];


  // If the moving piece is the king, we need to track its new position
  // since isKingInCheck uses the stored king position to find it on the board
  List<int>? originalKingPosition;
  if (pieces.type == ChessPieceType.king) {
    originalKingPosition = pieces.isWhite ? whiteKingPosition : blackKingPosition;


    // Temporarily update the king's tracked position to the destination
    if (pieces.isWhite) {
      whiteKingPosition = [endRow, endCol];
    } else {
      blackKingPosition = [endRow, endCol];
    }
  }


  // Apply the move on the board
  board[endRow][endCol] = pieces;
  board[startRow][startCol] = null;


  // Check if our own king is under attack in this simulated board state
  bool kingInCheck = isKingInCheck(pieces.isWhite);


  // Undo the move — restore the board to its original state
  board[startRow][startCol] = pieces;
  board[endRow][endCol] = originalDestinationPiece;


  // If the king moved, restore its tracked position as well
  if (pieces.type == ChessPieceType.king) {
    if (pieces.isWhite) {
      whiteKingPosition = originalKingPosition!;
    } else {
      blackKingPosition = originalKingPosition!;
    }
  }


  // The move is safe only if the king is NOT in check after it
  return !kingInCheck;
}
Enter fullscreen mode Exit fullscreen mode

Display of Killed Pieces

Now that we can move around and kill pieces, let’s display the killed pieces on opposite sides of the board. Let's head to the game_board and create a list of the pieces that have been taken by either side!

//game_board.dart
 // _GameBoardState


// Tracks white pieces that have been captured by the black player
List<ChessPieces> whitePiecesKilled = [];


// Tracks black pieces that have been captured by the white player
List<ChessPieces> blackPiecesKilled = [];
Enter fullscreen mode Exit fullscreen mode
//game_board.dart
// void movePiece()
// If the destination square is occupied by an enemy piece, capture it
if (board[newRow][newCol] != null) {
  // Identify the piece being captured
  var killedPiece = board[newRow][newCol];


  // Add it to the appropriate captured pieces list based on its color
  if (killedPiece!.isWhite) {
    whitePiecesKilled.add(killedPiece); // white piece captured by black
  } else {
    blackPiecesKilled.add(killedPiece); // black piece captured by white
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we go down to the GridView.builder and wrap it with a column widget

//game_board.dart
 //..the rest of the code
       Column(
              children:[ GridView.builder(
                itemCount: 8 * 8,
                physics: const NeverScrollableScrollPhysics(),
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 8,
                ),
                itemBuilder: (context, index) {
                  //gets row and col position from square
                  int row = index ~/ 8;
                  int col = index % 8;

                  //checker if selected or not
             bool isSelected = selectedRow == row && selectedCol == col;

                  //CHECK if valid move
                  bool isValidmove = false;
                  for (var position in validMoves) {
                    //compare row and columns
                    if (position[0] == row && position[1] == col) {
                      isValidmove = true;
                    }
                  }
                  return SquaresGb(
                    isWhite: isWhite(index),
                    pieces: board[row][col],
                    isSelected: isSelected,
                    onTap: () => piecesSelected(row, col),
                    isValidmove: isValidmove,
                  );
                },
              ),
              ]
            ),
Enter fullscreen mode Exit fullscreen mode

Now we need to create a space where we can display the killed pieces we can accomplish this with the Expanded widget

The white ones at the top

//game_board.dart
body: Column(
  children: [
    // Display all white pieces that have been captured by the black player
    Expanded(
      child: GridView.builder(
        itemCount: whitePiecesKilled.length, // number of captured white pieces
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 7, // display up to 7 pieces per row
        ),
        itemBuilder: (context, index) => DeadPieces()
      ),
    ),
)
Enter fullscreen mode Exit fullscreen mode

The black ones at the bottom

//game_board.dart
//Black pieces taken
        Expanded(
  child: GridView.builder(
    itemCount: blackPiecesKilled.length, // number of captured black pieces
    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 7, // display up to 7 pieces per row
    ),
    itemBuilder: (context, index) => DeadPieces()
  ),
),
Enter fullscreen mode Exit fullscreen mode

Now we create another file in our components folder named “dead_pieces.dart”. The only things we need here are the image path and the boolean .isWhite to indicate if it is a white piece or not.

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




class DeadPieces extends StatelessWidget {
  final String imagePath;
  final bool isWhite;
  const DeadPieces({super.key,
  required this.imagePath,
  required this.isWhite});


  @override
  Widget build(BuildContext context) {
    return Image.asset(imagePath, color: isWhite ? Colors.grey : const Color.fromARGB(255, 109, 57, 57),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Now lets apply the DeadPieces again

White top:

//dead_pieces.dart
Expanded(
            child: GridView.builder(
              itemCount: WhitePiecesKilled.length,
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 7,
              ),
              itemBuilder: (context, index) => DeadPieces(
                imagePath: WhitePiecesKilled[index].imagePath,
                isWhite: true,//Added requirements
              ),
            ),
          ),
Enter fullscreen mode Exit fullscreen mode

Black bottom:

//dead_pieces.dart
 //Black pieces taken
          Expanded(
            child: GridView.builder(
              itemCount: BlackPiecesKilled.length,
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 7,
              ),
              itemBuilder: (context, index) => DeadPieces(
                imagePath: BlackPiecesKilled[index].imagePath,
                isWhite: false,//Added requirements
              ),
            ),
          ),
Enter fullscreen mode Exit fullscreen mode

Now that we have displayed the pieces that have been killed, let’s move on and create the turning based-system!

Here is what it looks like:

Demo of killed pieces

Turn-Based System

Now that we have displayed the pieces that have been killed, let’s move on and create the turning based-system!

First, we must declare on our game_board.dart a boolean term that indicates whose turn it is.

//game_board.dart
//a boolean to indicate turns
  bool isWhiteTurn = true;
//..the rest of your code
Enter fullscreen mode Exit fullscreen mode

Then we go back to our void pieceSelected to update the logic, we added a feature that checks the piece that belongs to the current player!

//game_board.dart
void piecesSelected(int row, int col) {
    setState(() {
      //No piece has been selected yet, this is the first selection
      if (selectedPiece == null && board[row][col] != null) {
        if (board[row][col]!.isWhite == isWhiteTurn) {
          selectedPiece = board[row][col];
          selectedRow = row;
          selectedCol = col;
        }
      }
Enter fullscreen mode Exit fullscreen mode

Then we add this at the bottom of the void function in order to flip turns

//game_board.dart
//Change turns
    isWhiteTurn = !isWhiteTurn;
Enter fullscreen mode Exit fullscreen mode

We have successfully created the turn-based system!.

Our Chess game is officialy finished try it yourself and with friends!

Conclusion:

Congratulations!! You just made a chess game using Flutter. Turns out it wasn’t so simple after all! Despite it being ancient 2D chess, you actually just made a big leap towards game development. You just learnt how game logic and mobile development can come together. Starting with the Flutter setup, where we gradually built the board, pieces, and logic, you can see how a full application comes from small, manageable parts.

When you understood and followed along this article, you now know how a 2D grid can be coded and how Object Oriented Programming works since we used classes, objects, and constructors. The selection of pieces and movements also highlights how to use conditionals and state updates to control what is shown on the screen.

You can even improve upon your logic building by adding modern chess moves that weren’t originally in our design or maybe new mechanics to explore!.

Now that you know how to make a game on Flutter, maybe you can try making checkers! Who knows, maybe this article was read by a future successful game developer like you? So don’t let your sparks of curiosity fizzle out, keep on coding, and have fun learning!

References

Top comments (0)