Fusion

Karts (Racing)

This tutorial is based on Karts game sample available on https://doc.photonengine.com/fusion/current/game-samples/fusion-karts#download

To run Karts project you need Editor Version 2020.3.47f1 or any 2020 LTS version.

Set up the Fusion

Open downloaded Karts project in unity.

Follow instruction in given link to set up Fussion AppId: https://doc.photonengine.com/fusion/current/tutorials/host-mode-basics/1-getting-started#step_6___create_an_app_id

Set up Tournament-SDK

Start by importing Tournament-SDK to your Unity project.

Setting up the scene

Add necessary objects

  1. In Project section, open Assets/Scenes -> double-click Launch.

    Launch

  2. In Hierarchy add 2 new GameObjects above Prompts, rename them to TournamentListUI and TournamentHubUI.

    SetUp

Adjust resolution

  1. Select TournamentListUI.

  2. In Rect Transform component in left top corner change adjustement to stretched.

  3. Set Left,Top,Right,Bottom parameters to 0.

    SetUp

  4. Do the same for TournamentHubUI.

Setting up the objects

  1. Add UIScreen as component to both new objects.

    SetUp

  2. Add TounamentHubScreen and TounamentListScreen to TounamentHubUI and TounamentHubUI respectively. Tournament(Hub/List)Screen can be found in Assets/TournamentSDK_Demo/Prefabs/DefaultUIScreens.

    SetUp

    Make sure SubScreen inside both TounamentHubScreen and TounamentListScreen is enabled!

    SetUp

  3. Disable TounamentListUI and TounamentHubUI.

Setting up necessary layout

  1. In Hierarchy go to -> Main Canvas/Main Menu Screen/LayoutGroup/Footer/LayoutGroup, select Exit Button and press Ctrl+D to duplicate the object.

    TButton

  2. Inspector will open right after object duplication, change button's name to Tournaments, also find Rect Transform change Width to 470.

    TButton

  3. Open newly created object in Hierarchy, select Text and change Text component to Tournaments.

    TButton

Setting up the buttons

Moving from Main Menu Screen to Tournament List UI

  1. Select Tournaments object created previosly in Hierarchy.

  2. Find Button component in Inspector.

  3. In On Click() field, drag ang drop Main Menu Screen from Hierarchy into field under Runtime Only.

  4. Change No Function to UIScreen/FocuScreen.

  5. In Hierarchy find TournamentListUI, drag and drop it into On Click() field under UIScreen.FocusScreen where None (UI Screen) is written.

    TButton

Moving from Tournament List UI to Main Menu Screen

  1. In Hierarchy go to TournamentListUI/TournamentListScreen/SubScreen/Canvas/Title.

  2. Select ButtonUnderline(Back), in it's Button component drag and drop TournamentListUI into field under Runtime Only.

  3. then change No Function to UIScreen/Back.

    TButton

Moving from Tournament List UI to Tournament Hub UI

  1. Go to TournamentListUI/TournamentListScreen/SubScreen/Canvas/Scroll View/ViewPort/Content/TournamentListItem/ButtonUnderline(Open)/(Play).

  2. Add new action to On Click().

  3. Drag TournamentListUI into field under Runtime Only.

  4. Change No Function to UIScreen/FocusScreen.

  5. Drag TournamentHubUI into field under UIScreen.FocusScreen.

    TButton

    Due to we used prefabs of TournamentListScreen and TournamentHubScreen, GUITournamentListScreen script has unset variable TournamentHubScreen.

  6. Go to TournamentListUI/TournamentListScreen/SubScreen there will be unset Tournament Hub Screen.

  7. From TournamentHubUI/TournamentHubScreen drag SubScreen into unset Tournament Hub Screen.

    TButton

Moving from Tournament Hub UI to Tournament List UI

Go to ButtonUnderline(Back) in TournamentHubUI same way as for TournamentListUI.

  1. Add another action on button by pressing +.

  2. Drag and drop TournamentHubUI into field under Runtime Only.

  3. Change No Function to UIScreen/Back.

    TButton

  4. Go to TournamentHubUI/TournamentHubScreen/SubScreen in GUISubScreen component will be unset Return Screen.

  5. From TournamentListUI/TournamentListScreen drag SubScreen into unset Return Screen field.

    TButton

