Leaderboard in a simple tic tac toe game

I want to create a simple tic tac toe, where we keep track of Wins, lost, Ties to calculate the score and update win lost and tie track.
I was thinking of using a formula to calculate the score using won, lost and ties.

score := win*100 - lost*120 + tie*50

I was thinking that I could store the metadata in each entry in the leaderboard, in the format of :

metadata := map[string]interface{}{"W": win, "L": lost, "T": tie}

Now, I couldn’t find how can I find the score of a user in leaderboard apart from LeaderboardRecordsList function, extract the metadata from it, and calculate the new score and update the won, lost, tie metadata. But I am getting an empty result even tho in my Nakama console i can see 2 uses in leaderboard with scores.

Here’s the code

func updateLeaderboard(ctx context.Context, nk runtime.NakamaModule, logger runtime.Logger, userID string, win, lost, tie int) {
	account, err := nk.AccountGetId(ctx, userID)
	if err != nil {
		logger.Error("failed to fetch user: %v", err)
	}

	leaderboardID := "69420-50000-3000-2"

	score := win*100 - lost*80 + tie*50 + 100
	metadata := map[string]interface{}{"W": win, "L": lost, "T": tie}

	ownerIds := []string{userID}

	cursor := ""

	records, ownerRecords, _, _, err := nk.LeaderboardRecordsList(ctx, leaderboardID, ownerIds, 100, cursor, int64(3600))
	if err != nil {
		logger.WithField("err", err).Error("Leaderboard record haystack error.")
		return
	}

	if records != nil {
		for _, record := range records {
			logger.Info("Leaderboard record: %s, Score: %d, Subscore: %d", record.LeaderboardId, record.Score, record.Subscore)
		}
	}

	if ownerRecords != nil {
		for _, ownerRecord := range ownerRecords {
			// Log the owner record details
			logger.Info("Owner record: %s, Score: %d, Subscore: %d", ownerRecord.LeaderboardId, ownerRecord.Score, ownerRecord.Subscore)
		}
	}
}

And here’s the image of my Nakama Console

I am having problem in this, Could anyone help me in this ?
And if this is not a best practice to keep a track of scores , and there’s a better and more elegant way of doing it, then please recommend it too, it would mean a lot.

Hello @mahanshAdtiya,

The score metadata is really just meant to be used to store some static information about the score, not to be continuously updated.

I’d recommend you to keep track of this data in a storage object, and whenever you recalculate the score, you can update it on the leaderboard with LeaderboardRecordWrite, and use the overrideOperator param with set if needed.

Best.

So my main goal is to make a server-authoritative multiplayer tic tac toe game. In this I want to implement a leaderboard system that tracks the ranking and performance of players.
So I was thinking of keeping track of number of total wins, lost and ties to calculate the score each player and whoever has the highest score will be on top of the leaderboard.

So the best way to do that would be that whenever a new user is created, I create a storage object which contains won lost and tied for each player, and whenever a match is over, i update these variables in storage and re calculate the score and update it on leaderboard?

Yes, that’s what I was suggesting.

Our template project implements authoritative match handlers for tic-tac-toe which can serve as an example where you can add the above logic.

I have run into some problem, I have created the leaderboard, I am able to calculate score, everything’s working fine and as expected.
But I am not able to fetch leaderboard records from server in client side, I am getting an empty array, even though I can clearly see records in my nakama console, Do you know what could possibly be causing this error ?

Here’s the code :
My Nakama Store

import { create } from 'zustand';
import { Client } from '@heroiclabs/nakama-js';
import { v4 as uuidv4 } from 'uuid';
import useUserStore from './useUser';

