Quantum
Bomberman (ARENA)
Step by step instruction of recreation of the project
Set up the project
Set up Quantum
To set up the Quantum App-ID follow the instructions on following link: https://doc.photonengine.com/quantum/current/quantum-100/quantum-101#step_3_create_and_link_a_quantum_appid
May be that instead of App Settings > App Id Realtime
you will have to go App Settings > App Id
.
Tournament-SDK UI implementation
Start by importing Tournament-SDK to your Unity project.
To set up UI to direct player among MainMenu
, TournamentListScreen
and TournamentHubScreen
, first we are going to add necessary changes/additions to the game code.
Prepare Scripts to implement Tournament-SDK UI
UITournamentList
-
Create new script called
UITournamentList
. -
Open new script and copy paste following code that is necessary to direct player from
TournamentListScreen
toMainMenu
orTournamentHubScreen
:using Quantum.Demo; public class UITournamentList : UIScreen<UITournamentList> { public void OnGetBackToMainMenu() { HideScreen(); // Hide Tournament List Screen UIConnect.ShowScreen(); // Show Main Menu screen } public void OnPlayOpenTournamentHub() { HideScreen(); // Hide Tournament List Screen UITournamentHub.ShowScreen(); // Show Tournament Hub Screen } }
UITournamentHub
-
Create new script called
UITournamentHub
. -
Open new script and copy paste following code that is necessary to direct player from
TournamentHubScreen
toTournamentListScreen
:using Quantum.Demo; public class UITournamentHub : UIScreen<UITournamentHub> { public void OnGetBackToTournamentList() { HideScreen(); // Hide Tournament Hub Screen UITournamentList.ShowScreen(); // Show Tournament List Screen } }
Prepare code to implement Tournament-SDK UI
-
Open script called
UIConnect
. -
Add variable to store
GUITournamentHubScreen
crucial for tournament initialization:[SerializeField] private GUITournamentHubScreen TournamentHubScreen;
-
Add two following methods:
// Directs player from MainMenu to TournamentListScreen public void OnTournamentClicked() { HideScreen(); // Hide MainMenu UITournamentList.ShowScreen(); // Show Tournament List Screen } // Method that will redirect player back to the TournamentHubScreen after the tournament match has been finished public void ReturnToTournament(long tournamentId) { HideScreen(); // Hide MainMenu TournamentHubScreen.Initialize(tournamentId); // Initialize tournament using tournamentId saved from last played tournament UITournamentHub.ShowScreen(); // Show Tournament Hub Screen }
Add UI
Tournament Button
-
In
Hierarchy
go toUICanvas > Menu > LayoutHorzontal > VerticalLayout > Content > ConnectPanel
. -
Duplicate last element called
ReconnectButton
. -
Rename it to
TournamentButton
. -
Go
TournamentButton > ButtonText
and changeText
component toTOURNAMENTS
.
Tournament List & Tournament Hub
-
Add two new objects to
UICanvas
scene. -
Rename them to
TournamentListUI
andTournamentHubUI
. -
Drag&Drop
TournamentListScreen
andTournamentHubScreen
respectively. -
Make sure
TournamentListUI > TournamentListScreen > SubScreen
is Enabled. -
Find
Rect transform
component and resetLeft
/Right
/Bottom
/Top
to 0. -
Set scale for
X
/Y
/Z
to 1.
Set Up UI
-
Add newly created scripts
UITournamentList
andUITournamentHub
toMenu
object inHierarchy
to make them accessible. -
Drag&Drop
TournamentListUI
intoUITournamentList > Panel
component. -
Drag&Drop
TournamentHubUI
intoUITournamentHub > Panel
component.
TournamentButton
To make TournamentButton
direct player from MainMenu
to TournamentListScreen
:
-
Go to
UICanvas > Menu > LayoutHorzontal > VerticalLayout > Content > ConnectPanel > TournamentButton
. -
In
Button
component change firstOnClick
action fromUIConnect.OnReconnectClicked
toUIConnect.OnTournamentClicked
.
TournamentList to TournamentHub
To allow TournamentList
direct player to TournamentHub
:
-
Drag&Drop
TournamentHubUI > TournamentHubScreen > SubScreen
toTournamentListUI > TournamentListScreen > SubScreen
inGUITournamentListScreen > TournamentHubScreen
component. -
Go to
UICanvas > Menu > TournamentListUI > TournamentListScreen > SubScreen > Canvas > Scroll View > Viewport > Content > TournamentListItem > ButtonUnderline(Open)
findButton
component. -
Add new
OnClick()
action, drag&dropMenu
into empty field underRuntime Only
, changeNo Function
toUITournamentList.OnPlayOpenTournamentHub
-
Repeat this process for
TournamentHubUI
as well.
TournamentList to MainMenu
To allow TournamentList
direct player back to MainMenu
:
-
Go to
UICanvas > Menu > TournamentListUI > TournamentListScreen > SubScreen > Canvas > Title > ButtonUnderline(Back)
, findButton
component. -
Remove existing action and add three new actions.
-
For first action, drag&drop
Menu
into empty field underRuntime Only
and set it toUITournamentList.OnGetBackToMainMenu
. -
For second action, drag&drop
Directional Light
into empty field underRuntime Only
and set it toGameObject.SetActive
and make sure to enable it. -
For third action, drag&drop
UICanvasCustom
into empty field underRuntime Only
and set it toGameObject.SetActive
and make sure to enable it.
TournamentHub to TournamentList
-
Drag&Drop
TournamentListUI > TournamentListScreen > SubScreen
toTournamentHubUI > TournamentHubScreen > SubScreen
inGUISubScreen > ReturnScreen
component. -
Go to
UICanvas > Menu > TournamentHubUI > TournamentHubScreen > SubScreen > Canvas > Title > ButtonUnderline(Back)
, findButton
component. -
Add new action, drag&drop
Menu
into empty field underRuntime Only
and set it toUITournamentHub.OnGetBackToTournamentList
.
Backbone Integration
To initialize TournamentList
we need to create an object that will do it on every launch of the game.
-
Add new
GameObject
toUICanvas
and rename it toBackboneIntegration
. -
Add
BackboneManager
andResource Cache
components to it. -
Create new script called
BacboneIntegration
and copy paste following code into it.using Gimmebreak.Backbone.User; using Quantum.Demo; using System.Collections; using UnityEngine; using UnityEngine.UI; public class BackboneIntegration : MonoBehaviour { private WaitForSeconds waitOneSecond = new WaitForSeconds(1); [SerializeField] private Button tournamentButton = default; private IEnumerator Start() { // Disable tournament button until user is logged in tournamentButton.interactable = false; // wait until player nick was set (this happens on initial screen) while (string.IsNullOrEmpty(UIConnect.Instance.Username.text)) { yield return this.waitOneSecond; } // keep trying to initialize client while (!BackboneManager.IsInitialized) { yield return BackboneManager.Initialize(); yield return this.waitOneSecond; } // create arbitrary user id (minimum 64 chars) based on nickname // ClientInfo.Username is the nickname you set on the first launch of the game string arbitraryId = "1000000000000000000000000000000000000000000000000000000000000001" + UIConnect.Instance.Username.text; // log out user if ids do not match if (BackboneManager.IsUserLoggedIn && BackboneManager.Client.User.GetLoginId(LoginProvider.Platform.Anonym) != arbitraryId) { Debug.LogFormat("Backbone user({0}) logged out.", BackboneManager.Client.User.UserId); yield return BackboneManager.Client.Logout(); } // log in user if (!BackboneManager.IsUserLoggedIn) { yield return BackboneManager.Client.Login(LoginProvider.Anonym(true, UIConnect.Instance.Username.text, arbitraryId)); if (BackboneManager.IsUserLoggedIn) { Debug.LogFormat("Backbone user({0}) logged in.", BackboneManager.Client.User.UserId); } else { Debug.LogFormat("Backbone user failed to log in."); } } if (BackboneManager.IsUserLoggedIn) { // Enable tournament button, because if user is logged in, // then all the information needed is already available tournamentButton.interactable = true; } } }
-
Get back to the scene and find
BackboneIntegration
component. AddTournamentButton
to the missing field in component.
Implementing Tournament logic into the Quantum game
Quantum code changes/additions
To succesfully merge Tournament logic into Quantum game, let's first prepare game to handle/carry/mantain tournament information inside the game.
-
Open Quantum part of the project. Go to project location, there must be folder called
quantum_code
open it as a project in an editor. -
Open
RuntimePlayer.User
and copy paste following code into it:using Photon.Deterministic; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Quantum { partial class RuntimePlayer { public ColorRGBA Color; // Variables to store user's identificators for tournament public long TournamentUserId; public byte TournamentTeamId; partial void SerializeUserData(BitStream stream) { // implementation stream.Serialize(ref Color.R); stream.Serialize(ref Color.G); stream.Serialize(ref Color.B); stream.Serialize(ref Color.A); stream.Serialize(ref TournamentUserId); stream.Serialize(ref TournamentTeamId); } } }
-
Go to
gameSession.qtn
and add following variable to component to store list of players who have been destroyed by bomb explosion during the match. -
Set list as
[AllocateOnComponentAdded]
which will allocate it on every simulation.[AllocateOnComponentAdded] list<PlayerRef> Placements;
-
Find
BomberSystem
and copy paste following code into it:using System.Collections.Generic; namespace Quantum { public unsafe class BomberSystem : SystemMainThreadFilter<BomberSystem.BomberFilter> { public struct BomberFilter { public EntityRef Entity; public Bomber* Bomber; public Transform2D* Transform; } public override void Update(Frame f, ref BomberFilter filter) { var gridPosition = filter.Transform->Position.RoundToInt(Axis.Both); var isInvincible = false; #if DEBUG // Used for debugging purposes isInvincible = f.RuntimeConfig.IsInvincible; #endif if (isInvincible == false && f.Grid.GetCellPtr(gridPosition)->IsBurning) { // Death animation is triggered from OnEntityDestroyed // Add to list before destroy in order to get access to players info after match has finished to process tournament results foreach (var (entity, component) in f.GetComponentIterator<GameSession>()) { f.ResolveList(component.Placements).Add(f.Get<PlayerLink>(filter.Entity).Id); } f.Destroy(filter.Entity); } } } }
-
Now to apply added changes re-build the quantum project.
Game code changes/additions
UIConnecting
contains method OnConnectedToMaster()
that starts the game, in order to start a tournament game all players must have same room paramenters.
-
Find
OnConnectedToMaster()
method and change it as shown below, so on every launch it will check if Room name has been set and connect to specific Room.public void OnConnectedToMaster() { if else ... if else if ... // Above we have unchanged part of the code var joinRandomParams = new OpJoinRandomRoomParams(); _enterRoomParams = new EnterRoomParams(); _enterRoomParams.RoomOptions = new RoomOptions(); _enterRoomParams.RoomOptions.IsVisible = true; _enterRoomParams.RoomOptions.MaxPlayers = (byte)(TournamentMatchHandler.MaxUserTournament != 0 ? TournamentMatchHandler.MaxUserTournament : Input.MAX_COUNT); _enterRoomParams.RoomOptions.Plugins = new string[] { "QuantumPlugin" }; _enterRoomParams.RoomOptions.CustomRoomProperties = new Hashtable { { "HIDE-ROOM", false }, { "MAP-GUID", defaultMapGuid }, }; _enterRoomParams.RoomOptions.PlayerTtl = PhotonServerSettings.Instance.PlayerTtlInSeconds * 1000; _enterRoomParams.RoomOptions.EmptyRoomTtl = PhotonServerSettings.Instance.EmptyRoomTtlInSeconds * 1000; Debug.Log("Starting random matchmaking"); _enterRoomParams.RoomName = string.IsNullOrEmpty(TournamentMatchHandler.RoomName) ? null : TournamentMatchHandler.RoomName; // Below we have unchanged part of the code if }
UIGame
is responsible to track game state. -
Add
Disconnect
button to it, to disable it, so players couldn't spoil the process of the tournament game:// Variable that represents Disconnect button [SerializeField] private UnityEngine.UI.Button OnLeaveButton;
-
Change
Update()
method to:public void Update() { if (QuantumRunner.Default != null && QuantumRunner.Default.HasGameStartTimedOut) { UIDialog.Show("Error", "Game start timed out", () => { UIMain.Client.Disconnect(); }); } // Disable Disconnect button if tournament game if (TournamentMatchHandler.TournamentGame) OnLeaveButton.interactable = false; }
UIGameStats
is responsible for the end of the game -
Add variable to track if result procession started:
private bool resultProcessingStarted = false;
-
Then in
UpdateUI
method changeEnding
case to start result processing, right before finishing the game:case GameSessionState.Ending: if (TournamentMatchHandler.TournamentGame && !this.resultProcessingStarted) { StartCoroutine(ProcessResult(frame, gameSession.Winner)); } else if (!TournamentMatchHandler.TournamentGame) { _gameStateMessageTMP.text = gameSession.Winner.IsValid ? $"Player {gameSession.Winner._index} won!" : "DRAW!"; var timeUntilDisconnection = timer.GetRemainingTime(frame).AsFloat; // If more than 60 seconds are left until disconnection, write out counter in min + sec _timerTMP.text = timeUntilDisconnection > 60 ? $"Disconnection in {(int)timeUntilDisconnection / 60} min {(int)timeUntilDisconnection % 60} seconds" : $"Disconnection in {(int)timeUntilDisconnection} seconds"; _gameStateTMP.text = "Game Over"; if (timer.HasExpired(frame)) UIMain.Client.Disconnect(); } break;
-
Add new method called
ProcessResult
:private IEnumerator ProcessResult(Frame frame, PlayerRef winner) { this.resultProcessingStarted = true; List<Gimmebreak.Backbone.GameSessions.GameSession.User> users = new List<Gimmebreak.Backbone.GameSessions.GameSession.User>(); Dictionary<long, byte> results = new Dictionary<long, byte>(); Gimmebreak.Backbone.GameSessions.GameSession gameSession; List<PlayerRef> prs = new List<PlayerRef>(); foreach (var (entity1, component) in frame.GetComponentIterator<GameSession>()) { foreach (PlayerRef pr in frame.ResolveList(component.Placements)) { prs.Add(pr); } } prs.Add(winner); foreach (PlayerRef pr in prs) { RuntimePlayer runtimePlayer = frame.GetPlayerData(pr); users.Add(new Gimmebreak.Backbone.GameSessions.GameSession.User( runtimePlayer.TournamentUserId, runtimePlayer.TournamentTeamId) { Place = runtimePlayer.TournamentUserId == frame.GetPlayerData(winner).TournamentUserId ? 1 : 2 }); if (runtimePlayer.TournamentUserId == frame.GetPlayerData(winner).TournamentUserId) results.Add(runtimePlayer.TournamentUserId, 1); else results.Add(runtimePlayer.TournamentUserId, 0); } // Create new GameSession gameSession = new Gimmebreak.Backbone.GameSessions.GameSession( (long)UIMain.Client.CurrentRoom.CustomProperties["GameSessionId"], 0, users, (long)UIMain.Client.CurrentRoom.CustomProperties["TournamentMatchId"]); // Attach users and their results to current GameSession gameSession.Users.ForEach(user => { gameSession.AddStat(1, user.UserId, results[user.UserId]); }); //report game session yield return BackboneManager.Client.SubmitGameSession(gameSession); //refresh tournament data yield return BackboneManager.Client.LoadTournament((long)UIMain.Client.CurrentRoom.CustomProperties["TournamentId"]); // Leave session after result was submited TournamentMatchHandler.TournamentGame = false; UIConnect.Instance.ReturnToTournament((long)UIMain.Client.CurrentRoom.CustomProperties["TournamentId"]); UIMain.Client.Disconnect(); }
As we want to disable button in
UIGame
, we need to attach button to it. -
Go to
UICanvas > Game
there is empty fieldOnLeaveButton
, drag&dropUICanvas > Game > Panel > DisconnectButton
into empty field.
Implementing Tournament Match Handler
-
In
Hierarchy
go toUICanvas > TournamentHubUI > TournamentHubScreen > Canvas
at the very bottom of it, create a new game object and rename it toTournamentMatchHandler
. -
Create new script called
TournamentMatchHandler
and add this script as component toTournamentMatchHandler
object. -
Copy paste following code into it:
using ExitGames.Client.Photon; using Gimmebreak.Backbone.Core; using Gimmebreak.Backbone.Tournaments; using Photon.Realtime; using Quantum; using Quantum.Demo; using System.Collections; using System.Collections.Generic; using System.Data; using System.Linq; using UnityEngine; public class TournamentMatchHandler : TournamentMatchCallbackHandler, IInRoomCallbacks { Tournament tournament; TournamentMatch tournamentMatch; ITournamentMatchController tournamentMatchController; bool sessionStarted; bool creatingSession; private WaitForSeconds waitOneSec = new WaitForSeconds(1); private string tournamentSessionName; public static byte MaxUserTournament = 0; public static string RoomName = ""; public static bool TournamentGame = false; public override void OnJoinTournamentMatch(Tournament tournament, TournamentMatch match, ITournamentMatchController controller) { // User is requesting to join a tournament match, create or join appropriate session this.tournament = tournament; this.tournamentMatch = match; this.tournamentMatchController = controller; this.sessionStarted = false; this.creatingSession = false; this.tournamentSessionName = $"{this.tournamentMatch.Secret}_{this.tournamentMatch.CurrentGameCount}"; // Join Photon session StartCoroutine(JoinRoomRoutine()); } public void OnDisable() { if (UIMain.Client != null) { UIMain.Client.RemoveCallbackTarget(this); } } private IEnumerator JoinRoomRoutine() { while (this.tournamentMatch != null) { // Use ConnectedStatus instead of UIMain.Client.State, because Client is only created after UIConnect.Instance.OnConnectClicked(); // If you require specific region for tournament, you can use // tournament custom properties providing the info about required region. // string cloudRegion = this.tournament.CustomProperties.Properties["cloud-region"]; // If tournament match is finished then leave if (this.tournamentMatch.Status == TournamentMatchStatus.MatchFinished || this.tournamentMatch.Status == TournamentMatchStatus.Closed) { // Check if connected session is for finished match if (UIMain.Client != null && UIMain.Client.State == ClientState.Joined && UIMain.Client.CurrentRoom.Name == this.tournamentSessionName) { UIMain.Client.Disconnect(); } } // Try to connect to tournament match session else if (!(UIMain.Client != null && UIMain.Client.State == ClientState.Joined)) { // Set player propery with UserId so we can identify users in session // Set max players for session based on tournament phase setting MaxUserTournament = (byte)(this.tournament.GetTournamentPhaseById(this.tournamentMatch.PhaseId).MaxTeamsPerMatch * this.tournament.PartySize); // Join or create Photon session with tournamemnt match secret as session id RoomName = this.tournamentSessionName; PlayerDataContainer.Instance.RuntimePlayer.TournamentUserId = BackboneManager.Client.User.UserId; PlayerDataContainer.Instance.RuntimePlayer.TournamentTeamId = this.tournamentMatch.GetMatchUserById(BackboneManager.Client.User.UserId).TeamId; UIConnect.Instance.OnConnectClicked(); ExitGames.Client.Photon.Hashtable customProperties = new ExitGames.Client.Photon.Hashtable() { { "TournamentUserId", BackboneManager.Client.User.UserId}, { "TournamentTeamId", this.tournamentMatch.GetMatchUserById(BackboneManager.Client.User.UserId).TeamId} }; UIMain.Client.AddCallbackTarget(this); TournamentGame = true; UIMain.Client.LocalPlayer.SetCustomProperties(customProperties); } // If we are in wrong session then leave else if (UIMain.Client != null && UIMain.Client.State == ClientState.Joined && this.tournamentSessionName != UIMain.Client.CurrentRoom.Name) { UIMain.Client.Disconnect(); } yield return this.waitOneSec; } } public override bool IsConnectedToGameServerNetwork() { // Check if user is connected to photon and ready to join a session if (UIMain.Client != null && UIMain.Client.State == ClientState.Joined) { return true; } return false; } public override bool IsGameSessionInProgress() { // Check if game session has started return sessionStarted; } public override bool IsUserConnectedToMatch(long userId) { // Check if tournament match user is connected to session if (UIMain.Client.CurrentRoom == null) return false; foreach (var player in UIMain.Client.CurrentRoom.Players.Values) { if (player.CustomProperties.ContainsValue(userId)) { return true; } } return false; } public override bool IsUserReadyForMatch(long userId) { // In particular case if player is connected to the match it's considered to be ready return IsUserConnectedToMatch(userId); } public override void OnLeaveTournamentMatch() { this.tournament = null; this.tournamentMatch = null; this.tournamentMatchController = null; TournamentGame = false; UIMain.Client?.RemoveCallbackTarget(this); UIMain.Client?.Disconnect(); } public override void StartGameSession(IEnumerable<TournamentMatch.User> checkedInUsers) { // Start tournament game session with users that checked in. // Be aware that this callback can be called multiple times until // sessionStarted returns true. // Check if session has started if (sessionStarted) { return; } // Check if session is not being requested if (!this.creatingSession) { this.creatingSession = true; // Create tournament game session BackboneManager.Client.CreateGameSession( checkedInUsers, this.tournamentMatch.Id, 0) .ResultCallback((gameSession) => { this.creatingSession = false; // Check if game session was created if (gameSession != null) { // Indicate that session has started this.sessionStarted = true; if (UIMain.Client.LocalPlayer.IsMasterClient) { ExitGames.Client.Photon.Hashtable hashtable = new ExitGames.Client.Photon.Hashtable(); hashtable.Add("GameSessionId", gameSession.Id); hashtable.Add("TournamentMatchId", this.tournamentMatch.Id); hashtable.Add("TournamentId", this.tournament.Id); UIMain.Client.CurrentRoom.SetCustomProperties(hashtable); UIRoom.Instance.OnStartClicked(); } UITournamentHub.HideScreen(); } }) .Run(this); } } public void OnPlayerEnteredRoom(Player newPlayer) { if (this.tournamentMatchController != null) { // Report user who joined room this.tournamentMatchController.ReportJoinedUser((long)newPlayer.CustomProperties["TournamentUserId"]); } } public void OnPlayerLeftRoom(Player otherPlayer) { { // Report user who disconnected from room this.tournamentMatchController.ReportDisconnectedUser((long)otherPlayer.CustomProperties["TournamentUserId"]); } } public void OnPlayerPropertiesUpdate(Player targetPlayer, ExitGames.Client.Photon.Hashtable changedProps) { if (this.tournamentMatchController != null) { // Reporting status change will refresh match metadata this.tournamentMatchController.ReportStatusChange(); } } public void OnRoomPropertiesUpdate(ExitGames.Client.Photon.Hashtable propertiesThatChanged) { if (this.tournamentMatchController != null) { // Reporting status change will refresh match metadata this.tournamentMatchController.ReportStatusChange(); } } public void OnMasterClientSwitched(Player newMasterClient) { } }
-
Get back to Unity project, find
UICanvas > TournamentHubUI > TournamentHubScreen > SubScreen > Canvas > ActiveMatchContainer
, find componentGUITournamentActiveMatch
there will be empty fieldMatchHandler
drag&dropTournamentMatchHandler
into it.
Tournament creation & Final test
Build project
We need at least 2 players to be sure that project is working fine, so build project to create 2nd player.
Don't start it immediately, wait till we create a tournament, otherwise tournament won't be visible on TournamentListScreen
.
Creating tournament template
Creating the template
-
There you will see
No tournament templates .Create your first template to get started
. -
Press
Create your first template
.
Edit template
-
Tournament template will appear. Select
Edit template
. -
Go to
Description
and setTournament Name
. -
Go to
Registration
set:
-
Maximum players
- 2 -
Party(team) size
- 1 -
Registration rules
->Open to everyone
- Go to
Format/Add Phase
In Format
set:
Teams
- 2Min teams per match
- 2Max Teams per match
- 2
Leave field Max loses
in Scores
empty
In Rounds
set:
Type
- BO3Minimum game time (minutes)
- 2Maximum round time (minutes)
- 8
-
On the bottom of the screen you should see
Careful - you have unsaved changes!
, pressSave Changes
.
Start tournament
-
Get back to
Tournament templates
page. -
Press
Schedule
, setTime
toYour current time + 5 minutes
and pressStart tournament
.
Final test
-
Now get back to Unity and start the project.
-
Press
Tournaments
button that was added earlier. You should seeTournamentListScreen
and the tournament that you just added.Tournament may be unavailable to register for some time, wait until
Sign up
button is available to register to tournament. -
Repeat the process in build version
-
When tournament will start, button
Ready to play
will become available. -
Press on both Unity and Build
Ready to play
. -
Finish the match on both players.
-
Get back to: https://www.tournament-sdk.com/schedule
-
You should see tournament.
-
Click on it, to see more information about the tournament and played matches.
-
To check more detailed information about every match played during the tournament, go to
Phase 1
. -
Press
Show matches
to the right from any participant. -
Click
Show details
for more information. -
Click
Game #ID
andStats
for more information.