Runomatic — Runo Client and Bot
1 About
2 Bot
2.1 Command Line Bot
2.2 Bot API
my-player
my-hand
admin-player
my-turn?
playable-card?
playable-cards
actions
hand-by-color
select-wildcard-color
select-action
play-turn
bot-loop
3 API Client
new-game
join-game
start-game
play-card
draw-card
open-games
get-state
quit-game
4 Types
4.1 Structure Definitions
Game  Descriptor
Card
Message
Player
Game  State
Open  Game
4.2 Converting from JSON
jsexpr->Game  State
jsexpr->Card
jsexpr->Player
jsexpr->Message
8.6

Runomatic — Runo Client and Bot

 (require runomatic) package: runomatic

1 About

While hanging out with some friends on a Saturday evening, we wanted to play Uno, and found a FOSS Uno webapp (source code), we wanted to add additional players to make the game more interesting. Plus it would be fun to play bots against each other. As a result I wrote this quick ‘n dirty “API Client” to the Runo API, and a simple rule-based bot to play the game.

The source code for this project is hosted on GitHub. This project is licensed is licensed under the terms of Unlicense.

2 Bot

The bot is available as an API and as a Command Line application.

2.1 Command Line Bot

Note: the dots outputted by the CLI’s bot loop correspond to a game tick.

Usage:

$ runomatic --help

runomatic [ <option> ... ]

 where <option> is one of

  -n <n>, --name <n> : The bot's name

  -l, --list : List open games

/ --game-id <i> : Join by game ID

| --game-name <n> : Join by game Name

| --game-descriptor <d> : Resume an existing game

\ --new-game <num-players> : Start a new game, and start when num-players join

  --help, -h : Show this help

  -- : Do not treat any remaining argument as a switch (at this level)

 /|\ Brackets indicate mutually exclusive options.

 Multiple single-letter switches can be combined after one `-'; for

  example: `-h-' is the same as `-h --'

List open games:

$ runomatic -l

ID                                               Name       Players Created

wspY17d6GkLGPVXU2dmfbDO7q4bZubEIjdxOBPb9bExfAvLx Game30517        1 2020-10-19 01:16:

44

Start a new game with three players:

$ runomatic --name billybob --new-game 3

Game name: Game89758 Game ID: cTNVmUBBfPU3s1NyFLalO5JG4azE97Awabln25PCYvFG6TDL

To resume session, use --game-descriptor cTNVmUBBfPU3s1NyFLalO5JG4azE97Awabln25PCYvFG

6TDL:uSkcPgYN8oWKtnaAJgbAYF4xbKq5QO2t17jQWYpyzhup72cZ

Open this URL to watch the game: https://runogame.com/play/cTNVmUBBfPU3s1NyFLalO5JG4a

zE97Awabln25PCYvFG6TDL/uSkcPgYN8oWKtnaAJgbAYF4xbKq5QO2t17jQWYpyzhup72cZ

....

Join an existing game:

$ runomatic --game-id cTNVmUBBfPU3s1NyFLalO5JG4azE97Awabln25PCYv

FG6TDL

Game name: Game89758 Game ID: cTNVmUBBfPU3s1NyFLalO5JG4azE97Awabln25PCYvFG6TDL

To resume session, use --game-descriptor cTNVmUBBfPU3s1NyFLalO5JG4azE97Awabln25PCYvFG

6TDL:ERvXVQ6gXoE2ZXlTz00vtymLlEs5NC50Wlkzu5zm6niKrbW6

Open this URL to watch the game: https://runogame.com/play/cTNVmUBBfPU3s1NyFLalO5JG4a

zE97Awabln25PCYvFG6TDL/ERvXVQ6gXoE2ZXlTz00vtymLlEs5NC50Wlkzu5zm6niKrbW6

....

Resume an existing session:

$ runomatic --game-descriptor cTNVmUBBfPU3s1NyFLalO5JG4azE97Awab

ln25PCYvFG6TDL:ERvXVQ6gXoE2ZXlTz00vtymLlEs5NC50Wlkzu5zm6niKrbW6

 

Game name: Game89758 Game ID: cTNVmUBBfPU3s1NyFLalO5JG4azE97Awabln25PCYvFG6TDL

To resume session, use --game-descriptor cTNVmUBBfPU3s1NyFLalO5JG4azE97Awabln25PCYvFG

6TDL:ERvXVQ6gXoE2ZXlTz00vtymLlEs5NC50Wlkzu5zm6niKrbW6

Open this URL to watch the game: https://runogame.com/play/cTNVmUBBfPU3s1NyFLalO5JG4a

zE97Awabln25PCYvFG6TDL/ERvXVQ6gXoE2ZXlTz00vtymLlEs5NC50Wlkzu5zm6niKrbW6

....

2.2 Bot API

 (require runomatic/bot) package: runomatic

procedure

(my-player state)  Player?

  state : GameState?
Get your player.

procedure

(my-hand state)  (listof Card?)

  state : GameState?
Get your player’s hand.

procedure

(admin-player state)  Player?

  state : GameState?
Get the player that created the game.

procedure

(my-turn? state)  boolean?

  state : GameState?
Is it my player’s turn?

procedure

(playable-card? state card)  boolean?

  state : GameState?
  card : Card?
Can the card be played?

procedure

(playable-cards state)  (listof Card?)

  state : GameState?
Cards that are immediately playable.

procedure

