Ludo Game with Flutter

Apoorv Maheshwari
Blockchain Research Lab
7 min readMay 10, 2021

Have you ever thought of gaining gaming experience with Flutter? Have you ever thought about building an app similar to Ludo King with modern features? Have you ever thought about how the Ludo app generates revenues? Here we go to build an interesting game with Flutter!

What is a good way to kill the time in the digital era? Of course, it’s “Playing Games”. During the 90s, we played board games and what was the favorite one at that time. Without a doubt, it was Ludo. In the 21st century, Mobile apps have replaced the physical boards and people of all ages now play it on their smartphone.

It’s hard to digest but true, that, Ludo King app net worth is around $285 million in 2020 and has achieved significant rank in the gaming industry.

Here I am, to assist you in developing a basic Ludo Game with Flutter which will run on both iOS and Android without any UI glitches. The base is free to use and is released under MIT license. Source code is here but read along to understand how and what it does.

LUDO Game - with Flutter
Ludo Game with Flutter

CAPABILITIES OF THE GAME

  1. Tokens can come out of Home Area only when dice has 6.
  2. True Dice
  3. Cut/Token Kill logic implemented.
  4. User can tap on a token to make it move. Only valid moves are made.
  5. Safe area “Star” implemented.
  6. Safe area coloured path near-final destination implemented.
  7. Tokens follow the only eligible path.
  8. Animated reversal of cut/killed tokens.
  9. Works on both iOS and Android

LIMITATIONS OF THE GAME

  1. User turn logic cannot be implemented.
  2. No server-side implementations.
  3. Cut/Token Kill logic may not be perfect.
  4. Complex cut logics missing.

This is just a basic implementation which will make you realise how easy it is to make a Ludo app using flutter.

Here are the basic game logics which you need to understand while compiling the application.

BOARD LOGIC

Ludo board is a very simple board. It is a 15×15 (minimum usable cell) board wherein only some predefined cells are used. For example, if we talk about Green tokens, they can be accessible at fixed number of cells which are addressable as:

                     //GREEN home positions               Token(TokenType.green, Position(2, 2), TokenState.initial, 0),               Token(TokenType.green, Position(2, 3), TokenState.initial, 1),               Token(TokenType.green, Position(3, 2), TokenState.initial, 2),               Token(TokenType.green, Position(3, 3), TokenState.initial, 3),
//All possible cells to which green can move tostatic const greenPath = [[6,1],[6,2],[6,3],[6,4],[6,5],[5,6],[4,6],[3,6],[2,6],[1,6],[0,6],[0,7],[0,8],[1,8],[2,8],[3,8],[4,8],[5,8],[6,9],[6,10],[6,11],[6,12],[6,13],[6,14],[7,14],[8,14],[8,13],[8,12],[8,11],[8,10],[8,9],[9,8],[10,8],[11,8],[12,8],[13,8],[14,8],[14,7],[14,6],[13,6],[12,6],[11,6],[10,6],[9,6],[8,5],[8,4],[8,3],[8,2],[8,1],[8,0],[7,0],[7,1],[7,2],[7,3],[7,4],[7,5],[7,6]];

There is no way a Green token can end up in any other cell in a valid move. Its home position is fixed and its path is fixed. Similarly goes for RED, YELLOW, BLUE.

SETTING UP THE BOARD

In the assets section, you will find an image saved named as “Ludo_board.png”. What we have to do is, setting up the image in correct position and aligning 15 rows and columns along with it. We have to mark the borders as well with Grey colour for clear indication of the edges of the board. Here is the required code:

class Board extends StatelessWidget {
List<List<GlobalKey>> keyRefrences;
Board(this.keyRefrences);
List<Container> _getRows() {
List<Container> rows = [];
for (var i = 0; i < 15; i++) {
rows.add(
Container(
child: LudoRow(i,keyRefrences[i]),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey),
bottom:
i == 14 ? BorderSide(color: Colors.grey) : BorderSide.none,
),
color: Colors.transparent,
),
),
);
}
return rows;
}

@override
Widget build(BuildContext context) {
return Center(
child: Card(
elevation: 10,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/Ludo_board.png"),
fit: BoxFit.fill,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[..._getRows()],
),
),
),
);
}
}

SETTING UP THE DICE

Dice plays the most important role in Ludo. Having an efficient and real dice is what are objective is. Our dice will be the combination of six images which will be representing 6 number in the dice i.e. 1,2,3,4,5 and 6. On every random rotation these numbers should appear on a non-biased bases. Here is how we will achieve our requirements.

class Dice extends StatelessWidget {

void updateDices(DiceModel dice) {

@override
Widget build(BuildContext context) {
List<String> _diceOneImages = [
"assets/1.png",
"assets/2.png",
"assets/3.png",
"assets/4.png",
"assets/5.png",
"assets/6.png",
];
final dice = Provider.of<DiceModel>(context);
final c = dice.diceOneCount;
var img = Image.asset(
_diceOneImages[c - 1],
gaplessPlayback: true,
fit: BoxFit.fill,
);
return Card(
elevation: 10,
child: Container(
height: 40,
width: 40,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () => updateDices(dice),
child: img,
),
),
],
),
],
),
),
);
}
}

For correcting the biasness of the device, we have to build a dice model with the utilization of Random() so that we can roll on the dice without any predictability. Here is how we can ply our condition.

class DiceModel with ChangeNotifier {
int diceOne = 1;

int get diceOneCount => diceOne;

void generateDiceOne() {
diceOne = Random().nextInt(6) + 1;
print("diceOne: $diceOne");
notifyListeners();
}
}

