Kuh-Handel: Documentation

technical specifications and implementation guide for Rust and Python

This page provides an overview of how to build your bot for kuh-handel. You can find the full Rust documentation here, it will also be useful if you implement your bot in Python, as you can still see how the messages work. The most relevant part of the Rust documentation are:

  • StateMessages, which send a current state of the game to the bot, which then needs to decide how to respond with the corresponding action.
  • ActionMessages, which are the actions the bot sends back to the game.
  • GameUpdate, which describes what actually happened in the game.

The basic flow for the bot is something like this:

  1. Bot receives a GameUpdate
  2. Bot answers with the action NoAction::OK
  3. Bot receives a StateMessage which requests the bot to make an action
  4. Bot answers with the corresponding Action

This tutorial is available for Rust and Python:

Get your bot done while the others are still oxidizing.

Your bot will be blazingly fast.

Setup

Register Bots

If you want to join the competition you need to register. Choose a unique name and a secure token/ password for authentication and replace them in the url:

curl -X POST "https://ufuk-guenes.com/kuh-handel/register?player_id=<your_bot_name>&token=<your_token>"

btw: we call it token and not password because we do not really store it securely...

Setup environment

Because the base package is implemented in Rust, you will first need to install Rust anyway (if you dont have it already). To do so you can just follow the official Rust installation guide

We will first create a virtual environment and install the newest version of pyhandel, the python wrapper around the core Rust implementation of kuh-handel:

python -m venv .venv
source .venv/bin/activate
pip install pyhandel

if you want to upgrade to a newer version of the package:

pip install --upgrade pyhandel pyhandel-stubs

Run the following Cargo command in your project directory:

cargo add kuh-handel-lib tokio

Or add the following line to your Cargo.toml:

kuh-handel-lib = "*"
tokio = "1.47.1"

Setup Coding Environment

We strongly recommend you install the mypy extension for type hinting/ checking in python. That way you will be able to see what the objects you receive consist of. It was a big pain to get that working and there are some quirks, for example you will be getting type hints that you can do something like this:

from pyhandel.messages.game_updates import AuctionKind
AuctionKind.NormalAuction.NormalAuction.NormalAuction.NormalAuction.NormalAuction.NormalAuction ...

This makes no sense and your code will crash if you actually try to run it.



Simple layout

Below you can find a template from which you can start implementing your own bot. We will go over the relevant parts step by step

import asyncio
from pyhandel import Value, Money, Points
from pyhandel.animals import Animal, AnimalSet
from pyhandel.client import Client
from pyhandel.messages.actions import (
    AuctionDecision,
    Bidding,
    InitialTrade,
    NoAction,
    PlayerTurnDecision,
    SendMoney,
    TradeOpponentDecision,
)
from pyhandel.messages.game_updates import (
    AuctionRound,
    GameUpdate,
    TradeOffer,
    AuctionKind,
    MoneyTrade,
    MoneyTransfer,
)
from pyhandel.player import PlayerId
from pyhandel.player.player_actions import PlayerActions
from pyhandel.player.wallet import Wallet


class Bot(PlayerActions):

    def setup(self):
        # the __init__ can not take any arguments,
        # setup your class with a separate function
        raise NotImplementedError

    def _draw_or_trade(self) -> PlayerTurnDecision:
        raise NotImplementedError

    def _trade(self) -> InitialTrade:
        raise NotImplementedError

    def _provide_bidding(self, state: AuctionRound) -> Bidding:
        raise NotImplementedError

    def _buy_or_sell(self, state: AuctionRound) -> AuctionDecision:
        raise NotImplementedError

    def _send_money_to_player(self, player: str, amount: int) -> SendMoney:
        raise NotImplementedError

    def _respond_to_trade(self, offer: TradeOffer) -> TradeOpponentDecision:
        raise NotImplementedError

    def _receive_game_update(self, update: GameUpdate) -> NoAction:
        raise NotImplementedError

async def run(client, num_rounds):
    for _ in range(num_rounds):
        await client.play_one_round("game") 

if __name__ == "__main__":
    bot_name = "your_bot_name"
    bot_token = "your_private_token"
    base_url = "s://ufuk-guenes.com"  # "://127.0.0.1:2000" 
    play_n_rounds = 1
    print_faulty_action_warning = True


    bot = Bot()
    bot.setup()
    client = Client(bot_name, bot_token, bot, base_url, print_faulty_action_warning)

    try:
        asyncio.run(run(client, play_n_rounds))
    except KeyboardInterrupt:
        print("Client shutdown")
    except Exception as e:
        print("Error: ", e)

