Creating a player bot to automate Mankala
Hello everyone! Once again, I am here to share my fun experiences incorporating new features for Mankala.
Problem
This time I have built a bot server for Mankala where you can automate the entire game process of playing the game moves by using the server endpoints present in the bot. This Mankala bot can be run in various ways, and for the easiest to understand, can be an example in Python, which I have used in our process.
Approach
To build this bot, I built a minimal HTTP server using Qt’s own QTcpServer. The BotApiServer class listens on port 8080 and handles raw HTTP requests by parsing the method and path manually, and when these requests are synced from the terminal, the changes can be observed in the GUI, showing a change in the number of shells.
The API exposes two endpoints:
GET /api/status— Returns the full game state as JSON: whose turn it is, the seeds in every pit, each player’s store, whether the game is over, and who won.POST /api/move— Accepts a JSON body with player (“player1” or “player2”) and pit_index (0-indexed), validates the move, executes it, and returns the updated state.
A list of documentation with all the endpoints present has also been added as part of the Help section present in the game, which also contains predefined scripts to run 1 bot or even 2 bots simultaneously, automating the entire game.
OpenAPI documentation:
openapi: 3.1.0
info:
title: Mankala Bot API
description: API to programmatically control and play Mankala.
version: 1.0.0
paths:
/api/status:
get:
summary: Get the current game status
description: Retrieves the full state of the board, scores, and turn information.
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
required: [in_progress, game_over, winner, turn, variant, pit_count, player1_pits, player2_pits, player1_store, player2_store, board]
properties:
in_progress: { type: boolean }
game_over: { type: boolean }
winner: { type: string, enum: ["", "player1", "player2"] }
turn: { type: string, enum: ["player1", "player2"] }
variant: { type: string, enum: ["Bohnenspiel", "Oware", "Pallanguli"] }
pit_count: { type: integer, minimum: 1 }
player1_pits: { type: array, items: { type: integer, minimum: 0 } }
player2_pits: { type: array, items: { type: integer, minimum: 0 } }
player1_store: { type: integer, minimum: 0 }
player2_store: { type: integer, minimum: 0 }
board: { type: array, items: { type: integer, minimum: 0 } }
/api/move:
post:
summary: Make a move
description: Executes a move for the specified player at the given pit index.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [player, pit_index]
properties:
player: { type: string, enum: ["player1", "player2"] }
pit_index: { type: integer, minimum: 0 }
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
required: [status, turn, game_over]
properties:
status: { type: string, enum: ["ok"] }
turn: { type: string, enum: ["player1", "player2"] }
game_over: { type: boolean }
winner: { type: string, enum: ["", "player1", "player2"] }
"400":
description: Bad Request (Invalid Move, Not Your Turn, Invalid JSON)
content:
application/json:
schema:
type: object
properties:
error: { type: string }
Here we can observe a Python script for a player vs bot game:
import urllib.request, json, time
API = "http://localhost:8080/api"
def status():
return json.loads(urllib.request.urlopen(f"{API}/status").read())
def move(player, pit):
req = urllib.request.Request(f"{API}/move",
data=json.dumps({"player": player, "pit_index": pit}).encode(), method="POST")
req.add_header("Content-Type", "application/json")
return json.loads(urllib.request.urlopen(req).read())
# Bot plays as player2, waits for human player1
while True:
st = status()
if st["game_over"] or not st["in_progress"]:
print(f"Game Over! Winner: {st['winner']}")
break
if st["turn"] != "player2":
time.sleep(0.5) # wait for human
continue
pits = st["player2_pits"]
# pick first non-empty pit
pit = next((i for i, v in enumerate(pits) if v > 0), -1)
if pit == -1:
break
print(f"Bot moves pit {pit}")
move("player2", pit)
time.sleep(1.5) # animation delay
For the above code, we can adjust the localhost port and the animation speed based on our needs and save it.
Challenges I faced:
I would say the synchronization of the GUI with the server was hard. So to make the GUI animations correctly incorporated, I made the apiMoveMade signal using which the change in game state was listened in GameWindowLandscape.qml and GameWindowPortrait.qml files.
This was something really new that I have done till now and was a fun experience for me.
Thanks for reading 🚀....