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:
- In case you don't have Flutter installed yet, click here.
- For the Android emulator, if you haven't installed it yet, click here.
- 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.
Once clicked and a folder has been created, name your project as you please, but follow Flutter’s project naming rule:
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
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
You can stop, restart and hot reload the emulator with the control panel
Set up main.dart
Next, head to the lib folder and click on the main.dart file
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(
),
);
}
}
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();
}
}
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()),
));
}
}
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())),
));
}
}
But even after removing the excess squares, it would still be scrollable so we would need to put physics: const NeverScrollableScrollPhysics(), as shown above
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],
);
}
}
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;
}
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
Now drag the folder into your lib folder along with the dart files that belong on your code.
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
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
Then run on the terminal
#Terminal
flutter pub get
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.
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,});
}
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);
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
}
}
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
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),
);
}
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',
);
}
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',
);
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',
);
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',
);
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',
);
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',
);
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
}
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],
);
},
),
);
}
It would likely look more like this:
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,
});
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;
}
}
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
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;
}
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;
}
});}
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,
);
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,
)
)
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.
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,
});
}
//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,
),
);
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),
);
Now restart your emulator and try to select one of the pieces. It would most likely look like this:
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 = [];
//game_board.dart
// if a piece is selected, calculate it’s valid moves
validMoves = calculateRawValidMoves(selectedRow, selectedCol, selectedPiece);
(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
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;
}
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;
now, we add this line at the end so it returns all the moves that was added in candidateMoves.
//game_board.dart
return candidateMoves;
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;
we also add this requirement to the constructor
//squares_gb.dart
required this.isValidmove,
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;
}
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;
}
}
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;
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;
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;
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;
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;
}
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',
);
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 = [];
});
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);
}
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),
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;
}
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;
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;
}
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;
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;
}
Add this in game_board.dart as an indicator that a king is being checked
//game_board.dart
//GAME STATUS
Text(checkStatus ? "CHECK!" : ""),
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;
}
Now we can reset the game if someone is being checkmated!
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;
}
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;
}
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 = [];
//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
}
}
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,
);
},
),
]
),
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()
),
),
)
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()
),
),
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),
);
}
}
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
),
),
),
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
),
),
),
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:
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
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;
}
}
Then we add this at the bottom of the void function in order to flip turns
//game_board.dart
//Change turns
isWhiteTurn = !isWhiteTurn;
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!

















Top comments (0)