use kuh_handel_lib::Value;
use kuh_handel_lib::client::Client;
use kuh_handel_lib::messages::actions::{
    AuctionDecision, Bidding, InitialTrade, NoAction, PlayerTurnDecision, SendMoney,
    TradeOpponentDecision,
};
use kuh_handel_lib::messages::game_updates::{AuctionRound, GameUpdate, TradeOffer};
use kuh_handel_lib::player::base_player::PlayerId;
use kuh_handel_lib::player::player_actions::PlayerActions;

#[derive(Default)]
struct Bot;

impl PlayerActions for Bot {
    fn _draw_or_trade(&mut self) -> PlayerTurnDecision {
        todo!()
    }

    fn _trade(&mut self) -> InitialTrade {
        todo!()
    }

    fn _provide_bidding(&mut self, state: AuctionRound) -> Bidding {
        todo!()
    }

    fn _buy_or_sell(&mut self, state: AuctionRound) -> AuctionDecision {
        todo!()
    }

    fn _send_money_to_player(&mut self, player: &PlayerId, amount: Value) -> SendMoney {
        let _ = amount;
        todo!()
    }

    fn _respond_to_trade(&mut self, offer: TradeOffer) -> TradeOpponentDecision {
        todo!()
    }

    fn _receive_game_update(&mut self, update: GameUpdate) -> NoAction {
        todo!()
    }
}

pub async fn run(client: &mut Client, num_rounds: u32) {
    for _ in 0..num_rounds {
        client.play_one_round("pvp_games".to_string()).await;
    }
}

#[tokio::main]
async fn main() {
    let name = "your_bot_name".to_string();
    let token = "your_private_token".to_string();
    let base_url = "s://ufuk-guenes.com".to_string(); // "://127.0.0.1:2000"
    let raise_faulty_action_warning = true;
    let play_n_rounds = 1;

    let bot = Box::new(Bot::default());

    let mut client = Client::new(name, token, bot, base_url, raise_faulty_action_warning);

    run(&mut client, play_n_rounds).await;
}


This is the main function, which starts the bot, once you have it implemented. Don't worry, we will go over how to do that in a second.

We first need to define a couple of variables. The name of your bot and the corresponding token you defined during registration. The url where the bot connects to which is either the url already present or if you somehow managed to setup your own kuh-handle server you can connect to the api over the localhost. You can also turn on or off if you want to the bot to print out what illegal/ faulty moves you made after it has finished a game.

The bot should be the only thing you need to implement yourself. It is responsible for the actual logic of how to play the game. The client is just there to handle the connection to the server and make sure the turn taking is followed correctly.

Because the python class is just a wrapper around our rust code, we have some quirks to work around. For example you can not overwrite the standard __init__ function and pass additional arguments. Thats why will just implement a setup function where you could pass your own arguments and initialize the bot however you want. So we need to call that setup function after initializing the bot.

if __name__ == "__main__":
    bot_name = "your_bot_name"
    bot_token = "your_private_token"
    base_url = "s://ufuk-guenes.com"  # "://127.0.0.1:2000" 
    play_n_rounds = 1
    print_faulty_action_warning = True

    bot = Bot()
    bot.setup()
    client = Client(bot_name, bot_token, bot, base_url, print_faulty_action_warning)

    try:
        asyncio.run(run(client, play_n_rounds))
    except KeyboardInterrupt:
        print("Client shutdown")
    except Exception as e:
        print("Error: ", e)
#[tokio::main]
async fn main() {
    let name = "your_bot_name".to_string();
    let token = "your_private_token".to_string();
    let base_url = "s://ufuk-guenes.com".to_string(); // "://127.0.0.1:2000"
    let raise_faulty_action_warning = true;
    let play_n_rounds = 1;

    let bot = Box::new(Bot::default());

    let mut client = Client::new(name, token, bot, base_url, raise_faulty_action_warning);

    run(&mut client, play_n_rounds).await;
}

We can then call the client to let the bot play one round of kuh-handel (or just let it play multiple times sequentially).

You need to pick what kind of game you want to play. There are different options you can choose from:

  • "pvp_games": In this game mode you play against bots implemented by other players. These games count towards your win rate and are used for the ranking. We might add one of the bots we (Leon and Ufuk) have implemented to fill up a game if there are not enough players, or if we just feel like it hihihi...
  • "server_bot_game": Your bot will only play games against our bots. You can use this game mode to find bugs, see if your bot makes sensible actions and generally see if your bot works (lets be honest, it wont...). These games do not count into the results, but we might look at them and shame you anyway.