Implementing Tournament-SDK

Create/Set up BackboneManager

  1. Create new GameObject on the very bottom of Main Canvas, rename it to BackboneManager.

  2. Press Add Component -> start typing Backbone Manager, it will appear in search tab, click on it.

  3. Backbone Manager component will appear in Inspector. Make sure Initialize On Start box is UN-ticked.

    BBManager

  4. Add Resource Cache component to Backbone Manager object.

Implementing client initialization flow into BackboneIntegration

  1. In Inspector of BackboneManager object select Add Component -> type BackboneIntegration -> select New Script/Create and Add.

    BBManager

  2. Add following code into BackboneIntegration script.

    using Gimmebreak.Backbone.User;
    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(ClientInfo.Username))
            {
                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" + ClientInfo.Username;
            // 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, ClientInfo.Username, 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;
            }
        }
    }
    

    BackboneIntegration component now misses the TournamentButton

  3. Drag Tournaments button from MainMenuScreen/LayoutGroup/Footer/LayoutGroup, into TournamentButton field in BackboneIntegration component.

    BBManager

Preparation to implement TournamentMatchHandler

Further changes are done in order to prevent any errors while implementing TournamentMatchHandler. Changes are done in game logic itself so that tournament match would have access to necessary data.

GameLauncher

Go to GameLauncher script add/change variables/methods as follows:

  1. Add SessionName variable to store name of the session, so we could gain access to it from other Game classes:

    public string SessionName
    {
        get
        {
            if (_runner != null) { return _runner.SessionInfo.Name; }
            return null;
        }
    }
    
  2. Add SetTournamentLobby() to set gameMode suitable for tournaments.

    // First player tries to connect to session as a Host,
    // if session is already created, then user tries to reconnect as a Client
    public void SetTournamentLobby() => _gameMode = GameMode.AutoHostOrClient;
    
  3. Go to JoinOrCreateLobby() method, in _runner.StartGame({...}) change:

    DisableClientSessionCreation = true
    

TO

// Allows game to attempt to create a session as a Client
// Condition is false only when tournament starts the session
// False because we don't want to disable that option
DisableClientSessionCreation = _gameMode != GameMode.AutoHostOrClient
  1. Change SetConnectionStatus() to:

    private void SetConnectionStatus(ConnectionStatus status)
    {
        Debug.Log($"Setting connection status to {status}");
    
        ConnectionStatus = status;
    
        if (!Application.isPlaying)
            return;
    
        if (status == ConnectionStatus.Disconnected || status == ConnectionStatus.Failed)
        {
            SceneManager.LoadScene(LevelManager.LOBBY_SCENE);
            UIScreen.BackToInitial();
            // We know that GameMode.AutoHostOrClient is set only when tournament is starting the session
            // If it is tournament session, then by the end of it we want player to be directed to TournamentHubUI
            if (_gameMode == GameMode.AutoHostOrClient)
            {
                LevelManager.LoadTournamentHub();
            }
        }
    }
    
  2. Change OnPlayerJoined() to:

    public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
    {
        if (runner.IsServer)
        {
            // GameMode.AutoHostOrClient allows tournament session Host to spawn a GameManager
            if (_gameMode == GameMode.Host || _gameMode == GameMode.AutoHostOrClient)
                runner.Spawn(_gameManagerPrefab, Vector3.zero, Quaternion.identity);
            var roomPlayer = runner.Spawn(_roomPlayerPrefab, Vector3.zero, Quaternion.identity, player);
            roomPlayer.GameState = RoomPlayer.EGameState.Lobby;
        }
        SetConnectionStatus(ConnectionStatus.Connected);
    }
    

LevelManager

Go to LevelManager script add/change variables/methods as follows:

  1. Add variables:

    // LevelManager will use these variables to redirect player back to TournamentHubScreen after tournament match
    [SerializeField] private UIScreen tournamentHubScreenUI;
    [SerializeField] private GUITournamentHubScreen tournamentHubScreenGUI;
    [SerializeField] private UIScreen tournamentListScreenUI;
    
  2. Add method LoadTournamentHub():

    public static void LoadTournamentHub() 
    {
        // Redirect player to TournamentListUI and then to TournamentHubUI to keep correct chronology
        UIScreen.Focus(Instance.tournamentListScreenUI);
        Instance.tournamentHubScreenGUI.Initialize(GameManager.Instance.TournamentId);
        UIScreen.Focus(Instance.tournamentHubScreenUI);
    }
    

