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
state : GameState?
procedure
state : GameState?
procedure
(admin-player state) → Player?
state : GameState?
procedure
state : GameState?
procedure
(playable-card? state card) → boolean?
state : GameState? card : Card?
procedure
(playable-cards state) → (listof Card?)
state : GameState?
procedure
state : GameState?
procedure
(select-wildcard-color state) → string?
state : GameState?
procedure
(select-action state) → (or/c Card? 'draw)
state : GameState?
procedure
g : GameDescriptor? state : GameState? = (get-state g)
procedure
(bot-loop game-descriptor start-game-at-capacity) → void? game-descriptor : GameDescriptor? start-game-at-capacity : exact-nonnegative-integer?
3 API Client
(require runomatic/client) | package: runomatic |
procedure
(new-game player-name) → GameDescriptor?
player-name : string?
procedure
(join-game game-id player-name) → GameDescriptor?
game-id : string? player-name : string?
procedure
(start-game game-descriptor) → boolean?
game-descriptor : GameDescriptor?
procedure
(play-card game-descriptor card-id [ selected-color]) → boolean? game-descriptor : GameDescriptor? card-id : string? selected-color : string? = ""
procedure
game-descriptor : GameDescriptor?
procedure
(open-games) → (listof OpenGame?)
procedure
(get-state game-descriptor) → GameState?
game-descriptor : GameDescriptor?
procedure
game-descriptor : GameDescriptor?
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?
struct
(struct Card (id color value) #:extra-constructor-name make-Card #:transparent) id : string? color : (or/c string? #f) value : string?
struct
(struct Message (type data) #:extra-constructor-name make-Message #:transparent) type : string? data : string?
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?
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?
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?
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?