CodeHub
HomeUploadContact
Back to Hub

⁠Ludo Game

March 23, 2026 Watch Tutorial

Wiring Schematic

 ⁠Ludo Game wiring diagram

Hardware Required

  • Adruino Board
    Buy on Amazon

Source Code

Arduino / C++
1#include <MCUFRIEND_kbv.h>
2#include <TouchScreen.h>
3#include <Adafruit_GFX.h>
4
5MCUFRIEND_kbv tft;
6
7// ===== PINS & CALIBRATION (MEGA) =====
8#define XP 6
9#define YP A1
10#define XM A2
11#define YM 7
12#define TS_MINX 248
13#define TS_MAXX 920
14#define TS_MINY 251
15#define TS_MAXY 878
16
17#define MINPRESSURE 10
18#define MAXPRESSURE 1000
19
20TouchScreen ts(XP, YP, XM, YM, 300);
21#define BUZZER 49
22
23// ===== COLORS =====
24#define BLACK   0x0000
25#define WHITE   0xFFFF
26#define RED     0xF800
27#define BLUE    0x001F 
28#define GREEN   0x07E0 
29#define YELLOW  0xFFE0 
30#define SIDEBAR_BG 0x2124 
31
32// ===== GAME CONFIG =====
33int CW = 16; 
34int turn = 0; 
35int totalPlayers = 4;
36int diceVal = 1;
37
38int pPos[4][4]; 
39bool hasWon[4];
40int winOrder[4];
41int winnersCount = 0;
42
43enum State { MENU, WAIT_ROLL, SELECT_PIECE, ANIM, GAME_OVER };
44State currentState = MENU;
45
46void setup() {
47  pinMode(BUZZER, OUTPUT);
48  uint16_t id = tft.readID();
49  if (id == 0xD3D3) id = 0x9486;
50  tft.begin(id);
51  tft.setRotation(1); 
52  
53  randomSeed(analogRead(A8) + analogRead(A9) + micros());
54  
55  // STRONG SOUND TEST (Standard Tone)
56  // If you don't hear this, your buzzer is broken or wiring is loose
57  tone(BUZZER, 1000); delay(100); noTone(BUZZER);
58  delay(50);
59  tone(BUZZER, 2000); delay(100); noTone(BUZZER);
60  
61  drawMenu();
62}
63
64void loop() {
65  int x, y;
66  
67  if (touch(&x, &y)) {
68    if (currentState == MENU) {
69      if (x > 60 && x < 260) {
70        if (y > 80 && y < 120) startGame(2);       
71        else if (y > 130 && y < 170) startGame(3); 
72        else if (y > 180 && y < 220) startGame(4); 
73      }
74    }
75    else if (currentState == WAIT_ROLL) {
76      if (x > 240 && y > 100 && y < 200) rollRealDice();
77    }
78    else if (currentState == SELECT_PIECE) {
79      int pIdx = getTouchedPiece(x, y);
80      if (pIdx != -1) movePiece(turn, pIdx, diceVal);
81    }
82    else if (currentState == GAME_OVER) {
83      drawMenu();
84    }
85    delay(120);
86  }
87}
88
89// ================= SAFE AUDIO WRAPPER =================
90// Uses standard tone() but forces it OFF immediately to save power
91void playSafeTone(int freq, int duration) {
92  tone(BUZZER, freq);
93  delay(duration); // Play for this long
94  noTone(BUZZER);  // CUT POWER IMMEDIATELY
95}
96
97// ================= DICE ENGINE =================
98void rollRealDice() {
99  int dx = 280; int dy = 150; int size = 50; 
100  
101  // Animation
102  for(int i=0; i<12; i++) {
103    int r = random(1, 7);
104    drawRealDice(dx, dy, size, r, WHITE, BLACK);
105    
106    // Play VERY SHORT standard tone (10ms)
107    // Short enough to not flicker, Long enough to hear
108    playSafeTone(500 + (i*100), 10);
109    
110    delay(30 + (i*5)); 
111  }
112  
113  // Result
114  diceVal = random(1, 7);
115  drawRealDice(dx, dy, size, diceVal, YELLOW, BLACK); 
116  
117  // Success Beep
118  playSafeTone(2000, 100);
119  
120  int moves = countMovablePieces(turn, diceVal);
121  if (moves == 0) {
122    delay(800);
123    nextTurn();
124  } else if (moves == 1) {
125    int idx = getFirstMovable(turn, diceVal);
126    delay(500);
127    movePiece(turn, idx, diceVal);
128  } else {
129    currentState = SELECT_PIECE;
130    updateStatus("SELECT");
131  }
132}
133
134void movePiece(int p, int idx, int steps) {
135  currentState = ANIM;
136  updateStatus("MOVING");
137  
138  // Leave Base
139  if (pPos[p][idx] == -1) {
140    if (steps == 6) {
141      erasePiece(p, idx);
142      pPos[p][idx] = 0;
143      drawPiece(p, idx);
144      
145      playSafeTone(1500, 100); 
146      
147      currentState = WAIT_ROLL;
148      updateSidebar();
149      return;
150    }
151  }
152  
153  // Move
154  for(int i=0; i<steps; i++) {
155    erasePiece(p, idx); 
156    pPos[p][idx]++;
157    drawPiece(p, idx);  
158    
159    // Optional: Tiny tick (Uncomment if you want walking sound)
160    // playSafeTone(1000, 5);
161    
162    delay(100); 
163    if(pPos[p][idx]==57) break;
164  }
165  
166  checkKill(p, idx);
167  
168  if (checkPlayerWin(p)) {
169    handleWin(p);
170    return; 
171  }
172  
173  if (steps == 6) {
174    currentState = WAIT_ROLL;
175    updateStatus("ROLL 6!");
176  } else {
177    nextTurn();
178  }
179}
180
181void checkKill(int p, int idx) {
182  int myPos = pPos[p][idx];
183  if (myPos > 51) return; 
184  if (myPos==0||myPos==8||myPos==13||myPos==21||myPos==26||myPos==34||myPos==39||myPos==47) return; 
185  
186  int myGrid = getGlobalStep(p, myPos);
187  
188  for(int op=0; op<totalPlayers; op++) {
189    if(op == p) continue;
190    for(int oi=0; oi<4; oi++) {
191      if(pPos[op][oi] != -1 && pPos[op][oi] <= 51) {
192        int opGrid = getGlobalStep(op, pPos[op][oi]);
193        if(myGrid == opGrid) {
194          // Kill Sound
195          playSafeTone(150, 100); 
196          delay(50); 
197          playSafeTone(100, 150);
198          
199          erasePiece(op, oi);
200          pPos[op][oi] = -1;
201          drawPiece(op, oi);
202        }
203      }
204    }
205  }
206}
207
208// ================= GAME LOGIC =================
209void startGame(int p) {
210  totalPlayers = p;
211  turn = 0;
212  winnersCount = 0;
213  for(int i=0; i<4; i++) {
214    hasWon[i] = false;
215    winOrder[i] = -1;
216    for(int j=0; j<4; j++) pPos[i][j] = -1;
217  }
218  tft.fillScreen(BLACK);
219  drawBoard();
220  drawSidebar();
221  updateSidebar();
222  drawAllPieces();
223  currentState = WAIT_ROLL;
224}
225
226bool checkPlayerWin(int p) {
227  int count = 0;
228  for(int i=0; i<4; i++) if (pPos[p][i] == 57) count++;
229  return (count == 4);
230}
231
232void handleWin(int p) {
233  hasWon[p] = true;
234  winOrder[winnersCount] = p;
235  winnersCount++;
236  
237  playSafeTone(1000, 100); delay(50);
238  playSafeTone(2000, 100); delay(50);
239  playSafeTone(3000, 300); 
240  
241  if (totalPlayers == 2) {
242    drawGameOverScreen();
243    return;
244  }
245  
246  if (winnersCount == totalPlayers - 1) {
247    for(int i=0; i<totalPlayers; i++) if(!hasWon[i]) winOrder[winnersCount] = i; 
248    drawGameOverScreen();
249  } else {
250    tft.fillRect(60, 100, 200, 40, BLACK);
251    tft.setCursor(70, 110);
252    tft.setTextColor(WHITE); tft.setTextSize(2);
253    tft.print("P"); tft.print(p+1); tft.print(" FINISHED!");
254    delay(2000);
255    drawBoard(); 
256    drawAllPieces();
257    nextTurn(); 
258  }
259}
260
261void nextTurn() {
262  int loopGuard = 0;
263  do {
264    turn++;
265    if (turn >= totalPlayers) turn = 0;
266    loopGuard++;
267  } while (hasWon[turn] && loopGuard < 10);
268  currentState = WAIT_ROLL;
269  updateSidebar();
270}
271
272// ================= GRAPHICS & HELPERS =================
273void drawSidebar() {
274  tft.fillRect(240, 0, 80, 240, SIDEBAR_BG);
275  tft.drawFastVLine(240, 0, 240, WHITE);
276  tft.setCursor(253, 110);
277  tft.setTextColor(WHITE); tft.setTextSize(2); tft.print("TAP");
278}
279
280void updateSidebar() {
281  tft.fillRect(245, 10, 70, 80, SIDEBAR_BG);
282  tft.setCursor(250, 20); tft.setTextColor(WHITE); tft.setTextSize(2); tft.print("TURN");
283  uint16_t c = (turn==0)?RED : (turn==1)?BLUE : (turn==2)?YELLOW : GREEN;
284  tft.fillCircle(280, 60, 20, c);
285  tft.drawCircle(280, 60, 20, WHITE);
286  drawRealDice(280, 150, 50, diceVal, WHITE, BLACK);
287  updateStatus("WAITING");
288}
289
290void updateStatus(char* msg) {
291  tft.fillRect(242, 210, 76, 30, SIDEBAR_BG);
292  tft.setCursor(245, 215); tft.setTextColor(WHITE); tft.setTextSize(1); tft.print(msg);
293}
294
295void drawRealDice(int cx, int cy, int s, int n, uint16_t bgCol, uint16_t dotCol) {
296  tft.fillRoundRect(cx - s/2, cy - s/2, s, s, 8, bgCol); 
297  tft.drawRoundRect(cx - s/2, cy - s/2, s, s, 8, BLACK);
298  int r = s/10; int d = s/4;  
299  if (n==1 || n==3 || n==5) tft.fillCircle(cx, cy, r, dotCol);
300  if (n!=1) { tft.fillCircle(cx - d, cy - d, r, dotCol); tft.fillCircle(cx + d, cy + d, r, dotCol); }
301  if (n>=4) { tft.fillCircle(cx + d, cy - d, r, dotCol); tft.fillCircle(cx - d, cy + d, r, dotCol); }
302  if (n==6) { tft.fillCircle(cx - d, cy, r, dotCol); tft.fillCircle(cx + d, cy, r, dotCol); }
303}
304
305void drawBoard() {
306  tft.fillRect(0, 0, 240, 240, WHITE);
307  drawBaseSquare(0, 0, RED); drawBaseSquare(144, 0, BLUE);
308  drawBaseSquare(0, 144, GREEN); drawBaseSquare(144, 144, YELLOW);
309  
310  tft.fillTriangle(120,120, 96,96, 144,96, BLUE);    
311  tft.fillTriangle(120,120, 144,96, 144,144, YELLOW);
312  tft.fillTriangle(120,120, 144,144, 96,144, GREEN); 
313  tft.fillTriangle(120,120, 96,144, 96,96, RED);     
314  
315  for(int y=0; y<15; y++) {
316    for(int x=0; x<15; x++) {
317      if ((x<6 && y<6) || (x>8 && y<6) || (x<6 && y>8) || (x>8 && y>8)) continue;
318      if (x>=6 && x<=8 && y>=6 && y<=8) continue;
319      tft.drawRect(x*CW, y*CW, CW, CW, BLACK);
320      if(x>=1 && x<=5 && y==7) fillCell(x,y,RED);     
321      if(x==7 && y>=1 && y<=5) fillCell(x,y,BLUE);    
322      if(x>=9 && x<=13 && y==7) fillCell(x,y,YELLOW); 
323      if(x==7 && y>=9 && y<=13) fillCell(x,y,GREEN);  
324    }
325  }
326  fillCell(1, 6, RED); fillCell(8, 1, BLUE); fillCell(13, 8, YELLOW); fillCell(6, 13, GREEN);
327  uint16_t SAFE = 0xBDF7;
328  fillCell(2,6,SAFE); fillCell(6,2,SAFE); fillCell(8,12,SAFE); fillCell(12,8,SAFE);
329}
330
331void drawBaseSquare(int x, int y, uint16_t c) {
332  tft.fillRect(x, y, 96, 96, c);
333  tft.fillRect(x+15, y+15, 66, 66, WHITE);
334}
335
336void fillCell(int x, int y, uint16_t c) {
337  tft.fillRect(x*CW+1, y*CW+1, CW-2, CW-2, c);
338}
339
340void drawPiece(int p, int idx) {
341  int pos = pPos[p][idx];
342  int px, py;
343  uint16_t c = (p==0)?RED : (p==1)?BLUE : (p==2)?YELLOW : GREEN;
344  if (pos == -1) {
345    int bx = (p==1||p==2)?144:0; int by = (p==2||p==3)?144:0;
346    int ox = (idx%2)*30 + 33; int oy = (idx/2)*30 + 33;
347    px=bx+ox; py=by+oy;
348  } else {
349    int gx, gy;
350    getGridCoord(p, pos, &gx, &gy);
351    px=gx*CW+8; py=gy*CW+8;
352  }
353  tft.fillCircle(px, py, 6, c);
354  tft.drawCircle(px, py, 6, BLACK);
355}
356
357void erasePiece(int p, int idx) {
358  int pos = pPos[p][idx];
359  if (pos == -1) {
360    int bx = (p==1||p==2)?144:0; int by = (p==2||p==3)?144:0;
361    tft.fillRect(bx+15, by+15, 66, 66, WHITE);
362    for(int i=0; i<4; i++) if(i!=idx && pPos[p][i]==-1) drawPiece(p, i);
363  } else {
364    int gx, gy;
365    getGridCoord(p, pos, &gx, &gy);
366    uint16_t bg = WHITE;
367    if(gx>=1 && gx<=5 && gy==7) bg=RED;      
368    if(gx==7 && gy>=1 && gy<=5) bg=BLUE;     
369    if(gx>=9 && gx<=13 && gy==7) bg=YELLOW;  
370    if(gx==7 && gy>=9 && gy<=13) bg=GREEN;   
371    if(gx==1 && gy==6) bg=RED; if(gx==8 && gy==1) bg=BLUE;
372    if(gx==13 && gy==8) bg=YELLOW; if(gx==6 && gy==13) bg=GREEN;
373    if((gx==2&&gy==6)||(gx==6&&gy==2)||(gx==8&&gy==12)||(gx==12&&gy==8)) bg=0xBDF7;
374    tft.fillRect(gx*CW+1, gy*CW+1, CW-2, CW-2, bg);
375    
376    int myGlobal = -1;
377    if (pos <= 51) myGlobal = getGlobalStep(p, pos);
378
379    for(int pp=0; pp<totalPlayers; pp++) {
380      for(int ii=0; ii<4; ii++) {
381        if(pp==p && ii==idx) continue;
382        int otherPos = pPos[pp][ii];
383        if (otherPos == -1) continue; 
384        bool match = false;
385        if (pos > 51 && otherPos > 51 && pp == p && otherPos == pos) match = true;
386        else if (pos <= 51 && otherPos <= 51) {
387           int otherGlobal = getGlobalStep(pp, otherPos);
388           if (myGlobal == otherGlobal) match = true;
389        }
390        if (match) drawPiece(pp, ii);
391      }
392    }
393  }
394}
395
396void drawAllPieces() {
397  for(int p=0; p<totalPlayers; p++) for(int i=0; i<4; i++) drawPiece(p, i);
398}
399
400void getGridCoord(int p, int s, int *gx, int *gy) {
401  if(s > 51) {
402    int w = s - 52; 
403    if(p==0) { *gx=1+w; *gy=7; } 
404    else if(p==1) { *gx=7; *gy=1+w; } 
405    else if(p==2) { *gx=13-w; *gy=7; } 
406    else if(p==3) { *gx=7; *gy=13-w; } 
407    return;
408  }
409  int g = getGlobalStep(p, s);
410  if(g<=4) { *gx=1+g; *gy=6; }
411  else if(g==5) { *gx=6; *gy=5; }
412  else if(g<=10) { *gx=6; *gy=5-(g-5); }
413  else if(g==11) { *gx=7; *gy=0; }
414  else if(g==12) { *gx=8; *gy=0; }
415  else if(g<=17) { *gx=8; *gy=1+(g-13); }
416  else if(g==18) { *gx=9; *gy=6; }
417  else if(g<=23) { *gx=10+(g-19); *gy=6; }
418  else if(g==24) { *gx=14; *gy=7; }
419  else if(g==25) { *gx=14; *gy=8; }
420  else if(g<=30) { *gx=13-(g-26); *gy=8; }
421  else if(g==31) { *gx=8; *gy=9; }
422  else if(g<=36) { *gx=8; *gy=10+(g-32); }
423  else if(g==37) { *gx=7; *gy=14; }
424  else if(g==38) { *gx=6; *gy=14; }
425  else if(g<=43) { *gx=6; *gy=13-(g-39); }
426  else if(g==44) { *gx=5; *gy=8; }
427  else if(g<=49) { *gx=4-(g-45); *gy=8; }
428  else if(g==50) { *gx=0; *gy=7; }
429  else if(g==51) { *gx=0; *gy=6; } 
430}
431
432int countMovablePieces(int p, int d) {
433  int c=0;
434  for(int i=0; i<4; i++) {
435    int pos = pPos[p][i];
436    if((pos==-1 && d==6) || (pos!=-1 && pos+d<=57)) c++;
437  }
438  return c;
439}
440
441int getFirstMovable(int p, int d) {
442  for(int i=0; i<4; i++) {
443    int pos = pPos[p][i];
444    if((pos==-1 && d==6) || (pos!=-1 && pos+d<=57)) return i;
445  }
446  return 0;
447}
448
449int getTouchedPiece(int x, int y) {
450  for(int i=0; i<4; i++) {
451    int pos = pPos[turn][i];
452    int px, py;
453    if(pos==-1) {
454       int bx = (turn==1||turn==2)?144:0; int by = (turn==2||turn==3)?144:0;
455       int ox = (i%2)*30 + 33; int oy = (i/2)*30 + 33;
456       px=bx+ox; py=by+oy;
457    } else {
458       int gx, gy;
459       getGridCoord(turn, pos, &gx, &gy);
460       px=gx*CW+8; py=gy*CW+8;
461    }
462    if(abs(x-px) < 12 && abs(y-py) < 12) return i;
463  }
464  return -1;
465}
466
467int getGlobalStep(int p, int s) {
468  int off = p * 13;
469  return (s + off) % 52;
470}
471
472void drawMenu() {
473  tft.fillScreen(BLACK);
474  
475  tft.setTextSize(4);
476  tft.setCursor(65, 30);
477  tft.setTextColor(YELLOW);
478  tft.print("LUDO PRO");
479  
480  // BIG BUTTONS
481  tft.fillRect(60, 80, 200, 40, BLUE);
482  tft.drawRect(60, 80, 200, 40, WHITE);
483  tft.setCursor(105, 92);
484  tft.setTextColor(WHITE);
485  tft.setTextSize(2);
486  tft.print("2 PLAYERS");
487
488  tft.fillRect(60, 130, 200, 40, GREEN);
489  tft.drawRect(60, 130, 200, 40, WHITE);
490  tft.setCursor(105, 142);
491  tft.setTextColor(BLACK); 
492  tft.print("3 PLAYERS");
493
494  tft.fillRect(60, 180, 200, 40, RED);
495  tft.drawRect(60, 180, 200, 40, WHITE);
496  tft.setCursor(105, 192);
497  tft.setTextColor(WHITE);
498  tft.print("4 PLAYERS");
499}
500
501void drawGameOverScreen() {
502  currentState = GAME_OVER;
503  tft.fillScreen(BLACK);
504  tft.setTextSize(3); tft.setCursor(40, 30); tft.setTextColor(YELLOW); tft.print("GAME OVER");
505  tft.setTextSize(2);
506  int y = 80;
507  for(int i=0; i<totalPlayers; i++) {
508    int pID = winOrder[i];
509    if (pID == -1) continue; 
510    uint16_t c = (pID==0)?RED : (pID==1)?BLUE : (pID==2)?YELLOW : GREEN;
511    tft.setCursor(40, y); tft.setTextColor(WHITE);
512    if (i==0) tft.print("1st: "); else if (i==1) tft.print("2nd: ");
513    else if (i==2) tft.print("3rd: "); else tft.print("4th: ");
514    tft.setTextColor(c);
515    if(pID==0) tft.print("RED"); else if(pID==1) tft.print("BLUE");
516    else if(pID==2) tft.print("YELLOW"); else tft.print("GREEN");
517    y += 40;
518  }
519  tft.setCursor(40, 210); tft.setTextColor(WHITE); tft.setTextSize(1); tft.print("TAP TO RESTART");
520}
521
522bool touch(int *x, int *y) {
523  TSPoint p = ts.getPoint();
524  pinMode(XM, OUTPUT); pinMode(YP, OUTPUT);
525  if (p.z > MINPRESSURE && p.z < MAXPRESSURE) {
526    int rawX = map(p.x, TS_MINX, TS_MAXX, 320, 0);
527    int rawY = map(p.y, TS_MINY, TS_MAXY, 0, 240);
528    if(rawX < 0) rawX = 0; if(rawX > 320) rawX = 320;
529    if(rawY < 0) rawY = 0; if(rawY > 240) rawY = 240;
530    *x = rawX; *y = rawY;
531    return true;
532  }
533  return false;
534}
Breadboard
Buy on Amazon
  • TFT Display
    Buy on Amazon
  • Active Buzzer
    Buy on Amazon