GameManager

Go to GameManager script add/change variables/methods as follows:

  1. Add variables:

    // Values to personalize tournament match and later proceed it's statistics  
    [Networked(OnChanged = nameof(OnLobbyDetailsChangedCallback))] public long GameSessionId { get; set; }
    [Networked(OnChanged = nameof(OnLobbyDetailsChangedCallback))] public long TournamentId { get; set; } = 0;
    [Networked(OnChanged = nameof(OnLobbyDetailsChangedCallback))] public long TournamentMatchId { get; set; }
    
  2. Add method:

    // Allows us to chech if game is a tournament game
    public bool IsTournamentGame() => this.isActiveAndEnabled && this.TournamentId != 0;
    

ClientInfo

Go to ClientInfo script add/change variables/methods as follows:

  1. Add static variables to get information about Tournament(User/Team)Id when match result is being proceed:

    public static long TournamentUserId
    {
        get => long.Parse(PlayerPrefs.GetString("C_TournamentUserId", "0"));
        set => PlayerPrefs.SetString("C_TournamentUserId", value.ToString());
    }
        
    public static byte TournamentTeamId
    {
        get => byte.Parse(PlayerPrefs.GetString("C_TournamentTeamId", "0"));
        set => PlayerPrefs.SetString("C_TournamentTeamId", value.ToString());
    }
    

RoomPlayer

Go to RoomPlayer script add/change variables/methods as follows:

  1. Add variables:

    // These variables allow us to identify player and to proceed players statistics after match
    [Networked] public long TournamentUserId { get; set; }
    [Networked] public byte TournamentTeamId { get; set; }
    
  2. In Spawned() method find RPC_SetPlayerStats() and change it to:

    RPC_SetPlayerStats(ClientInfo.Username, ClientInfo.KartId, ClientInfo.TournamentUserId, ClientInfo.TournamentTeamId);
    
  3. Change RPC_SetPlayerStats() method to:

    private void RPC_SetPlayerStats(NetworkString<_32> username, int kartId, long tournamentUserId, byte tournamentTeamId)
    {
        // Allows game to store and update information about Tournament(Team/User)Id on in game object
        Username = username;
        KartId = kartId;
        TournamentUserId = tournamentUserId;
        TournamentTeamId = tournamentTeamId;
    }
    

Add new GUI/UIs to LevelManager

Level Manager is responsible for returning player back to TournamentHubUI after match is finished.

  1. Go to Main Canvas.

  2. Add TournamentHubScreenUI, TournamentHubScreenGUI and TournamentListUI as shown:

    TMH

Implementing TournamentMatchHandler

  1. Go to TournamentHubUI/TournamentHubScreen/SubScreen/Canvas.

  2. Add empty object to Canvas and rename it to TournamentMatchHandler.

  3. Add new script to it called TournamentMatchHandler.

    TMH

  4. Add following code into it.

