2/26/2016
Abe Getzler
The objective of this exercise is to test FireBase DB’s suitability as an AI2 vehicle to run a multiplayer game like Rock Paper Scissors (Rochambeau).
GameTypes
⦁ RPS
⦁ ⦁ name: “Rock Paper Scissors”
⦁ ⦁ blurb
⦁ ⦁ moves : [Rock, Paper, Scissors]
⦁ ⦁ winning moves
⦁ ⦁ ⦁ Rock
⦁ ⦁ ⦁ ⦁ Scissors : crushes
⦁ ⦁ ⦁ Paper
⦁ ⦁ ⦁ ⦁ Rock : covers
⦁ ⦁ ⦁ Scissors
⦁ ⦁ ⦁ ⦁ Paper : cuts
⦁ RPSSL
⦁ ⦁ blurb
⦁ ⦁ moves : [Rock, Paper, Scissors, Spock, Lizard]]
⦁ ⦁ winning moves
⦁ ⦁ ⦁ Rock
⦁ ⦁ ⦁ ⦁ Scissors : crushes
⦁ ⦁ ⦁ ⦁ Lizard: crushes
⦁ ⦁ ⦁ Paper
⦁ ⦁ ⦁ ⦁ Rock : covers
⦁ ⦁ ⦁ ⦁ Spock: disproves
⦁ ⦁ ⦁ Scissors
⦁ ⦁ ⦁ ⦁ Paper : cuts
⦁ ⦁ ⦁ ⦁ Lizard : decapitates
⦁ ⦁ ⦁ Spock
⦁ ⦁ ⦁ ⦁ Scissors : smashes
⦁ ⦁ ⦁ ⦁ Rock : vaporizes
⦁ ⦁ ⦁ Lizard
⦁ ⦁ ⦁ ⦁ Paper : eats
⦁ ⦁ ⦁ ⦁ Spock : poisons
Game Types could be kept on the server, to allow for new game types being introduced from the server side. (Not implemented yet.)
Design methodology
To avoid conflicts from simultaneous updates; we will try to follow these rules on the server side
- never store summaries (counts, maxima, sums, analyses)
- never update anything in place unless you own it and no one else reads it.
- never keep lists, only subtags.
- only insert.
Also, because a FirebaseDB query with a unique tag will return as a value the JSON of everything under that tag (but not the tag), we will include a duplicate of the unique parent ID under the subtag ID, to make it accessible through the lookup-in-pairs block.
Server contents:
PlayerNames
⦁ Player name (unique, scrub blanks and quotes)
⦁ ⦁ ID : playerID
⦁ ⦁ last login datetime
⦁ ⦁ ⦁ current match ID
⦁ ⦁ ⦁ challenge question (not yet implemented)
⦁ ⦁ ⦁ answer (not yet implemented)
⦁ ⦁ ⦁ newsfeed
⦁ ⦁ ⦁ ⦁ matchID1 : last move YYYYMMDDHHmmss
⦁ ⦁ ⦁ ⦁ matchID2 : last move YYYYMMDDHHmmss
⦁ ⦁ ⦁ matches
⦁ ⦁ ⦁ ⦁ match ID YYYYMMDDHHmmss-Initiator
⦁ ⦁ ⦁ ⦁ ⦁ matchID : matchID (duplicated for convenience in JSON extract handling)
⦁ ⦁ ⦁ ⦁ ⦁ game type
⦁ ⦁ ⦁ ⦁ ⦁ target rounds : 3
⦁ ⦁ ⦁ ⦁ ⦁ target players : 2
⦁ ⦁ ⦁ ⦁ ⦁ players
⦁ ⦁ ⦁ ⦁ ⦁ ⦁ player 1 ID : true
⦁ ⦁ ⦁ ⦁ ⦁ ⦁ player 2 ID : true
⦁ ⦁ ⦁ ⦁ ⦁ ⦁ ...
⦁ ⦁ ⦁ ⦁ ⦁ current round : 3
⦁ ⦁ ⦁ ⦁ ⦁ last move YYYYMMDDHHmmss (for cleanup)
⦁ ⦁ ⦁ ⦁ ⦁ rounds
⦁ ⦁ ⦁ ⦁ ⦁ ⦁ 1
⦁ ⦁ ⦁ ⦁ ⦁ ⦁ ⦁ player 1 ID: move
⦁ ⦁ ⦁ ⦁ ⦁ ⦁ ⦁ player 2 ID: move
⦁ ⦁ ⦁ ⦁ ⦁ ⦁ 2
⦁ ⦁ ⦁ ⦁ ⦁ ⦁ ⦁ player 1 ID: move
⦁ ⦁ ⦁ ⦁ ⦁ ⦁ ⦁ player 2 ID: move
⦁ ⦁ ⦁ ⦁ ⦁ ⦁ …
- ...
Players get to choose their own name, mirrored in TinyDB. Names must be registered on the FireBaseDB server to insure uniqueness. The challenge question and answer are filled in at registration time to allow the player to reclaim his Player Name into TinyDB on a new device without being rejected as a duplicate on the server side.
Players get to play multiple matches simultaneously, since opponents might be scattered world-wide.
(TODO: The newsfeed system might be unnecessary, if players monitor their current game directly in its host player subtree.)
To allow each player to have to monitor only one FirebaseDB key, the newsfeed subkey of each player ID has subkeys for each match that might require his attention. Other players’ apps insert match IDs and last move timestamps into the news feeds of their opponents after they make moves, to trigger opponents’ Data Changed events. This is a broadcast model.
Matches
⦁ pending
⦁ ⦁ match ID YYYYMMDDHHmmss-Initiator
⦁ ⦁ ⦁ game type (RPS/RPSSL)
⦁ ⦁ ⦁ target rounds : 3
⦁ ⦁ ⦁ target players : 2
⦁ ⦁ ⦁ players
⦁ ⦁ ⦁ ⦁ player 1 ID : true
⦁ ⦁ ⦁ ⦁ ⦁ ...
⦁ ⦁ ⦁ ⦁ ...
⦁ running
⦁ ⦁ match ID YYYYMMDDHHmmss-Initiator : true
⦁ ⦁ match ID YYYYMMDDHHmmss-Initiator : true
⦁ ⦁ ...
There are two legs to the Matches branch: pending and running, of interest to people who want to join or watch a match, respectively.
Match IDs are designed to insure uniqueness (no guids are available), for chronological cleanup, and for possible filtering by initiator player ID. Pending matches do not yet have the required minimum number of players to start. Once a pending match has enough players, the app of the last player to join transfers it to the Running section and removes it from the Pending section.
Once a match completes, the last player to move removes it from the running branch.
The Initiator of a match stores the match information under his PlayerID, and he and the other Players insert their moves into that match tree as the game progresses, and monitor that subtree if they are playing or watching that match.
FireBaseDB Tags and Subtags
To allow FirebaseDB to return JSON strings for tags with subtags (/ separator),
we have to exclude spaces from our Firebase tags. Here I have used underscores and CamelCase to highlight the words in my tags.
TinyDB tags
⦁ CURRENTPLAYERID
⦁ CURRENTMATCHID
⦁ MATCHES - a list of this owner’s matchIDs, hosted or not
App Dialogs
Initialization
globals
Screen1.Initialize
At Initialization time, we want to show the current PlayerID from TinyDB. If there isn’t one, show the Registration fields. We need to save the base FirebaseDB project bucket so that we can extend it later for newsfeed monitoring.
flush_all_fdbs
set_PlayerID
This user’s PlayerID is kept in TinyDB. If there is no PlayerID available, expose the Register Horizontal Arrangement.
get_PlayerID
Status header: current playerID, newsfeed, selected matchID,
Player Login Designer
Login / Register
Designer Dialog for Registering
btnRegister.Click
scrub procedure
Anything that isn’t an upper or lower case letter or number is replaced with ‘_’.
Registering a new PlayerID is a two phase process. All PlayerIDs are trimmed of trailing blanks, and retrieved from the PlayerNames branch of FirebaseDB.
fdbGetLogin.GotValue
If the returned PlayerID from FireBaseDB is blank, it’s a new ID, so we proceed to add it using procedure new_PlayerID. Otherwise we alert the user.
new_PlayerID
Player IDs are stored as subtags under a constant FireBaseDB tag, the global PlayerNames_FDB_TAG. The “/” starts a new JSON subtree . The “true” value is a place holder, to be replaced by subfields later on.
The new PlayerID is taken as the current one, in TinyDB and through procedure log_signin.
log_signin
A last_login_date datetime value is kept to allow tracking and cleanup of dead PlayerIDs.
login date tag values
All tags, both TinyDB and FireBaseDB, are accessed through global variables, to avoid typos and to take advantage of typeblocking at block edit time.
btnHideRegister
A Hide button in the Register Arrangement allows the user to hide it until he requests a new PlayerID.
Menu button
If the user hasn’t yet picked a Player Name, he can’t ask for his running games, join a pending game, or start a new game. The Menu button exposes a Vertical Arrangement with more action buttons. References: get_PlayerID, HaveRunningGames.
HaveGames
The My Running Games button is disabled if the player has no running games. Since a list is expected, we return a default value of an empty list.
Designer Menu arrangement
btnMyGames.Click
lpkSelectGame.BeforePicking
lpkSelectGame.AfterPicking
Reference: setMatchID
btnJoin.Click
The Menu Join button does not itself do a join. It prompts FirebaseDB for a list of pending matches that he can select from and join.
fdbGetAllPending.GotValue
When Firebase comes back with the JSON tree of all pending games, we decode the JSON and load it into a ListView, and make it visible for selection. The Web1.JSONTextDecode block is explained at the MIT web site, http://ai2.appinventor.mit.edu/reference/components/connectivity.html#Web and also see this link for how to navigate a tree: http://ai2.appinventor.mit.edu/reference/other/xml.html.
lvwJoinGame.AfterPicking
A ListView Selection is forced to be text, so it has to be split and stripped to extract the gameID.
btnNewGame.Click
Running Matches awaiting your move
⦁ view completed rounds
⦁ make your move for the current round
Running matches awaiting other player moves
⦁ refresh button
Join a pending match
⦁ view pending matches by type
⦁ select a match
⦁ join the match
Monitor a match
fdbGameMonitor.GotValue
fdbGameMonitor.DataChanged
showJSONmatch
summary
clause
A game summary consists of series of clauses, each with its own subkey of a match tree and a description.
playByPlay
extract_players
rounds
Called by: playByPlay
get_winning_moves
round
announce_round
announce_incomplete_round
ItsMyMove
Called by: announce_incomplete_round
didHePlay
Called by: announce_incomplete_round
announce_complete_round
Called by: announce_round
whatDidHePlay
Called by: announce_complete_round
judge
Called by: announce_complete_round
Rules
Rules - RPS
Rules - RPSSL
Initiate a match
select game type, target rounds, target wins
Designer layout - Initiate a match
Designer components - Initiate a match
Select a Game Type
Game Type List Picker blocks
Game Type Blurb Lookup
Game Type blurbs tell the moves in English. They are stored with the rules for each game type in a list of pairs structure designed for use with the lookup in pairs block.
The Blurb lookup is packaged in a value procedure, because it will probably happen again when it’s time to choose a move. Notice how the lookup proceeds in opposite order (inner to outer) to the nesting of the lookup table (Rules).
Generating a unique gameID for a new game
For lack of a proper guid provider, we use a sortable datetimestamp combined with the current PlayerID, which should be unique.
We will use this as the new current match ID, accessed through set and get procedures:
createMatchID
yyyyMMDDhhmmss
setMatchID
To set the current match, we update TinyDB, identify it on the Text of it ListPicker, and set the project bucket of the FirebaseDB Game Monitor to watch the match in its owner’s matches area.
matchBucket
This bucket value homes in our db monitor to the current match.
getMatchID
Saving a pending match to FireBaseDB
pendingMatchfdbKey
The pending matches FireBaseDB path, based on RPSSL.
hostMatchFDBKey
This assumes a base bucket of RPSSL.
putPendingMatch
For each pending match, we store its game type, target rounds, and target players, all under its matchID in the Matches/pending/ branch. references: pendingMatchfdbKey
putHostMatch
references: hostMatchFDBKey
btnInitiate
A pending match is stored temporarily in the Matches/pending section as well as under the matches subsection of the initiating player. References: getMatchID, createMatchID, putPendingMatch, putHostMatch, get_PlayerID, joinMatch
putPendingMatch
References: pendingMatchfdbKey
joinMatch
Joining a match is done both at the pending match section and at the home copy of the match under the originating player, and in TinyDB1 tag MATCHES. After joining the match, we double check if the match has enough players to commence in fdbPostJoin.GotValue.
add_game
This adds a matchID to the app’s TinyDB list of matches, for display in the matchID list picker.
hostMatchfdbKey
Reference:
hostPID
The host player ID of a match is kept after the “-” in the matchID.
fdbPostJoin.GotValue
After joining a match, the match is checked if it has enough players to start its first round. The newly looked up match is gotten from Firebase as the entire JSON string under the joined pending matchID. The JSON is decoded into a tree rooted at the matchID. If enough players have joined the match, the pending match is erased from the Matches/pending leg, and added to the Matches/running leg for people who want to watch matches in progress. All the players in this match are alerted by updating their newsfeed timestamp for this match. References: enoughPlayers, matchJoined , alertPlayers,
enoughPlayers
matchJoined
alertPlayers
alertPlayer
This serves to alert a player by inserting a matchID and timestamp into his newsfeed.
make first move
Review closed matches
⦁ delete closed matches
댓글 없음:
댓글 쓰기