async def run(client, num_rounds):
    for _ in range(num_rounds):
        await client.play_one_round("pvp_games") 

pub async fn run(client: &mut Client, num_rounds: u32) {
    for _ in 0..num_rounds {
        client.play_one_round("pvp_games".to_string()).await;
    }
}

Now lets have a look at what functions you will need to implement. We will first look at what those functions are and afterwards show you a basic starting point, so you see how the package works in detail.

Create a bot class which inherits from PlayerActions which defines the interface the bot uses.

And then as mentioned before, you can implement the setup() function to initialize your bot however you want.

class Bot(PlayerActions):

    def setup():
        # the __init__ can not take any arguments,
        # setup your class with a separate function
        raise NotImplementedError

Create a bot struct, which will implement the PlayerActions trait.

#[derive(Default)]
struct Bot;

One main part of the game is deciding if you want to draw or trade a card. This function is where you will make that decision.

def _draw_or_trade(self) -> PlayerTurnDecision:
        raise NotImplementedError
fn _draw_or_trade(&mut self) -> PlayerTurnDecision {
        todo!()
}

If the game enters the trading only phase, this is where you decide what trade you will do. You can of course also just call this function yourself in the _draw_or_trade when you have decided to trade.

def _trade(self) -> InitialTrade:
        raise NotImplementedError
fn _trade(&mut self) -> InitialTrade {
        todo!()
}

When someone else has drawn a card your bot will be asked to place a bid on that animal or you can decide to pass.

def _provide_bidding(self, state: AuctionRound) -> Bidding:
        raise NotImplementedError
fn _provide_bidding(&mut self, state: AuctionRound) -> Bidding {
        todo!()
}

If you are the host of an auction and it is over you will need to decide if you want to sell the animal or just buy it yourself.

def _buy_or_sell(self, state: AuctionRound) -> AuctionDecision:
        raise NotImplementedError
fn _buy_or_sell(&mut self, state: AuctionRound) -> AuctionDecision {
        todo!()
}

If you are the one who will need to pay for an animal after an auction, here is where you decide what combination of bills you send to your opponent. Remember, you will get no change, even if you can not exactly match the requested amount.

def _send_money_to_player(self, player: str, amount: int) -> SendMoney:
        raise NotImplementedError
fn _send_money_to_player(&mut self, player: &PlayerId, amount: Value) -> SendMoney {
        let _ = amount;
        todo!()
}

If one of your opponents challenges you to trade, you will need to decide how to respond. Either you just accept or you make a counter offer.

def _respond_to_trade(self, offer: TradeOffer) -> TradeOpponentDecision:
        raise NotImplementedError
fn _respond_to_trade(&mut self, offer: TradeOffer) -> TradeOpponentDecision {
        todo!()
}

The _receive_game_update is maybe the heart of your bot. This function will receive all the information of what has actually happened in the game: which trades where made, what money was exchanged, which players were exposed etc.

This is the ground truth for what actually happened. If your bot made an illegal response the game will (hopefully) correct it and you will see what actually happened in the game update. At the end of the game you will also receive information of what your illegal moves where and how they were corrected. So don't rely on the moves you calculated yourself, or do, i don't care.

We strongly recommend you start by implementing this function first. It will help you understand what you need to consider and you will get a feel for how the use the objects like the messages or actions you will need to send.

def _receive_game_update(self, update: GameUpdate) -> NoAction:
        raise NotImplementedError
fn _receive_game_update(&mut self, update: GameUpdate) -> NoAction {
        todo!()
}



Using an existing bot as a starting point

If you want you can also start by using the bot Ufuk has implemented and just overwrite the functions as you want.

Additionally to the bot name, it also takes a risk factor between 0 and 1, which controls how aggressive the bot will play.

Import the SimplePlayer and initialize it in the setup function.

from pyhandel.player.simple_player import SimplePlayer
class Bot(PlayerActions):

    def __init__(self) -> None:
        super().__init__()

    def setup(self, bot_name, risk):
        self.simple_player = SimplePlayer(bot_name, risk)

    def _provide_bidding(self, state):
        return self.simple_player._provide_bidding(state)

    def _receive_game_update(self, update):
        self.simple_player._receive_game_update(update)
        """
        continue with your own implementation if you want
        """