(actions state)  (listof (or/c Card? 'draw))

  state : GameState?
Actions immediately available to your player.

procedure

(hand-by-color hand)  (hash? string? (listof Card?))

  hand : (listof Card?)
The hand organized by color.

procedure

(select-wildcard-color state)  string?

  state : GameState?
Select an ideal color for the player to play a wildcard as.

procedure

(select-action state)  (or/c Card? 'draw)

  state : GameState?
Select an action to play.

procedure

(play-turn g [state])  boolean?

  g : GameDescriptor?
  state : GameState? = (get-state g)
Play the turn. Result indicates if the turn was played.

procedure

(bot-loop game-descriptor    
  start-game-at-capacity)  void?
  game-descriptor : GameDescriptor?
  start-game-at-capacity : exact-nonnegative-integer?
Run the bot. If the game hasn’t been started, the bot will attempt to play the game when at least start-game-at-capacity players are have joined (including the bot itself).

3 API Client

 (require runomatic/client) package: runomatic

procedure

(new-game player-name)  GameDescriptor?

  player-name : string?
Create a new game.

procedure

(join-game game-id player-name)  GameDescriptor?

  game-id : string?
  player-name : string?
Join an existing game.

procedure

(start-game game-descriptor)  boolean?

  game-descriptor : GameDescriptor?
Start the game. Only works if the player referenced by the game-descriptor is an admin.

procedure

(play-card game-descriptor    
  card-id    
  [selected-color])  boolean?
  game-descriptor : GameDescriptor?
  card-id : string?
  selected-color : string? = ""
Play a card.

procedure

(draw-card game-descriptor)  boolean?

  game-descriptor : GameDescriptor?
Draw a card.

procedure

(open-games)  (listof OpenGame?)

Get the open games.

procedure

(get-state game-descriptor)  GameState?

  game-descriptor : GameDescriptor?
Get the game state.

procedure

(quit-game game-descriptor)  void?

  game-descriptor : GameDescriptor?
Quit the game.

4 Types

 (require runomatic/types) package: runomatic

4.1 Structure Definitions

struct

(struct GameDescriptor (game-id player-id)
    #:extra-constructor-name make-GameDescriptor
    #:transparent)
  game-id : string?
  player-id : string?
A token that represents a player in a particular game session. The game-id and player-id are both chosen by the Runo backend server.

struct

(struct Card (id color value)
    #:extra-constructor-name make-Card
    #:transparent)
  id : string?
  color : (or/c string? #f)
  value : string?
A card as given back from the server. Wildcards will have a color of #f.

struct

(struct Message (type data)
    #:extra-constructor-name make-Message
    #:transparent)
  type : string?
  data : string?
A game related message sent from the server, such as the reversal of play order or when a player wins the round.

struct

(struct Player (id
    name
    admin
    active
    hand
    hand-size
    draw-required
    points
    rounds-won
    game-winner
    ux-id)
    #:extra-constructor-name make-Player
    #:transparent)
  id : string?
  name : string?
  admin : boolean?
  active : boolean?
  hand : (or/c (listof Card?) #f)
  hand-size : exact-nonnegative-integer?
  draw-required : boolean?
  points : exact-nonnegative-integer?
  rounds-won : exact-nonnegative-integer?
  game-winner : boolean?
  ux-id : string?
A player as represented by the server. The fields are:
  • id corresponds to a GameDescriptor-player-id.

  • name is the human-readable name for the player.

  • admin is set to #t when the player can start a game.

  • active is #t when the player can play a card.

  • hand is a list of the cards the current player has in their hand. Other players hand is #f.

  • hand-size is the number of cards in this player’s hand. This field is always available to other players.

  • draw-required

  • points is the total number of points this player has earned.

  • rounds-won is the number of rounds won by this player.

  • game-winner is set to #t when the player won the entire game.

  • ux-id is unused by this Racket package, but is available for consumers of the API client.

struct

(struct GameState (id
    name
    active
    draw-pile-size
    discard-pile-size
    last-discard
    reverse
    point-to-win
    messages
    created-at
    started-at
    ended-at
    players
    max-players
    min-players)
    #:extra-constructor-name make-GameState
    #:transparent)
  id : string?
  name : string?
  active : boolean?
  draw-pile-size : exact-nonnegative-integer?
  discard-pile-size : exact-nonnegative-integer?
  last-discard : (or/c Card? #f)
  reverse : boolean?
  point-to-win : exact-nonnegative-integer?
  messages : (listof Message?)
  created-at : date?
  started-at : (or/c date? #f)
  ended-at : (or/c date? #f)
  players : (listof Player?)
  max-players : exact-nonnegative-integer?
  min-players : exact-nonnegative-integer?
A game state represented by the server. The fields are:
  • id corresponds to a GameDescriptor-game-id

  • name is the human readable name of the game.

  • active is #t when the game is in session.

  • draw-pile-size is the size of the draw pile.

  • discard-pile-size is the size of the discard pile.

  • last-discard is the last played card.

  • reverse is #t when the turn order is reversed.

  • point-to-win is the number of points necessary to win the entire game.

  • messages is the game messages such as when a player wins a round.

  • created-at is the time the game was created.

  • started-at is the time the game was started. If the game has yet to be started, it is #f.

  • ended-at is the time the game finished. If the game has yet to finish, it is #f.

  • players is a list of the game players, including your own player.

  • max-players is the maximum number of players that can join this game.

  • min-players is the minimum number of players necessary to start the game.

struct

(struct OpenGame (id name created players)
    #:extra-constructor-name make-OpenGame
    #:transparent)
  id : string?
  name : string?
  created : date?
  players : exact-nonnegative-integer?
An open game available to join.

4.2 Converting from JSON

procedure

(jsexpr->GameState jsexpr)  GameState?

  jsexpr : jsexpr?

procedure

(jsexpr->Card jsexpr)  Card?

  jsexpr : jsexpr?

procedure

(jsexpr->Player jsexpr)  Player?

  jsexpr : jsexpr?

procedure

(jsexpr->Message jsexpr)  Message?

  jsexpr : jsexpr?