Build Your Bot
Building your own bot is the most straightforward way to prove your superiority at this game. Not trash talking. Not gaslighting your opponent into thinking they miscounted trumps. Just cold, verifiable results.
We provide ready-to-go templates for Python, TypeScript, C#, Go, and Java. If you can write a function in any of these, you can build a bot. No need to know how HTTP works, how JSON is parsed, or how game engines communicate. Pick a template, duplicate the folder, edit one file. That’s the whole thing.
Prerequisites
Section titled “Prerequisites”- .NET 10, required to run
giretra-manage, the CLI tool you’ll use to test your bot - Your language’s runtime: Node 20+ for TypeScript, Python 3 for Python, Go 1.23+ for Go, Java 17+ for Java
That’s it. No databases, no message queues, no container orchestration.
Step 1: Clone the repository
Section titled “Step 1: Clone the repository”git clone https://github.com/giretra/giretra.gitcd giretraEverything happens inside this repo: templates, testing tools, and the bot folder where your bot will live.
Step 2: Copy a template
Section titled “Step 2: Copy a template”Copy one of the template folders in external-bots/ and rename it:
cp -r external-bots/random-python-bot external-bots/my-botcp -r external-bots/random-node-bot external-bots/my-botcp -r external-bots/random-dotnet-bot external-bots/my-botcp -r external-bots/random-go-bot external-bots/my-botcp -r external-bots/random-java-bot external-bots/my-botEach template folder contains:
| File | Purpose | Edit? |
|---|---|---|
bot.meta.json | Bot identity and launch configuration | Yes |
Bot file (bot.py, bot.ts, Bot.cs, bot.go, Bot.java) | Your game logic | Yes |
| Server file | HTTP boilerplate | No |
| Type definitions | Data structures for requests and responses | No |
Step 3: Configure bot.meta.json
Section titled “Step 3: Configure bot.meta.json”Open bot.meta.json and update the identity fields:
{ "name": "my-bot", "displayName": "My Awesome Bot", "pun": "I never bluff... except when I do", "author": "Your Name", "authorGithub": "your-github-username"}Field reference:
| Field | Description |
|---|---|
name | Internal identifier. Must be unique, lowercase, no spaces. This is also your folder name. |
displayName | What shows up in leaderboards and match results. |
pun | A one-liner that shows up next to your bot’s name. Optional but encouraged. |
author | Your name. |
authorGithub | Your GitHub username. |
init | How to install dependencies or compile. Runs once before your bot starts. |
launch | How to start your bot’s server. The engine sets a PORT environment variable. |
Leave init and launch alone unless you need to change how your bot builds or starts. The templates have these set up correctly.
Step 4: Implement your bot logic
Section titled “Step 4: Implement your bot logic”Your bot makes exactly 3 types of decisions:
- Choose a cut position. Before each deal, someone cuts the deck. Pick a position between 6 and 26.
- Choose a negotiation action. The bidding phase. The engine gives you a list of valid actions (announce a mode, accept, double, redouble). Pick one.
- Choose a card to play. The heart of your strategy. The engine gives you a list of valid cards. Pick one.
Here’s a complete bot in each language. This is the only file you edit:
bot.py
import randomfrom bot_types import *
class Bot: def __init__(self, match_id: str): self.match_id = match_id
def choose_cut(self, ctx: ChooseCutContext) -> CutResult: position = random.randint(6, 26) return CutResult(position=position, from_top=True)
def choose_negotiation_action( self, ctx: ChooseNegotiationActionContext ) -> NegotiationActionChoice: # ctx.valid_actions has everything you're allowed to do return ctx.valid_actions[0]
def choose_card(self, ctx: ChooseCardContext) -> Card: # ctx.valid_plays has every legal card you can play return random.choice(ctx.valid_plays)bot.ts
import type { ChooseCutContext, ChooseNegotiationActionContext, ChooseCardContext, CutResult, NegotiationActionChoice, Card} from './types';
export class Bot { constructor(public readonly matchId: string) {}
chooseCut(ctx: ChooseCutContext): CutResult { const position = Math.floor(Math.random() * 21) + 6; return { position, fromTop: true }; }
chooseNegotiationAction(ctx: ChooseNegotiationActionContext): NegotiationActionChoice { // ctx.validActions has everything you're allowed to do return ctx.validActions[0]; }
chooseCard(ctx: ChooseCardContext): Card { // ctx.validPlays has every legal card you can play return ctx.validPlays[Math.floor(Math.random() * ctx.validPlays.length)]; }}Bot.cs
namespace RandomDotnetBot;
public class Bot{ private readonly Random Rng = new(); public string MatchId { get; }
public Bot(string matchId) => MatchId = matchId;
public CutResult ChooseCut(ChooseCutContext ctx) { return new CutResult { Position = Rng.Next(6, 27), FromTop = true }; }
public NegotiationActionChoice ChooseNegotiationAction( ChooseNegotiationActionContext ctx) { // ctx.ValidActions has everything you're allowed to do return ctx.ValidActions[0]; }
public Card ChooseCard(ChooseCardContext ctx) { // ctx.ValidPlays has every legal card you can play return ctx.ValidPlays[Rng.Next(ctx.ValidPlays.Count)]; }}bot.go
package main
import "math/rand/v2"
type Bot struct { MatchID string}
func NewBot(matchID string) *Bot { return &Bot{MatchID: matchID}}
func (b *Bot) ChooseCut(ctx ChooseCutContext) CutResult { return CutResult{Position: rand.IntN(21) + 6, FromTop: true}}
func (b *Bot) ChooseNegotiationAction( ctx ChooseNegotiationActionContext,) NegotiationActionChoice { // ctx.ValidActions has everything you're allowed to do return ctx.ValidActions[0]}
func (b *Bot) ChooseCard(ctx ChooseCardContext) Card { // ctx.ValidPlays has every legal card you can play return ctx.ValidPlays[rand.IntN(len(ctx.ValidPlays))]}Bot.java
package randomjavabot;
import java.util.concurrent.ThreadLocalRandom;import randomjavabot.BotTypes.*;
public class Bot { private final String matchId;
public Bot(String matchId) { this.matchId = matchId; }
public CutResult chooseCut(ChooseCutContext ctx) { int position = ThreadLocalRandom.current().nextInt(6, 27); return new CutResult(position, true); }
public NegotiationActionChoice chooseNegotiationAction( ChooseNegotiationActionContext ctx) { // ctx.validActions() has everything you're allowed to do return ctx.validActions().get(0); }
public Card chooseCard(ChooseCardContext ctx) { // ctx.validPlays() has every legal card you can play var plays = ctx.validPlays(); return plays.get(ThreadLocalRandom.current().nextInt(plays.size())); }}That’s it. Three methods. The engine tells you what’s legal through validActions and validPlays, so you can’t accidentally make an illegal move. You just pick from the menu. Start dumb, iterate.
The request contexts also include your hand, the current trick, scores, and negotiation history, everything you need to build a real strategy.
Step 5: Test your bot
Section titled “Step 5: Test your bot”All commands below assume you’re at the root of your Giretra clone. Use ./giretra-manage.sh on Linux/macOS or giretra-manage.cmd on Windows.
Validate
Section titled “Validate”The first command you should run. It plays your bot through 100 matches against Kialasoa and checks everything works:
./giretra-manage.sh validate my-botYou get a full report with:
- Rule violations and crash counts (your target: zero)
- Response times per decision type (min, avg, P50, P95, P99, max)
- Game mode coverage (whether your bot was tested across all six game modes)
- Performance trend (response time stability across the run)
You can tweak the run with flags like -n 500 for more matches, -o Razavavy for a tougher opponent, -d for a determinism check, or -v for verbose violation details.
Benchmark
Section titled “Benchmark”Once your bot validates clean, pit it against the built-in opponents:
./giretra-manage.sh benchmark my-bot Kialasoa./giretra-manage.sh benchmark my-bot Razavavy./giretra-manage.sh benchmark my-bot Eva| Bot | Difficulty | Strategy |
|---|---|---|
Kialasoa | Easy | Picks randomly. If you can’t beat it, something’s wrong. |
Razavavy | Medium | Tracks cards and reads partner signals. |
Eva | Hard | Card counting, void inference, positional play. The current boss. |
The benchmark runs 1000 matches by default and gives you win rates with 95% confidence intervals, ELO ratings, and statistical significance. Use -n to change the match count.
Swiss tournament
Section titled “Swiss tournament”See where your bot ranks against all available bots:
./giretra-manage.sh swissThis discovers all bots (built-in and external) and runs a full Swiss-system tournament with a final leaderboard, win/loss records, and ELO ratings. You can also pass specific bot names to limit the participants.
Try it in the browser
Section titled “Try it in the browser”You can run the full play.giretra.com web app locally to play against your bot in real game conditions. First, install the frontend dependencies:
cd src/Giretra.Web/ClientApp/giretra-webnpm installThen start the app from the repository root with the --offline flag:
dotnet run --project src/Giretra.Web -- --offlineThis launches the ASP.NET backend and the Angular frontend with mocked authentication, so you don’t need any external services. Open http://localhost:4200 in your browser to access the app.
Pay attention to the console output. If your external bot fails to load (missing dependencies, broken init script, port conflict), the error will show up in stdout. This is the fastest way to catch startup issues before they become mysterious test failures.
Observation events (optional)
Section titled “Observation events (optional)”If you want your bot to track what other players are doing, subscribe to notifications by adding event types to bot.meta.json:
"notifications": ["card-played", "trick-completed", "deal-ended"]Then implement the corresponding handler methods:
def on_card_played(self, ctx: CardPlayedContext) -> None: # Track which cards have been played pass
def on_trick_completed(self, ctx: TrickCompletedContext) -> None: # Track tricks won by each team pass
def on_deal_ended(self, ctx: DealEndedContext) -> None: # Review deal results passonCardPlayed(ctx: CardPlayedContext): void { // Track which cards have been played}
onTrickCompleted(ctx: TrickCompletedContext): void { // Track tricks won by each team}
onDealEnded(ctx: DealEndedContext): void { // Review deal results}public virtual void OnCardPlayed(CardPlayedContext ctx){ // Track which cards have been played}
public virtual void OnTrickCompleted(TrickCompletedContext ctx){ // Track tricks won by each team}
public virtual void OnDealEnded(DealEndedContext ctx){ // Review deal results}func (b *Bot) OnCardPlayed(ctx CardPlayedContext) { // Track which cards have been played}
func (b *Bot) OnTrickCompleted(ctx TrickCompletedContext) { // Track tricks won by each team}
func (b *Bot) OnDealEnded(ctx DealEndedContext) { // Review deal results}public void onCardPlayed(CardPlayedContext ctx) { // Track which cards have been played}
public void onTrickCompleted(TrickCompletedContext ctx) { // Track tricks won by each team}
public void onDealEnded(DealEndedContext ctx) { // Review deal results}Available events: deal-started, card-played, trick-completed, deal-ended, match-ended.
These are great for building memory: tracking which cards have been played, detecting voids, counting points. But they’re completely optional. A stateless bot works just fine as a starting point.
One rule: no calling external resources during play. No API calls to a cloud AI, no phoning home to a remote server, no downloading strategy files mid-game.
Your bot runs locally, makes decisions locally. You can use whatever local libraries and dependencies you want: math libraries, data structures, even a local ML model. Just keep it self-contained.
Next steps
Section titled “Next steps”Start with the dumbest bot that works. Validate it. Then make it smarter, one decision at a time. Beat Kialasoa, then go after Razavavy, then try to dethrone Eva.
Ready to go live? Follow the Publish Your Bot guide to open a pull request and make your bot playable on play.giretra.com.
Want to understand the protocol under the hood or build a bot in a language without a template? Check out Build from Scratch.