-
Notifications
You must be signed in to change notification settings - Fork 2
Arimaa Game Server JSON API
Below is a description of the server queries implemented so far for the back-end of the site, as well as those to be implemented. NOTE: At the moment, because the server is still in development, the format and behavior of any query or response is still subject to change.
API is as of https://github.com/lightvector/arimaa-server/commit/03e993a52c7480efbc5c9035804c25c7859f9330
All requests and responses are in JSON.
All paths are relative to the subpath under which the API server is running, for example:
http://playarimaa.org/api/
These are various common types of data used in the communication to and from the server:
-
<Username> = <String>- Login name of a user -
<GameID> = <String>- Alphanumeric hash used as an identifier for a game -
<SiteAuth> = <String>- Authorization token returned from logging in to the site -
<ChatAuth> = <String>- Authorization token returned from joining a chat channel -
<GameAuth> = <String>- Authorization token returned from joining a game -
<Timestamp> = <Double>- Seconds since the epoch (1970-01-01 00:00 UTC). Includes fractional seconds. The precision is unspecified by this API. -
<ChatChannel> = <String>- A string representing a path that can be posted to via chat. Currently, the only legal values for<ChatChannel>are"main"and"game/"<GameID>.
Common response types from the server for various queries:
-
<Message> = {"message":<String>}- Ex: "Ok", "Move 3g accepted",... -
<Error> = {"error":<String>}- Ex: "Missing field 'field'", "Illegal move", ...
All queries return <Error> if not successful.
To register an account:
POST /accounts/register {"username":<Username>,"email":<String>,"password":<String>,"isBot":<Bool>,"priorRating":<String>}
POST /accounts/register {"username":<Username>,"email":<String>,"password":<String>,"isBot":<Bool>,"priorRating":<String>,"oldSiteAuth":<SiteAuth>}
If successful: {"username":<Username>,"siteAuth":<SiteAuth>}
"priorRating" indicates an estimate of the the player's rating that the server will use as an initial guess.
If you are currently logged into as a guest with some SiteAuth and are attempting to register an account with that username , you can provide "oldSiteAuth" to avoid failing due to the overlapping username.
To verify an email address and complete the registration of a new account:
POST /accounts/verifyEmail {"username":<Username>,"verifyAuth":<String>}
If successful: <Message>
"verifyAuth" should be the authentication token received from the email triggered by the register query. Accounts whose emails have not been verified will be cleaned up and deleted periodically.
To resend the email for completing registration of a new account:
POST /accounts/resendVerifyEmail {"username":<Username>,"siteAuth":<String>}
If successful: <Message>
To request a password reset:
POST /accounts/forgotPassword {"username":<Username>}
If successful: <Message>
Triggers an email to the specified user. NOTE: For this particular query, an email address is also acceptable for the "username".
To request a password reset:
POST /accounts/resetPassword {"username":<Username>,"resetAuth":<String>,"password":<String>}
If successful: <Message>
"resetAuth" should be the authentication token received from the email triggered by the forgotPassword query.
To request an email address change:
POST /accounts/changePassword{"username":<Username>,"password":<String>,"siteAuth":<SiteAuth>,"newPassword":<String>}
If successful: <Message>
To request an email address change:
POST /accounts/changeEmail {"username":<Username>,"password":<String>,"siteAuth":<SiteAuth>,"newEmail":<String>}
If successful: <Message>
Triggers an email to the new address for confirmation of the change.
To request an email address change:
POST /accounts/confirmChangeEmail {"username":<Username>,"changeAuth":<String>}
If successful: <Message>
"changeAuth" should be the authentication token received from the email triggered by the changeEmail query.
To log in:
POST /accounts/login {"username":<Username>,"password":<String>}
If successful: {"username":<Username>,"siteAuth":<SiteAuth>}
NOTE: For this particular query, an email address is also acceptable for the "username".
NOTE: Usernames are not case specific for logging in. However, the case used for registration will be remembered and used for display to other users, regardless of the login case. The username returned upon success from this query will have the registration case, rather than the login case.
To log in without creating a permanent account:
POST /accounts/login {"username":<Username>}
POST /accounts/login {"username":<Username>,"oldSiteAuth":<SiteAuth>}
If successful: {"username":<Username>,"siteAuth":<SiteAuth>}
If you were currently logged into as a guest with some SiteAuth and are attempting to re-log in as that guest again, you can provide "oldSiteAuth" to avoid failing due to the overlapping username.
To log out:
POST /accounts/logout {"siteAuth":<SiteAuth>}
If successful: <Message>
To check whether a given siteAuth is valid or not:
POST /accounts/authLoggedIn {"siteAuth":<SiteAuth>}
If successful: {"value":<Boolean>}
To get a list of all logged-in users:
POST /accounts/usersLoggedIn {}
If successful: {"users":[ShortUserInfo,ShortUserInfo,...]}
To get a list notifications to be displayed to the user:
GET /accounts/<Username>/<SiteAuth>/notifications
If successful: [<String>,<String>,...]
-
Short publicly viewable information returned when querying about a user:
<ShortUserInfo> = { "name":<Username>, "rating":<Double>, #Elo-style rating, based on internal rating model. "ratingStdev":<Double>, #Measure of uncertainty of rating "isBot":<Bool>, "isGuest":<Bool> } -
The manner in which a game ended (others may be added in the future):
<EndingReason> = "g" - If game ended by goal "e" - If game ended by elimination "m" - If game ended by immobilization "t" - If game ended by one player running out of time "r" - If game ended by resignation "f" - If game ended by forfeit "i" - If game ended by submission of an illegal move (applies to bots only) "a" - If game was adjourned without a winner "x" - If game was interrupted in the middle of play (such as by a server crash) -
Specifies alternative types of games that could be played:
<GameType> = "standard" - Standard Arimaa game "handicap" - Players can set up with a partial set of pieces or have differing time controls "directsetup" - Game begins with a specific predetermined position -
For games that have ended, information about the ending:
<GameResult> = { "winner":<String>, #"g" or "s", or "n" if there was no winner "reason":<EndingReason>, #The time that the turn began for the player-to-move on the turn the game ended. On a normal #game end (goal/elim/immo), this is equal to endTime, on resignation or timeout it is the #start-of-turn time for the player whose turn it was when the resignation/timeout occured. "lastMoveStartTime":<Timestamp> #The time this game ended with this result, whether due to a move being played, or due to resignation or timeout or anything else "endTime":<Timestamp>, #Whether this game was counted for statistics/ratings (as opposed to being invalidated for being too short or manually) #Normally, games where the losing player never made any moves are not counted, even if otherwise marked as rated games. "countForStats":<Boolean> } -
Board positions:
#Board written in FEN-order (a8-h8, a7-h7, ... a1-h1) #but without compression of empty spaces and with dots for spaces #ex:"rrrrrrrr/chdemdhc/......../......../....E.../.H....H./C.DM.D.C/RRRRRRRR" <BoardPosition> = <String> -
Information about the time control for a player in a game:
<TimeControl> = { #All values are in seconds "initialTime":<Int>, #The initial amount of time a player begins with on the clock "increment":<Int>, #OPTIONAL - Time added to a player's clock at the beginning of each move. "delay":<Int>, #OPTIONAL - Length of time into a move before a player's clock actually begins decrementing. "maxReserve":<Int>, #OPTIONAL - if present, cap player's clock to this amount at each end of turn. "maxMoveTime":<Int>, #OPTIONAL - if present, the maximum allowed time for a player to make one move. #Option for soft-capping the maximum total length of a game. #If present, after this many full turns pass, go into overtime and thereafter decrease "increment" and "delay" #exponentially, multiplying by (1-1/30) at the end of each full turn (on average, halving approximately every 20 full turns). #For example, if this is 70, then at the end of each turn 71s, 72s, 73s,... the increment/delay will multiply by 29/30, #and the total amount of time ever available for a player will be initialTime + (increment + delay) * (70 + 30). "overtimeAfter":<Int> #OPTIONAL }In the event that a time control is displayed to the user as a single string, the following is a recommended format for display:
<initialTime>[+<increment>][~<delay>][(<maxReserve> maxresv)]/[(<maxMoveTime> max/mv)][(max <overtimeAfter>t)]
where times are displayed using suffixes in {s,m,h,d} for seconds, minutes, hours, days, respectively.For example, a game with:
initialTime=930, delay=15, maxMoveTime=60, overtimeAfter=70
would be:
15m30s~15s(1m max/mv)(ovt 70t)
A postal game with:
initialTime=1209600,increment=129600,maxReserve=1814400
would be:
14d+1d12h(21d maxresv)Games whose possible reserve (
initialTimecapped atmaxReserve) is at least 2 days or whose normal per-move time (increment + delay, capped atmaxMoveTime) is at least 2 hours for at least one player will be considered postal games for the purposes of game search and display and game handling and cleanup details. -
Information about an open game that players can join:
<OpenGameData> = { "creator":<ShortUserInfo>, #OPTIONAL - present if the game was user-created (versus automatically as part of an event) "joined":[<ShortUserInfo>,...] #Users joined and ready to start the game, including the creator. "creationTime":<TimeStamp> #Time that this game was first opened } -
Information about an active game being played right now:
<ActiveGameData> = { "moveStartTime":<Timestamp>, #Time that clock started running for the current move "timeSpent":<Double>, #Seconds since the start of the current move "gClockBeforeTurn":<Double>, #Seconds on gold's clock before the turn started (prior to adding increment) "sClockBeforeTurn":<Double>, #Seconds on silver's clock before the turn started (prior to adding increment) "gPresent":<Bool>, #Whether gold is present and connected to the game "sPresent":<Bool> #Whether silver is present and connected to the game } -
Surface-level information returned when querying about a game or games:
<GameMetadata> = { "id":<GameID>, "numPly":<Int>, #Total number of half-moves made in the game (ex: {1g, 1s, 2g, 2s, 3g} = 5 ply) "startTime":<Timestamp>, #OPTIONAL - Time that the game (and the game clock) was started "gUser":<ShortUserInfo>, #OPTIONAL - absent for open games where the gold player is not yet determined "sUser":<ShortUserInfo>, #OPTIONAL - absent for open games where the silver player is not yet determined "gTC":<TimeControl>, #Time control for Gold "sTC":<TimeControl>, #Time control for Silver "rated":<Bool>, #Whether the game should be used for ratings. "countForStats" (see above) must also be true for a game to actually count. "postal":<Bool>, #Whether the game is a postal game "gameType":<GameType>, "tags":[<String>,<String>,...], #Tags indicating games that belong to various events, etc. "openGameData":<OpenGameData>, #OPTIONAL - present only for open games "activeGameData":<ActiveGameData>, #OPTIONAL - present only for games being played right now "result":<GameResult>, #OPTIONAL - present if the game is finished "position":<BoardPosition>, "now":<Timestamp> #Server timestamp as of the reporting of this metadata #Sequence number for polling queries, incremented on each update, starts at 0. "sequence":<Long> #OPTIONAL - present for open or active games. } -
Information about the time a move was played:
<MoveTime> = { "start":<Timestamp>, #Time that the clock (or delay) started running for this move "time":<Timestamp> #Time that this move was received and played }Most of the time,
startwill be the same astimeof the previous move, but in the event of a game resumption beginning on that move, it will be the time that the game was resumed. -
Detailed information returned when querying about a game:
<GameState> = { "history":[<String>,<String>,...], #Move history in the order [1g,1s,2g,2s...] ex: ["Ra1","Ra7","Ra1n Ra2n Ra3n Ra4n"] "moveTimes":[<MoveTime>,<MoveTime>,...], #Time that each move was received and played "toMove":<String>, #Next player to move, either "g" or "s" "meta":<GameMetadata>, }
To get the metadata about a single game:
GET /games/<GameID>/metadata
GET /games/<GameID>/metadata?minSequence=<INT>
GET /games/<GameID>/metadata?minSequence=<INT>&timeout=<INT>
(will also accept old arimaa.com numeric game ids in place of <GameID> - TODO Unimplemented)
If successful: <GameMetadata>
If minSequence is specified then blocks so long as:
- Fewer than "timeout" seconds have passed (default 20, max 120).
- The game remains open and/or active.
- The latest gamestate has a
sequencenumber less thanminSequence.
(returning immediately if any of these conditions are not true at the time of the query).
In the case of a timeout, also returns the latest game state (even if it doesn't satisfy minSequence) rather than returning an error.
To get the metadata about a set of games:
GET /games/search?key=value&key=value&...
Legal keys and values:
open=<Bool> #Require games to be open or not, default false
active=<Bool> #Require games to be active or not, default false
rated=<Bool> #Require games to be rated or not
postal=<Bool> #Require games to be postal or not
gameType=<String> #Restrict to games with a certain gameType
user1=<UserName> #Involves the given user
user2=<UserName> #Involves the given user
gUser=<UserName> #Gold player is the given user
sUser=<UserName> #Silver player is the given user
creator=<UserName> #Game creator is the specified user. Must also specify open=true.
creatorNot=<UserName> #Game creator is NOT the specified user. Must also specify open=true.
minTime=<Timestamp> #Game ends on or after this, or for open/active games, created/started on or after this
maxTime=<Timestamp> #Game ends on or before this, or for open/active games, created/started on or before this
minDateTime=<String> #Same as minTime, but parses a date+time+zone string.*
maxDateTime=<String> #Same as minTime, but parses a date+time+zone string.*
includeUncounted=<Bool>#If true, include games where countForStats==false (by default, searches exclude such games)
limit=<Int> #Limit the number of returned games to this many (default 50, max 1000)
* Accepted datetime formats are ISO8601 and similar: "yyyy-MM-dd'T'HH-mm-ss" plus a timezone.
Timezones can be specified both as "(+|-)ZZ:ZZ" such as "+05:00" or "-06:00"
or by tz database id such as "America/San_Francisco" or "Europe/London".
Both zone and time of day are optional, with zone defaulting to UTC and time of day defaulting to 00:00:00.
Ex: "2015-06-01", "2015-09-01T15:00:00-06:00", "2015-01-01 America/New_York"
If successful: [<GameMetadata>,<GameMetadata>,...]
Games returned are sorted in order of decreasing ending time, or if open=true or active=true was provided, in order of decreasing created/started time.
To get the full state of a game:
GET /games/<GameID>/state
GET /games/<GameID>/state?minSequence=<INT>
GET /games/<GameID>/state?minSequence=<INT>&timeout=<INT>
(will also accept old arimaa.com numeric game ids in place of <GameID> - TODO Unimplemented)
If successful: <GameState>
If minSequence is specified then blocks so long as:
- Fewer than "timeout" seconds have passed (default 20, max 120).
- The game remains open and/or active.
- The latest gamestate has a
sequencenumber less thanminSequence.
(returning immediately if any of these conditions are not true at the time of the query).
In the case of a timeout, also returns the latest game state (even if it doesn't satisfy minSequence) rather than returning an error.
Websocket streaming interface for gamestates.
WARNING!!!: Websockets are buggy in the current server due to bugs in the underlying scala web libraries.
Server messages are of the form: <GameState> or <Error> or single newline (the latter of which is sent occasionally by the server to avoid timeout).
All messages by the client will be ignored.
To create games:
#Standard game
POST /games/actions/create {
"siteAuth":<SiteAuth>,
"tc":<TimeControl>,
"gUser":<Username>, #OPTIONAL - specify a fixed user to play this side
"sUser":<Username>, #OPTIONAL - specify a fixed user to play this side
"rated":<Bool>,
"gameType":"standard"
}
#Handicap game
#Time controls are separate, and setup moves including fewer than the full set of 16 pieces are legal.
POST /games/actions/create {
"siteAuth":<SiteAuth>,
"gTC":<TimeControl>,
"sTC":<TimeControl>,
"gUser":<Username>, #OPTIONAL - specify a fixed user to play this side
"sUser":<Username>, #OPTIONAL - specify a fixed user to play this side
"gameType":"handicap"
}
#Game starting at a specific position (TODO - unimplemented)
POST /games/actions/create {
"siteAuth":<SiteAuth>,
"gTC":<TimeControl>,
"sTC":<TimeControl>,
"position":<BoardPosition>,
"toMove":<String>, #Next player to move, either "g" or "s"
"gameType":"directsetup"
}
If successful: {"gameID":<GameID>,"gameAuth"<GameAuth>}
Generally, clients should follow a game creation immediately with a GET /games/<GameID>/state?minSequence=<INT>
query in order to watch for other users joining. The creator of the game need not join the game, and the open game
will be cleaned up if the creator leaves.
Additionally, the creator of a game should begin providing heartbeats via the heartbeat
query below. Failing to send heartbeats and timing out will result in the open game being cleaned up.
To join an open game:
POST /games/<GameID>/actions/join {"siteAuth":<SiteAuth>}
If successful: {"gameAuth":<GameAuth>}
Generally, clients should follow a game joining immediately with a GET /games/<GameID>/state
and/or GET /games/<GameID>/state?minSequence=<INT> query in order to watch for the game starting, or
to watch for a decline of the join.
For a user-created game, the game will immediately start upon the other side sending a accept query,
and the joining user does not need to send a accept query (although sending it anyways is harmless).
For a non-user-created game (the creator field is absent from <OpenGameData>), both joining users
must send a accept query to start the game.
After joining, a client should begin providing heartbeats via the heartbeat query below. Failing to
send heartbeats and timing out will result in leaving the game.
To leave a game:
POST /games/<GameID>/actions/leave {"gameAuth":<GameAuth>}
If successful: <Message>
Leaving a game will leave the game for all gameAuths for this game for this user.
To accept and begin a game with another player that has joined:
POST /games/<GameID>/actions/accept {"gameAuth":<GameAuth>, "opponent":<Username>}
If successful: <Message>
The "opponent" field should be the opponent that the current user is accepting to play.
For a user-created game, only the creator of the game needs to accept to start the game.
For a non-user-created game, both joining users must send accept query to start the game.
To decline a join of a game from another player:
POST /games/<GameID>/actions/decline {"gameAuth":<GameAuth>, "opponent":<Username>}
If successful: <Message>
The "opponent" field should be the opponent that the creator of the game is declining to play.
To send a heartbeat:
POST /games/<GameID>/actions/heartbeat {"gameAuth":<GameAuth>}
If successful: <Message>
NOTE: When the user is present in an active or open game, clients should send a heartbeat more frequently than every 15 seconds to indicate the presence of the user. This is used for the "gPresent" and "sPresent" fields in an active game, as well as for cleaning up open games or join requests upon timeout.
To resign a game:
POST /games/<GameID>/actions/resign {"gameAuth":<GameAuth>}
If successful: <Message>
Games resigned or otherwise lost where the losing player never made a single move will not be counted for statistics or ratings (even if the game would otherwise be marked as rated). See "countForStats" and "includeUncounted" fields/parameters above.
To make a move:
POST /games/<GameID>/actions/move {
"gameAuth":<GameAuth>,
"move":<String>, #ex: "Ra1n Ra2n Ra3e Rb3e Rc3x"
"plyNum:<Int> #1g = 0, 1s = 1, 2g = 2, 2s = 3, ...
}
If successful: <Message>
Moves should be sent in standard Arimaa notation (http://arimaa.com/arimaa/learn/notation.html). A move of "resign" and "resigns" are both alternative ways to resign in addition to the /games/<GameID>/actions/resign query. Undos/takebacks are not supported.
Games resigned or otherwise lost where the losing player never made a single move will not be counted for statistics or ratings (even if the game would otherwise be marked as rated). See "countForStats" and "includeUncounted" fields/parameters above.
-
A single line of chat:
<ChatLine> = { "id":<Long>, #A per-channel identifier for this line of chat that increments per line "channel":<ChatChannel>, "username":<Username>, "timestamp":<Timestamp>, "event":<String>, #"msg", "join", "leave", "timeout" - indicates what happened "label":<String>, #OPTIONAL - used for things like the turn number at the time of the message for in-game chat. "text":<String> #OPTIONAL - only exists for "msg" events. The text the user wrote. }
- To join a chat channel:
POST /chat/<ChatChannel>/join {"siteAuth":<SiteAuth>}
If successful:{"chatAuth":<ChatAuth>}
- To leave a chat channel:
POST /chat/<ChatChannel>/leave {"chatAuth":<ChatAuth>}
If successful:<Message>
- To send a heartbeat to a chat channel:
POST /chat/<ChatChannel>/heartbeat {"chatAuth":<ChatAuth>}
If successful:<Message>
Clients should heartbeat more frequently than once every 2 minutes to stay connected to a chat channel.
- To post in a chat channel:
POST /chat/<ChatChannel>/post {"chatAuth":<ChatAuth>,"text":<String>}
If successful:<Message>
To get a list of all logged-in users:
POST /chat/<ChatChannel>/usersLoggedIn {}
If successful: {"users":[ShortUserInfo,ShortUserInfo,...]}
-
To get the chat from a channel:
GET /chat/<ChatChannel>/
GET /chat/<ChatChannel>/?minId=<Long>,minTime=<Timestamp>,doWait=<Bool>
If successful:{"lines":[<ChatLine>,<ChatLine>,...]}Parameters
minIdand/orminTimecan be used to filter returned chat lines to only those with at least the specified id or timestamp.minIddefaults to the id of the line 5000 lines before the end of the current chat. Additionally, no more than 5000 lines will be returned in a single call - if more than 5000 lines meet the specified filter conditions, the earliest 5000 lines are returned.If
doWaitis specified and no lines meet the desired filter conditions, then blocks until:- Approximately 15 seconds have passed.
- A new line of chat does meet the desired filter conditions.
- An internal server event happens that causes the query to return despite having no new lines.
Websocket two-way communication interface for chat.
WARNING!!!: Websockets are buggy in the current server due to bugs in the underlying scala web libraries.
Server messages are of the form:
{"chatAuth":<ChatAuth>} #response to any join action
<ChatLine> #whenever a new message is posted to the chat
<Error> #in response to invalid queries or possibly server-side errors
<a blank line> #sent occasionally by server to prevent some timeouts in some cases
Client messages must be of the form:
{"action":"join","text":<SiteAuth>} #join the chat as a specific user
{"action":"post","text":<String>} #post a message to the chat, only legal if successfully joined
Although normal http queries can be performed using the "chatAuth" returned in response to a join message,
it is not necessary to do so when using this interface (for example, the heartbeat query is unnecessary).
- How do we prevent spamming of registration of new accounts?
- Should we add a country field to user information for registration? What format should countries or country codes be in?
- Add queries for finding and viewing stats about users
- General query throttling in appropriate spots
- Asynchronous game creation for postal games. (One player creates game, logs off, then another player accepts.)
- Think about and rework the tags field