struct Bot {
    simple_player: SimplePlayer,
}

impl PlayerActions for Bot {
    fn _draw_or_trade(&mut self) -> PlayerTurnDecision {
        self.simple_player._draw_or_trade()
    }

    fn _receive_game_update(&mut self, update: GameUpdate) -> NoAction {
        self.simple_player._receive_game_update(update);
        /*
        continue with your own implementation if you want
         */
    }

then in each function where you want to use it, just call the corresponding function of the simple_player and if required, pass the arguments as well. This is maybe the fastest way to get a bot running, and then you can decide which parts the bot you want to improve. Independent of what function you use the simple bot for, if you use, you have to also pass the game update to it, otherwise, it has no idea of whats happening.

More explanations

Below you can find in depth descriptions to implement the Methods. It shows how the basic functions are

import random as rn

class Bot(PlayerActions):

    def _draw_or_trade(self) -> PlayerTurnDecision:
        if rn.random() < 0.5:
            return PlayerTurnDecision.Trade(self._trade())
        return PlayerTurnDecision.Draw()

    def _trade(self) -> InitialTrade:
        player = # choose player id
        animal = # choose animal to trade
        trade = InitialTrade(player, animal, 1, [0])
        print(trade.amount)
        return trade
    
    def _provide_bidding(self, state: AuctionRound) -> Bidding:
        host = state.host
        animal = state.animal
        bids = state.bids
        
        if rn.random() < 0.5:
            return Bidding.Bid(rn.randint(1, 100))
        return Bidding.Pass()
    
    def _buy_or_sell(self, state: AuctionRound) -> AuctionDecision: 
        host = state.host
        animal = state.animal
        bids = state.bids

        if rn.random() < 0.5:
            return AuctionDecision.Buy()
        return AuctionDecision.Sell()

    def _send_money_to_player(self, player: PlayerId, amount: Value) -> SendMoney: 
        if rn.random() < 0.05:
            return SendMoney.WasBluff()
        return SendMoney.Amount([0, 0])

    def _respond_to_trade(self, offer: TradeOffer) -> TradeOpponentDecision: 
        challenger = offer.challenger
        animal = offer.animal
        count = offer.animal_count
        card_offer = offer.challenger_card_offer

        if rn.random() < 0.5:
            return TradeOpponentDecision.Accept()
        return TradeOpponentDecision.CounterOffer([10, 0])
    
    def _receive_game_update(self, update):
        match update:
            case GameUpdate.Auction(kind):
                self.handle_auction(kind)
            case GameUpdate.End(ranking):
                self.handle_end(ranking)
            case GameUpdate.Start(wallet, player_order, animals):
                self.handle_start(wallet, player_order, animals)
            case GameUpdate.ExposePlayer(player, wallet):
                self.handle_expose_player(player, wallet)
            case GameUpdate.Inflation(money):
                self.handle_inflation(money)
            case GameUpdate.Trade(challenger, opponent, animal, animal_count, receiver, money_trade):
                self.handle_trade(challenger, opponent, animal, animal_count, receiver, money_trade)
        return NoAction.Ok()
    
    def handle_auction(self, kind: AuctionKind):
        match kind:
            case AuctionKind.NoBiddings(host, animal):
                pass
            case AuctionKind.NormalAuction(rounds, from_player, to_player, money_transfer):
                match money_transfer:
                    case MoneyTransfer.Public(card_amount, min_value):
                        pass
        raise NotImplementedError()
        
    def handle_end(self, ranking: list[tuple[PlayerId, Points]]):
        raise NotImplementedError()
    
    def handle_start(self, wallet: Wallet, player_order: list[PlayerId], animals: list[AnimalSet]):
        notes = wallet.bank_notes
        first_animal = animals[0]
        inflation = first_animal.inflation
        animal_value = first_animal.animal
        raise NotImplementedError()

    def handle_trade(self, challenger: PlayerId, opponent: PlayerId, animal: Animal, animal_count: int, receiver: PlayerId, money_trade: MoneyTrade):
        match money_trade:
            case MoneyTrade.Public(challenger_offer, opponent_offer):
                pass
            case MoneyTrade.Private(challenger_offer, opponent_offer):
                pass
        raise NotImplementedError()

    def handle_expose_player(self, player: PlayerId, wallet: Wallet):
        raise NotImplementedError()
    
    def handle_inflation(self, money: Money):
        raise NotImplementedError()


blazingly fast rust example

fn greet(name: &str) {
      println!("Hello, {}!", name);
    }