SETTING UP THE TOKENS

We basically have four colours on our board namely green, yellow, blue and red. Along with the Token’s house position, the token itself should be of the same colour. To maintain the colour combination of the Tokens in accordance with their houses, we will implement the following code:

class Tokenp extends StatelessWidget {
final Token token;
final List<double> dimentions;
Function(Token)callBack;
Tokenp(this.token, this.dimentions);
Color _getcolor() {
switch (this.token.type) {
case TokenType.green:
return Colors.green;
case TokenType.yellow:
return Colors.yellow[900];
case TokenType.blue:
return Colors.blue[600];
case TokenType.red:
return Colors.red;
}
return Colors.red;
}

@override
Widget build(BuildContext context) {
final gameState = Provider.of<GameState>(context);
final dice = Provider.of<DiceModel>(context);
return AnimatedPositioned(
duration: Duration(milliseconds: 100),
left: dimentions[0],
top: dimentions[1],
width: dimentions[2],
height: dimentions[3],
child: GestureDetector(
onTap: (){
gameState.moveToken(token, dice.diceOne);
},
child: Card(
elevation: 5,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getcolor(),
boxShadow: [
BoxShadow(
color: _getcolor(),
blurRadius: 5.0, // soften the shadow
spreadRadius: 1.0, //extend the shadow
)
]),
),
),
),
);
}
}

There will be five Token states which will be active throughout the game. These are:

  1. initial: their starting position
  2. home: their house position
  3. normal: their active game position
  4. safe: the safe position which will be only one within a house
  5. safeinpair: in this state, the tokens cannot be killed if any two or more tokens are on the same position.
LUDO Board in its initial state
Complete setup of LUDO

VALID MOVE LOGIC

  • A move is valid if it ends within its path array.
  • The token is not at the final destination.
  • If the token is at home then it can only accept 6 as a valid move.

CUT/KILL LOGIC

  • Any token which ends up in a preoccupied cell cuts/kills any different colour token already present.
  • Two tokens if on the same cell create mutual safety.
  • Star cells are always provide safety.

GAME PLAY

As mentioned above, turn logic has not been implemented, neither is any AI present but you can use this app as a starting point. In order to play the game, you need to first run it using a simulator or device.

  • Tap on dice at the bottom.
  • Tap on the token.
  • Token moves if the move is valid.
  • Cut will happen when applicable.
  • A cut token will traverse all the way back to home occupying the first available slot.

Here is a gist of the logic implemented in code:

moveToken(Token token, int steps) {
Position destination;
int pathPosition;
if (token.tokenState == TokenState.home) return;
if (token.tokenState == TokenState.initial && steps != 6) return;
if (token.tokenState == TokenState.initial && steps == 6) {
destination = _getPosition(token.type, 0);
pathPosition = 0;
_updateInitalPositions(token);
_updateBoardState(token, destination, pathPosition);
this.gameTokens[token.id].tokenPosition = destination;
this.gameTokens[token.id].positionInPath = pathPosition;
notifyListeners();
} else if (token.tokenState != TokenState.initial) {
int step = token.positionInPath + steps;
if (step > 56) return;
destination = _getPosition(token.type, step);
pathPosition = step;
var cutToken = _updateBoardState(token, destination, pathPosition);
int duration = 0;

for (int i = 1; i <= steps; i++) {
duration = duration + 500;
var future = new Future.delayed(Duration(milliseconds: duration), () {
int stepLoc = token.positionInPath + 1;
this.gameTokens[token.id].tokenPosition =
_getPosition(token.type, stepLoc);
this.gameTokens[token.id].positionInPath = stepLoc;
token.positionInPath = stepLoc;
notifyListeners();
});
}

When the Token is cut/killed, here is how it will return to its house.

_cutToken(Token token) {
switch (token.type) {
case TokenType.green: //for green token
{
this.gameTokens[token.id].tokenState = TokenState.initial;
this.gameTokens[token.id].tokenPosition = this.greenInitital.first;
this.greenInitital.removeAt(0);
}
break;
case TokenType.yellow: //for yellow token
{
this.gameTokens[token.id].tokenState = TokenState.initial;
this.gameTokens[token.id].tokenPosition = this.yellowInitital.first;
this.yellowInitital.removeAt(0);
}
break;
case TokenType.blue: //for blue token
{
this.gameTokens[token.id].tokenState = TokenState.initial;
this.gameTokens[token.id].tokenPosition = this.blueInitital.first;
this.blueInitital.removeAt(0);
}
break;
case TokenType.red: //for red token
{
this.gameTokens[token.id].tokenState = TokenState.initial;
this.gameTokens[token.id].tokenPosition = this.redInitital.first;
this.redInitital.removeAt(0);
}
break;
}
}
The Playing Mode
The real GamePlay

What Else?

Did you enjoy the gaming experience in Flutter? If you have any suggestions, bugs, questions, feature requests or anything please feel free to contact me!

Want to improve your game and learn more? How about adding a Server with Firebase and implementing AI with moves predictions? How about putting ads in? How about setting up a Main Menu with multiple modes and gameplays?

There is lot to improve, of course — this is just a basic example of a game. But it should have given a basic idea of the core concepts of game development with Flutter.

Here is the link for Repository. Refer to it for more details and do no forget to follow and give a.

--

--

Apoorv Maheshwari
Blockchain Research Lab

Blockchain | Flutter Developer | Artificial Intelligence | Freelance Content Writer | Contributor at GeeksforGeeks