const useNakamaClient = create((set, get) => ({
  client: null,
  session: null,
  socket: null,
  isMatching: false,
  matchID: null,
  leaderboard: [],

  initializeClient: () => {
    const client = new Client('defaultkey', 'localhost', '7350');
    client.ssl = false;
    set({ client });
  },
  authenticate: async () => {
    const { deviceID, setDeviceID, setUserID } = useUserStore.getState();
    const { client } = get();
    
    if (!deviceID) {
      const newDeviceID = uuidv4();
      setDeviceID(newDeviceID);
    }

    try {
      const session = await client.authenticateDevice(deviceID, true);
      setUserID(session.user_id);
      set({ session });
    } catch (error) {
      console.error('Authentication failed:', error);
      throw error;
    }
  },
  fetchLeaderboard: async () => {
    const { session, client } = get();
    const leaderboardID = '69420-50000-3000-2';

    try {
      if (!client || !session) {
        console.error('Nakama client or session is not available.');
        return;
      }

      const leaderboardRecords = await client.listLeaderboardRecords(session, leaderboardID, null, 20, null, null, null);

      const leaderboardData = leaderboardRecords.map(record => ({
        rank: record.rank,
        username: record.username,
        score: record.score
      }));

      set({ leaderboard: leaderboardData });
      console.log('Leaderboard fetched:', leaderboardData);
    } catch (error) {
      console.error('Failed to fetch leaderboard:', error);
      throw error;
    }
  },
  updateName: async (userName) => {
    const { session, client } = get();
    if (!userName) return;

    try {
      if (!client || !session) {
        console.error('Nakama client or session is not available.');
        return;
      }

      const updatePayload = { username: userName };
      const updateResponse = await client.updateAccount(session, updatePayload);

      console.log('Username updated on the server:', updateResponse);
    } catch (error) {
      console.error('Failed to update username on the server:', error);
      throw error;
    }
  },
  findMatch: async (ai = false) => {
    set({ isMatching: true }); 
    const { session, client , socket} = get();
    const rpcid = 'find_match';

    try {
      const matches = await client.rpc(session, rpcid, { ai });
      const matchID = matches.payload.matchIds[0]

      set({ matchID });

      await socket.joinMatch(matchID);
      
      set({ isMatching: false }); 
    } catch (error) {
      console.error('Match could not be found:', error);
      set({ isMatching: false }); 
      throw error;
    }
  },
  makeMove: async (index) => {
    const data = { "position": index };
    const { socket, matchID } = get();
    await socket.sendMatchState(matchID, 4, JSON.stringify(data));
  },
  createSocketConnection: async () => {
    const { session, client } = get();
    const socket = client.createSocket(client.ssl, false);
    try {
      await socket.connect(session);
      set({ socket });
    } catch (error) {
      console.error('Failed to connect WebSocket:', error);
      throw error;
    }
  },
  closeSocketConnection: () => {
    const { socket } = get();
    if (socket) {
      socket.disconnect();
      set({ socket: null });
      console.log('WebSocket connection closed.');
    }
  },
}));

export default useNakamaClient;

Here I am calling it :

import React, {useEffect} from 'react'
import {useNakamaClient} from '../hooks'

function LeaderBoard() {
  const{leaderboard, fetchLeaderboard} = useNakamaClient()
  useEffect(() => {
    fetchLeaderboard(); 
  }, [fetchLeaderboard]);

  console.log("WTF", leaderboard)
  return (
    <div>LeaderBoard</div>
  )
}

export default LeaderBoard

Above was my client side, Here’s my server function where I am creating the leaderboard

func createLeaderboard(ctx context.Context, nk runtime.NakamaModule, logger runtime.Logger) error {
	leaderboardID := "69420-50000-3000-2"

	resetSchedule := "59 23 * * 0"

	metadata := map[string]interface{}{
		"displayString": "Weekly Top Scores",
	}

	err := nk.LeaderboardCreate(ctx, leaderboardID, true, "desc", "set",resetSchedule,metadata,true)
	if err != nil {
		logger.Error("Could not create leaderboard: %v", err)
		return err
	}

	logger.Info("Leaderboard '%s' created successfully!", leaderboardID)
	return nil
}

And here’s my nakama console:

But I am getting this

Here’s My leaderboard and it’s properties:

I think should instead be:

const leaderboardData = leaderboardRecords.records.map(record => ({
  rank: record.rank,
  username: record.username,
  score: record.score
}));

Not sure why there’s no error being surfaced.

This block is giving error

  fetchLeaderboard: async () => {
    const { session, client } = get();
    const leaderboardID = '69420-50000-3000-2';

    try {
      if (!client || !session) {
        console.error('Nakama client or session is not available.');
        return;
      }

      const leaderboardRecords = await client.listLeaderboardRecords(session, leaderboardID, null, 20, null, null, null);
      console.log('WTF', leaderboardRecords)
      const leaderboardData = leaderboardRecords.map(record => ({
        rank: record.rank,
        username: record.username,
        score: record.score
      }));

      set({ leaderboard: leaderboardData });
      console.log('Leaderboard fetched:', leaderboardData);
    } catch (error) {
      console.error('Failed to fetch leaderboard:', error);
      throw error;
    }
  },

I don’t why becuz I am using find_match, it’s working fine, without any problem, although I made some changes and it’s working fine, but it’s the way I intended it to work.

Earlier I was calling the function to fetch leaderboard in a useEffect, but now i am calling it using a button and it’s working.