using Fusion;
using Gimmebreak.Backbone.Core;
using Gimmebreak.Backbone.Tournaments;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TournamentMatchHandler : TournamentMatchCallbackHandler
{
    Tournament tournament;
    TournamentMatch tournamentMatch;
    ITournamentMatchController tournamentMatchController;
    bool sessionStarted;
    bool creatingSession;
    private GameLauncher _launcher;
    private WaitForSeconds waitOneSec = new WaitForSeconds(1);
    private string tournamentSessionName;

    void Awake()
    {
        _launcher = FindObjectOfType<GameLauncher>();
    }

    public void OnDisable()
    {
        // Unattach Actions from RoomPlayer Actions
        RoomPlayer.PlayerJoined -= OnPlayerEnteredRoom;
        RoomPlayer.PlayerLeft -= OnPlayerLeftRoom;
        RoomPlayer.PlayerChanged -= OnPlayerPropertiesUpdate;
    }

    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}";
        
        // Attach actions to RoomPlayer Actions
        RoomPlayer.PlayerJoined += OnPlayerEnteredRoom;
        RoomPlayer.PlayerLeft += OnPlayerLeftRoom;
        RoomPlayer.PlayerChanged += OnPlayerPropertiesUpdate;

        // Join Photon session
        StartCoroutine(JoinRoomRoutine());
    }

    private IEnumerator JoinRoomRoutine()
    {
        while (this.tournamentMatch != null)
        {
            // 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 (GameLauncher.ConnectionStatus == ConnectionStatus.Connected &&
                    _launcher.SessionName == this.tournamentSessionName)
                {
                    _launcher.LeaveSession();
                }
            }
            // Try to connect to tournament match session
            else if (GameLauncher.ConnectionStatus == ConnectionStatus.Disconnected)
            {
                // Set player propery with UserId so we can identify users in session
                // Set max players for session based on tournament phase setting
                ServerInfo.MaxUsers = (byte)(this.tournament.GetTournamentPhaseById(this.tournamentMatch.PhaseId).MaxTeamsPerMatch * this.tournament.PartySize);
                // Join or create Photon session with tournamemnt match secret as session id
                ServerInfo.LobbyName = this.tournamentSessionName;
                ClientInfo.LobbyName = this.tournamentSessionName;
                ClientInfo.TournamentUserId = BackboneManager.Client.User.UserId;
                ClientInfo.TournamentTeamId = this.tournamentMatch.GetMatchUserById(BackboneManager.Client.User.UserId).TeamId;
                _launcher.SetTournamentLobby();
                _launcher.JoinOrCreateLobby();
            }
            // If we are in wrong session then leave
            else if (GameLauncher.ConnectionStatus == ConnectionStatus.Connected &&
                     this.tournamentSessionName != _launcher.SessionName)
            {
                _launcher.LeaveSession();
            }

            yield return this.waitOneSec;
        }
    }

    public override bool IsConnectedToGameServerNetwork()
    {
        // Check if user is connected to photon and ready to join a session
        if (GameLauncher.ConnectionStatus == ConnectionStatus.Connected)
        {
            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
        foreach (RoomPlayer rp in RoomPlayer.Players)
        {
            if (rp.TournamentUserId == 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;
        _launcher.LeaveSession();
    }

    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;
                        RoomPlayer.Local.RPC_ChangeReadyState(true);
                        RoomPlayer.Local.KartId = 1;
                        // Set session properties
                        GameManager.Instance.GameSessionId = gameSession.Id;
                        GameManager.Instance.TournamentId = this.tournament.Id;
                        GameManager.Instance.TournamentMatchId = this.tournamentMatch.Id;
                    }
                })
                .Run(this);
        }
    }

    // Photon callback when player entered the session
    public void OnPlayerEnteredRoom(RoomPlayer player)
    {
        long userId = player.TournamentUserId;
        if (this.tournamentMatchController != null)
        {
            // Report user who joined room
            this.tournamentMatchController.ReportJoinedUser(userId);
        }
    }

    // Photon callback when player disconnected from session
    public void OnPlayerLeftRoom(RoomPlayer player)
    {
        if (this.tournamentMatchController != null)
        {
            long userId = player.TournamentUserId;
            // Report user who disconnected from room
            this.tournamentMatchController.ReportDisconnectedUser(userId);
        }
    }

    // Photon callback when player properties are updated
    public void OnPlayerPropertiesUpdate(RoomPlayer player)
    {
        if (this.tournamentMatchController != null)
        {
            // Reporting status change will refresh match metadata
            this.tournamentMatchController.ReportStatusChange();
        }
    }
}

Implementing TournamentMatchHandler into ActiveMatchContainer

After all necessary changes are done, add TournamentMatchHandler to ActiveMatchContainer as MatchHandler. ActiveMatchContainer can be found in TournamentHubUI/TournamentHubScreen/SubScreen/Canvas/ActiveMatchContainer.

TMH

Implementing Result submission

  1. Go to EndRaceUI script.
  2. Add variable that will keep track if result processing started.
private bool processingStarted = false;
  1. Find RedrawResultsList() method and edit it as follows:
public void RedrawResultsList(KartComponent updated)
{
    var parent = resultsContainer.transform;
    ClearParent(parent);

    var karts = GetFinishedKarts();
    for (var i = 0; i < karts.Count; i++)
    {
        var kart = karts[i];

        // As we disconnect player from session before dispawned is called,
        // this method will be called when kart.Controller.RoomUser is already destroyed.
        // Doesn't happen in ordinary match.
        if (kart.Controller.RoomUser != null)
        {
            Instantiate(resultItemPrefab, parent)
                .SetResult(kart.Controller.RoomUser.Username.Value, kart.LapController.GetTotalRaceTime(), i + 1);
        }
    }

    EnsureContinueButton(karts);
}
  1. Now edit EnsureContinueButton() method and add ProcessResult() method:
private void EnsureContinueButton(List<KartEntity> karts)
{
    var allFinished = karts.Count == KartEntity.Karts.Count;

    // Remove submission button and proceed result without user interaction to prevent any errors connected to that
    if (!this.processingStarted && this.isActiveAndEnabled && allFinished && GameManager.Instance.IsTournamentGame())
    {
        StartCoroutine(ProcessResult(karts));
    }
}

private IEnumerator ProcessResult(List<KartEntity> karts)
{
    // Notify that result processing has started
    this.processingStarted = true;

    List<GameSession.User> users = new List<GameSession.User>();
    Dictionary<long, float> results = new Dictionary<long, float>();
    GameSession gameSession;

    for (int i = 0; i < karts.Count; i++)
    {
        // Loop through each kart to obtain necessary information for result submition
        var tempUserId = tempSorted[i].Controller.RoomUser.TournamentUserId;
        var tempTeamId = tempSorted[i].Controller.RoomUser.TournamentTeamId;
        var tempUserValue = tempSorted[i].LapController.GetTotalRaceTime();

        // All karts are already sorted by time
        users.Add(new GameSession.User(tempUserId, tempTeamId) { Place = i + 1 });
        results.Add(tempUserId, tempUserValue);
    }

    // Create new GameSession
    gameSession = new GameSession(
        GameManager.Instance.GameSessionId,
        0,
        users,
        GameManager.Instance.TournamentMatchId);

    // Attach users and their results to current GameSession
    gameSession.Users.ForEach(user =>
    {
        gameSession.AddStat(1, user.UserId, (decimal)results[user.UserId]);
    });

    //report game session
    yield return BackboneManager.Client.SubmitGameSession(gameSession);
    //refresh tournament data
    yield return BackboneManager.Client.LoadTournament(GameManager.Instance.TournamentId);

    // Leave session after result was submited
    FindObjectOfType<GameLauncher>().LeaveSession();
}

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

  1. Go to https://www.tournament-sdk.com/tournaments

  2. There you will see No tournament templates .Create your first template to get started.

  3. Press Create your first template.

    TTemplate

Edit template

  1. Tournament template will appear. Select Edit template.

    TTemplate

  2. Go to Description and set Tournament Name.

    TTemplate

  3. Go to Registration set:

  • Maximum players - 2

  • Party(team) size - 1

  • Registration rules -> Open to everyone

    TTemplate

  1. Go to Format/Add Phase

In Format set:

  • Teams - 2
  • Min teams per match - 2
  • Max Teams per match - 2

Leave field Max loses in Scores empty

In Rounds set:

  • Type - BO3
  • Minimum game time (minutes) - 2
  • Maximum round time (minutes) - 8
  1. On the bottom of the screen you should see Careful - you have unsaved changes!, press Save Changes.

    TTemplate

Start tournament

  1. Get back to Tournament templates page.

  2. Press Schedule, set Time to Your current time + 5 minutes and press Start tournament.

    TTemplate

Final test

  1. Now get back to Unity and start the project.

  2. Press Tournaments button that was added earlier. You should see TournamentListScreen and the tournament that you just added.

    FinalTest

    Tournament may be unavailable to register for some time, wait until Sign up button is available to register to tournament.

    FinalTest

  3. Repeat the process in build version

  4. When tournament will start, button Ready to play will become available.

    FinalTest

  5. Press on both Unity and Build Ready to play.

    FinalTest

    FinalTest

  6. Finish the match on both players.

  7. Get back to: https://www.tournament-sdk.com/schedule

  8. You should see tournament.

    FinalTest

  9. Click on it, to see more information about the tournament and played matches.

  10. To check more detailed information about every match played during the tournament, go to Phase 1.

  11. Press Show matches to the right from any participant.

  12. Click Show details for more information.

    FinalTest

  13. Click Game #ID and Stats for more information.

    FinalTest