2017년 4월 20일 목요일

MultiPlayer Rock Paper Scissors on FireBaseDB


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.


global FirebaseDB tags.png


TinyDB tags

CURRENTPLAYERID
CURRENTMATCHID
MATCHES - a list of this owner’s matchIDs, hosted or not

TinyDB Tags.png

App Dialogs

Initialization

globals


Screen1.Initialize
Screen1_Initialize.png
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
flush_all_fdbs.PNG


set_PlayerID
set_PlayerID.png


This user’s PlayerID is kept in TinyDB.  If there is no PlayerID available, expose the Register Horizontal Arrangement.


get_PlayerID
get_PlayerID.png


Status header: current playerID, newsfeed, selected matchID,  

Player Login Designer


Login / Register

Designer Dialog for Registering
Designer_Register_Dialog.png

btnRegister.Click


btnRegister_Click.png
scrub procedure
scrub.png
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


fdbGetLogin_GotValue.png
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


new_PlayerID.png


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.


References: log_signin, set_PlayerID.
log_signin


A last_login_date datetime value is kept to allow tracking and cleanup of dead PlayerIDs.
log_signin.png
login date tag values


Player FDB Tags.png
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
btnHideRegister.png


A Hide button in the Register Arrangement allows the user to hide it until he requests a new PlayerID.


Menu button

btnMenuClick.png


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
HaveGames.png
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

DesignerMenu.png



btnMyGames.Click

btnMyGames_Click.png

lpkSelectGame.BeforePicking

lpkSelectGame_BeforePicking.png

lpkSelectGame.AfterPicking

lpkSelectGame_AfterPicking.png

Reference: setMatchID

btnJoin.Click

btnJoin_Click.png
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
fdbGetAllPending_GotValue.png


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
lvwJoinGame_AfterPicking.png
A ListView Selection is forced to be text, so it has to be split and stripped to extract the gameID.
References:  joinMatch, get_PlayerID.

btnNewGame.Click

btnNewGame_Click.png

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_GotValue.png

fdbGameMonitor.DataChanged

fdbGameMonitor_DataChanged.png

showJSONmatch

showJSONmatch.png
References: summary, playByPlay, getMatchID.

summary



summary.png
Called by: showJSONmatch.
References: clause.


clause
clause.png
A game summary consists of  series of clauses, each with its own subkey of a match tree and a description.


Called by: summary.


playByPlay

playByPlay.png
Called by: showJSONmatch.
References: extract_players,
extract_players
extract_players.png


Called by: playByPlay.


rounds
rounds.png
Called by: playByPlay


get_winning_moves
get_winning_moves.png
Called by: playByPlay.


round
round.png
Called by: playByPlay.


announce_round
announce_round.png
Called by: playByPlay.

announce_incomplete_round
announce_incomplete_round.png


Called by: playByPlay.

ItsMyMove
ItsMyMove.png

didHePlay
didHePlay.png




announce_complete_round
announce_complete_round.png


Called by: announce_round


whatDidHePlay
whatDidHePlay.png


judge
judge.png






Rules
rules.png
Rules - RPS
Rules - RPS.png
Rules - RPSSL
Rules - RPSSL.png

Initiate a match

select game type, target rounds, target wins

Designer layout - Initiate a match
Designer - Initiate Game Layout.png


Designer components - Initiate a match
Designer_Initiate_Game_Components.png


Select a Game Type


Game Type Selection screenshot.png

Game Type List Picker blocks
Game Type Selection Blocks.png


Game Type Blurb Lookup
Blurb Lookup blocks.png

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).


getBlurb.png


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


createMatchID.png


yyyyMMDDhhmmss
yyyyMMDDhhmmss.png


setMatchID


setMatchID.png
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
matchBucket.png
This bucket value homes in our db monitor to the current match.


getMatchID
getMatchID.png

Saving a pending match to FireBaseDB

pendingMatchfdbKey
The pending matches FireBaseDB path, based on RPSSL.


pendingMatchfdbKey.png
hostMatchFDBKey
hostMatchfdbKey.png
This assumes a base bucket of RPSSL.


putPendingMatch
putPendingMatch.png
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
putHostMatch.png
references: hostMatchFDBKey


btnInitiate
btnInitiate.png
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

putPendingMatch.png
References: pendingMatchfdbKey


joinMatch

joinMatch.png
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.add_game.png


hostMatchfdbKey


Reference: hostMatchfdbKey.png
hostPID
hostPID.png
The host player ID of a match is kept after the “-” in the matchID.
fdbPostJoin.GotValue
fdbPostJoin_GotValue.png
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


enoughPlayers.png
matchJoined
matchJoined.png


alertPlayers
alertPlayers.png
alertPlayer
This serves to alert a player by inserting a matchID and timestamp into his newsfeed.alertPlayer.png


make first move

Review closed matches

delete closed matches

quit

댓글 없음:

댓글 쓰기