Firestore দিয়ে লিডারবোর্ড তৈরি করুন

1. ভূমিকা

শেষ আপডেট: 2023-01-27

একটি লিডারবোর্ড তৈরি করতে কি কি লাগে?

তাদের মূলে, লিডারবোর্ডগুলি হল একটি জটিল ফ্যাক্টর সহ স্কোরের সারণী: যেকোন প্রদত্ত স্কোরের জন্য একটি র‌্যাঙ্ক পড়ার জন্য অন্য সমস্ত স্কোর সম্পর্কে কিছু ক্রমে জ্ঞান প্রয়োজন। এছাড়াও, যদি আপনার গেমটি বন্ধ হয়ে যায়, আপনার লিডারবোর্ডগুলি বড় হবে এবং প্রায়শই পড়া এবং লেখা হবে। একটি সফল লিডারবোর্ড তৈরি করার জন্য, এটি দ্রুত এই র্যাঙ্কিং অপারেশন পরিচালনা করতে সক্ষম হতে হবে।

আপনি কি নির্মাণ করবেন

এই কোডল্যাবে, আপনি বিভিন্ন ভিন্ন লিডারবোর্ড বাস্তবায়ন করবেন, প্রতিটি ভিন্ন দৃশ্যের জন্য উপযুক্ত।

আপনি কি শিখবেন

আপনি শিখবেন কিভাবে চারটি ভিন্ন লিডারবোর্ড বাস্তবায়ন করতে হয়:

  • র‍্যাঙ্ক নির্ধারণের জন্য সহজ রেকর্ড-গণনা ব্যবহার করে একটি নির্বোধ বাস্তবায়ন
  • একটি সস্তা, পর্যায়ক্রমে আপডেট করা লিডারবোর্ড
  • কিছু ট্রি বাজে কথা সহ একটি রিয়েল-টাইম লিডারবোর্ড
  • খুব বড় প্লেয়ার বেসের আনুমানিক র‌্যাঙ্কিংয়ের জন্য একটি স্টোকাস্টিক (সম্ভাব্য) লিডারবোর্ড

আপনি কি প্রয়োজন হবে

  • Chrome এর একটি সাম্প্রতিক সংস্করণ (107 বা তার পরে)
  • Node.js 16 বা উচ্চতর (যদি আপনি nvm ব্যবহার করেন তবে আপনার সংস্করণ নম্বর দেখতে nvm --version চালান)
  • একটি প্রদত্ত ফায়ারবেস ব্লেজ পরিকল্পনা (ঐচ্ছিক)
  • Firebase CLI v11.16.0 বা উচ্চতর
    CLI ইনস্টল করতে, আপনি npm install -g firebase-tools চালাতে পারেন বা আরও ইনস্টলেশন বিকল্পের জন্য CLI ডকুমেন্টেশন দেখুন।
  • জাভাস্ক্রিপ্ট, ক্লাউড ফায়ারস্টোর, ক্লাউড ফাংশন এবং ক্রোম ডেভটুল সম্পর্কে জ্ঞান

2. সেট আপ করা হচ্ছে

কোড পান

এই প্রজেক্টের জন্য আপনার যা যা প্রয়োজন আমরা তা একটি গিট রেপোতে রেখেছি। শুরু করার জন্য, আপনাকে কোডটি ধরতে হবে এবং আপনার প্রিয় ডেভ পরিবেশে এটি খুলতে হবে। এই কোডল্যাবের জন্য, আমরা ভিএস কোড ব্যবহার করেছি, তবে যেকোনো পাঠ্য সম্পাদক তা করবে।

এবং ডাউনলোড করা জিপ ফাইলটি আনপ্যাক করুন।

অথবা, আপনার পছন্দের ডিরেক্টরিতে ক্লোন করুন:

git clone https://github.com/FirebaseExtended/firestore-leaderboards-codelab.git

আমাদের শুরু বিন্দু কি?

আমাদের প্রকল্পটি বর্তমানে কিছু খালি ফাংশন সহ একটি ফাঁকা স্লেট:

  • index.html কিছু আঠালো স্ক্রিপ্ট রয়েছে যা আমাদের dev কনসোল থেকে ফাংশনগুলি শুরু করতে এবং তাদের আউটপুটগুলি দেখতে দেয়। আমরা আমাদের ব্যাকএন্ডের সাথে ইন্টারফেস করতে এটি ব্যবহার করব এবং আমাদের ফাংশন আহ্বানের ফলাফল দেখতে পাব। একটি বাস্তব-বিশ্বের দৃশ্যে, আপনি সরাসরি আপনার গেম থেকে এই ব্যাকএন্ড কলগুলি করতে চান—আমরা এই কোডল্যাবে একটি গেম ব্যবহার করছি না কারণ আপনি যখনই লিডারবোর্ডে একটি স্কোর যোগ করতে চান তখন একটি গেম খেলতে খুব বেশি সময় লাগবে .
  • functions/index.js আমাদের সমস্ত ক্লাউড ফাংশন রয়েছে। আপনি কিছু ইউটিলিটি ফাংশন দেখতে পাবেন, যেমন addScores এবং deleteScores , সেইসাথে ফাংশনগুলি আমরা এই কোডল্যাবে প্রয়োগ করব, যা অন্য ফাইলে সহায়ক ফাংশনগুলিকে কল করে।
  • functions/functions-helpers.js খালি ফাংশন রয়েছে যা আমরা বাস্তবায়ন করব। প্রতিটি লিডারবোর্ডের জন্য, আমরা পঠন, তৈরি এবং আপডেট ফাংশনগুলি বাস্তবায়ন করব এবং আপনি দেখতে পাবেন কীভাবে আমাদের বাস্তবায়নের পছন্দ আমাদের বাস্তবায়নের জটিলতা এবং এর স্কেলিং কার্যকারিতা উভয়কেই প্রভাবিত করে।
  • functions/utils.js আরও ইউটিলিটি ফাংশন রয়েছে। আমরা এই কোডল্যাবে এই ফাইলটি স্পর্শ করব না।

একটি Firebase প্রকল্প তৈরি এবং কনফিগার করুন

  1. Firebase কনসোলে , প্রজেক্ট যোগ করুন ক্লিক করুন।
  2. একটি নতুন প্রকল্প তৈরি করতে, পছন্দসই প্রকল্পের নাম লিখুন।
    এটি প্রকল্পের নামের উপর ভিত্তি করে কিছুতে প্রকল্প আইডি (প্রকল্পের নামের নীচে প্রদর্শিত) সেট করবে। আপনি ঐচ্ছিকভাবে এটিকে আরও কাস্টমাইজ করতে প্রকল্প আইডিতে সম্পাদনা আইকনে ক্লিক করতে পারেন।
  3. অনুরোধ করা হলে, Firebase শর্তাবলী পর্যালোচনা করুন এবং স্বীকার করুন।
  4. অবিরত ক্লিক করুন.
  5. এই প্রকল্পের জন্য Google Analytics সক্ষম করুন বিকল্পটি নির্বাচন করুন এবং তারপরে অবিরত ক্লিক করুন।
  6. ব্যবহার করতে একটি বিদ্যমান Google Analytics অ্যাকাউন্ট নির্বাচন করুন বা একটি নতুন অ্যাকাউন্ট তৈরি করতে একটি নতুন অ্যাকাউন্ট তৈরি করুন নির্বাচন করুন।
  7. প্রকল্প তৈরি করুন ক্লিক করুন।
  8. প্রজেক্ট তৈরি হয়ে গেলে Continue এ ক্লিক করুন।
  9. বিল্ড মেনু থেকে, ফাংশন ক্লিক করুন, এবং যদি অনুরোধ করা হয়, ব্লেজ বিলিং প্ল্যান ব্যবহার করতে আপনার প্রকল্প আপগ্রেড করুন।
  10. বিল্ড মেনু থেকে, ফায়ারস্টোর ডাটাবেসে ক্লিক করুন।
  11. প্রদর্শিত ডাটাবেস তৈরি ডায়ালগে, টেস্ট মোডে শুরু করুন নির্বাচন করুন, তারপরে পরবর্তী ক্লিক করুন।
  12. ক্লাউড ফায়ারস্টোর অবস্থান ড্রপ-ডাউন থেকে একটি অঞ্চল চয়ন করুন, তারপর সক্ষম করুন ক্লিক করুন।

আপনার লিডারবোর্ড কনফিগার করুন এবং চালান

  1. একটি টার্মিনালে, প্রোজেক্ট রুটে নেভিগেট করুন এবং firebase use --add । আপনার তৈরি করা Firebase প্রকল্পটি বেছে নিন।
  2. প্রকল্পের রুটে, firebase emulators:start --only hosting
  3. আপনার ব্রাউজারে, localhost:5000
  4. Chrome DevTools এর JavaScript কনসোল খুলুন এবং leaderboard.js আমদানি করুন :
    const leaderboard = await import("http://localhost:5000/scripts/leaderboard.js");
    
  5. leaderboard.codelab(); কনসোলে আপনি যদি একটি স্বাগত বার্তা দেখতে পান, আপনি প্রস্তুত! যদি না হয়, এমুলেটর বন্ধ করুন এবং 2-4 ধাপগুলি পুনরায় চালান৷

প্রথম লিডারবোর্ড বাস্তবায়নে ঝাঁপ দেওয়া যাক।

3. একটি সাধারণ লিডারবোর্ড প্রয়োগ করুন

এই বিভাগের শেষ নাগাদ, আমরা লিডারবোর্ডে একটি স্কোর যোগ করতে পারব এবং এটি আমাদের র‌্যাঙ্ক জানাতে পারব।

আমরা ঝাঁপ দেওয়ার আগে, এই লিডারবোর্ড বাস্তবায়ন কীভাবে কাজ করে তা ব্যাখ্যা করা যাক: সমস্ত খেলোয়াড়কে একটি একক সংগ্রহে সংরক্ষণ করা হয়, এবং সংগ্রহটি পুনরুদ্ধার করে এবং তাদের থেকে কতজন খেলোয়াড় এগিয়ে আছে তা গণনা করে একজন খেলোয়াড়ের র‌্যাঙ্ক সংগ্রহ করা হয়। এটি একটি স্কোর সন্নিবেশ করা এবং আপডেট করা সহজ করে তোলে। একটি নতুন স্কোর সন্নিবেশ করার জন্য, আমরা এটিকে সংগ্রহে যুক্ত করি এবং এটি আপডেট করার জন্য, আমরা আমাদের বর্তমান ব্যবহারকারীর জন্য ফিল্টার করি এবং তারপর ফলাফল নথিটি আপডেট করি। চলুন দেখে নেওয়া যাক কোডে কেমন দেখায়।

functions/functions-helper.js এ, createScore ফাংশনটি প্রয়োগ করুন, যা এটির মতো সহজবোধ্য:

async function createScore(score, playerID, firestore) {
  return firestore.collection("scores").doc().create({
    user: playerID,
    score: score,
  });
}

স্কোর আপডেট করার জন্য, আপডেট করা স্কোর ইতিমধ্যেই বিদ্যমান আছে তা নিশ্চিত করতে আমাদের শুধু একটি ত্রুটি পরীক্ষা যোগ করতে হবে:

async function updateScore(playerID, newScore, firestore) {
  const playerSnapshot = await firestore.collection("scores")
      .where("user", "==", playerID).get();
  if (playerSnapshot.size !== 1) {
    throw Error(`User not found in leaderboard: ${playerID}`);
  }
  const player = playerSnapshot.docs[0];
  const doc = firestore.doc(player.id);
  return doc.update({
    score: newScore,
  });
}

এবং অবশেষে, আমাদের সহজ কিন্তু কম-স্কেলযোগ্য র্যাঙ্ক ফাংশন:

async function readRank(playerID, firestore) {
  const scores = await firestore.collection("scores")
      .orderBy("score", "desc").get();
  const player = `${playerID}`;
  let rank = 1;
  for (const doc of scores.docs) {
    const user = `${doc.get("user")}`;
    if (user === player) {
      return {
        user: player,
        rank: rank,
        score: doc.get("score"),
      };
    }
    rank++;
  }
  // No user found
  throw Error(`User not found in leaderboard: ${playerID}`);
}

এর পরীক্ষা করা যাক! টার্মিনালে নিম্নলিখিতগুলি চালিয়ে আপনার ফাংশনগুলি স্থাপন করুন:

firebase deploy --only functions

এবং তারপরে, Chrome এর JS কনসোলে, কিছু অন্যান্য স্কোর যোগ করুন যাতে আমরা অন্যান্য খেলোয়াড়দের মধ্যে আমাদের র‌্যাঙ্কিং দেখতে পারি।

leaderboard.addScores(); // Results may take some time to appear.

এখন আমরা মিশ্রণে আমাদের নিজস্ব স্কোর যোগ করতে পারি:

leaderboard.addScore(999, 11); // You can make up a score (second argument) here.

লেখাটি সম্পূর্ণ হলে, আপনি "স্কোর তৈরি" বলে কনসোলে একটি প্রতিক্রিয়া দেখতে পাবেন। পরিবর্তে একটি ত্রুটি দেখছেন? কী ভুল হয়েছে তা দেখতে Firebase কনসোলের মাধ্যমে ফাংশন লগগুলি খুলুন৷

এবং, অবশেষে, আমরা আমাদের স্কোর আনতে এবং আপডেট করতে পারি।

leaderboard.getRank(999);
leaderboard.updateScore(999, 0);
leaderboard.getRank(999); // we should be last place now (11)

যাইহোক, এই বাস্তবায়ন আমাদের একটি প্রদত্ত স্কোরের র্যাঙ্ক আনার জন্য অবাঞ্ছিত রৈখিক সময় এবং মেমরির প্রয়োজনীয়তা দেয়। যেহেতু ফাংশন এক্সিকিউশনের সময় এবং মেমরি উভয়ই সীমিত, তাই শুধু এর অর্থই নয় যে আমাদের ফেচগুলি ক্রমশ ধীর হয়ে যাবে, কিন্তু লিডারবোর্ডে পর্যাপ্ত স্কোর যোগ করার পরে, ফলাফল ফেরানোর আগে আমাদের ফাংশনগুলি সময় শেষ হয়ে যাবে বা ক্র্যাশ হয়ে যাবে। স্পষ্টতই, আমরা যদি মুষ্টিমেয় কিছু খেলোয়াড়ের বাইরে স্কেল করতে যাচ্ছি তবে আমাদের আরও ভাল কিছুর প্রয়োজন হবে।

আপনি যদি একজন Firestore অনুরাগী হন, তাহলে আপনি COUNTটি সমষ্টিগত প্রশ্ন সম্পর্কে অবগত হতে পারেন, যা এই লিডারবোর্ডটিকে আরও বেশি পারফরম্যান্স করে তুলবে৷ এবং আপনি ঠিক হবে! COUNTটি প্রশ্নের সাথে, এটি একটি মিলিয়ন বা তার বেশি ব্যবহারকারীর নীচে সুন্দরভাবে স্কেল করে, যদিও এর কার্যকারিতা এখনও রৈখিক।

কিন্তু অপেক্ষা করুন, আপনি হয়তো নিজের মনেই ভাবছেন, যদি আমরা সংগ্রহে থাকা সমস্ত নথির হিসাব করতে যাই, আমরা প্রতিটি নথিকে একটি র‌্যাঙ্ক বরাদ্দ করতে পারি এবং তারপরে যখন আমাদের এটি আনতে হবে, আমাদের আনা হবে O(1) সময় এবং স্মৃতি! এটি আমাদের পরবর্তী পদ্ধতির দিকে নিয়ে যায়, পর্যায়ক্রমে আপডেট করা লিডারবোর্ড।

4. একটি পর্যায়ক্রমে আপডেট করা লিডারবোর্ড প্রয়োগ করুন৷

এই পদ্ধতির মূল চাবিকাঠি হল নথিতে র‌্যাঙ্ক সংরক্ষণ করা, তাই এটি আনার ফলে কোনও অতিরিক্ত কাজ ছাড়াই র‌্যাঙ্ক পাওয়া যায়। এটি অর্জন করতে, আমাদের একটি নতুন ধরনের ফাংশন প্রয়োজন হবে।

index.js এ, নিম্নলিখিত যোগ করুন:

// Also add this to the top of your file
const admin = require("firebase-admin");

exports.scheduledFunctionCrontab = functions.pubsub.schedule("0 2 * * *")
    // Schedule this when most of your users are offline to avoid
    // database spikiness.
    .timeZone("America/Los_Angeles")
    .onRun((context) => {
      const scores = admin.firestore().collection("scores");
      scores.orderBy("score", "desc").get().then((snapshot) => {
        let rank = 1;
        const writes = [];
        for (const docSnapshot of snapshot.docs) {
          const docReference = scores.doc(docSnapshot.id);
          writes.push(docReference.set({rank: rank}, admin.firestore.SetOptions.merge()));
          rank++;
        }
        Promise.all(writes).then((result) => {
          console.log(`Writes completed with results: ${result}`);
        });
      });
      return null;
    });

এখন আমাদের পড়া, আপডেট করা এবং লেখার ক্রিয়াকলাপগুলি সুন্দর এবং সহজ। লিখুন এবং আপডেট উভয়ই অপরিবর্তিত, তবে পড়া হয়ে যায় ( functions-helpers.js ):

async function readRank(playerID, firestore) {
  const scores = firestore.collection("scores");
  const playerSnapshot = await scores
      .where("user", "==", playerID).get();
  if (playerSnapshot.size === 0) {
    throw Error(`User not found in leaderboard: ${playerID}`);
  }

  const player = playerSnapshot.docs[0];
  if (player.get("rank") === undefined) {
    // This score was added before our scheduled function could run,
    // but this shouldn't be treated as an error
    return {
    user: playerID,
    rank: null,
    score: player.get("score"),
  };
  }

  return {
    user: playerID,
    rank: player.get("rank"),
    score: player.get("score"),
  };
}

দুর্ভাগ্যবশত, আপনি আপনার প্রকল্পে একটি বিলিং অ্যাকাউন্ট যোগ না করে এটি স্থাপন ও পরীক্ষা করতে পারবেন না। আপনার যদি একটি বিলিং অ্যাকাউন্ট থাকে, তবে নির্ধারিত ফাংশনের ব্যবধানটি ছোট করুন এবং আপনার ফাংশনটি যাদুকরীভাবে আপনার লিডারবোর্ড স্কোরগুলিতে র‌্যাঙ্ক বরাদ্দ করে দেখুন।

যদি তা না হয়, নির্ধারিত ফাংশনটি মুছুন এবং পরবর্তী বাস্তবায়নে এগিয়ে যান।

এগিয়ে যান এবং পরবর্তী বিভাগের জন্য প্রস্তুত করতে স্কোর সংগ্রহের পাশে 3টি বিন্দুতে ক্লিক করে আপনার Firestore ডাটাবেসের স্কোরগুলি মুছুন৷

Firestore scores document page with\nDelete Collection activated

5. একটি রিয়েল-টাইম ট্রি লিডারবোর্ড প্রয়োগ করুন৷

এই পদ্ধতিটি ডাটাবেস সংগ্রহে অনুসন্ধান ডেটা সংরক্ষণ করে কাজ করে। একটি ইউনিফর্ম সংগ্রহের পরিবর্তে, আমাদের লক্ষ্য হল একটি গাছের মধ্যে সবকিছু সংরক্ষণ করা যা আমরা নথির মাধ্যমে সরে যেতে পারি। এটি আমাদের একটি প্রদত্ত স্কোরের র্যাঙ্কের জন্য একটি বাইনারি (বা n-ary) অনুসন্ধান করতে দেয়। যে মত চেহারা কি হতে পারে?

শুরু করার জন্য, আমরা আমাদের স্কোরগুলিকে মোটামুটি এমনকি বালতিতে বিতরণ করতে সক্ষম হতে চাই, যার জন্য আমাদের ব্যবহারকারীরা লগিং করা স্কোরগুলির মান সম্পর্কে কিছু জ্ঞানের প্রয়োজন হবে; উদাহরণস্বরূপ, যদি আপনি একটি প্রতিযোগিতামূলক খেলায় দক্ষতা রেটিং এর জন্য একটি লিডারবোর্ড তৈরি করেন, আপনার ব্যবহারকারীদের দক্ষতা রেটিং প্রায় সবসময়ই স্বাভাবিকভাবে বিতরণ করা হবে। আমাদের র্যান্ডম স্কোর তৈরির ফাংশন JavaScript এর Math.random() ব্যবহার করে, যার ফলে মোটামুটি সমান বন্টন হয়, তাই আমরা আমাদের বালতিগুলিকে সমানভাবে ভাগ করব।

এই উদাহরণে আমরা সরলতার জন্য 3টি বালতি ব্যবহার করব, তবে আপনি সম্ভবত দেখতে পাবেন যে আপনি যদি একটি বাস্তব অ্যাপে এই প্রয়োগটি ব্যবহার করেন তবে আরও বালতি দ্রুত ফলাফল দেবে–একটি অগভীর গাছ মানে গড়ে কম সংগ্রহ আনা এবং কম লক বিতর্ক।

একজন খেলোয়াড়ের র‍্যাঙ্ক উচ্চতর স্কোর সহ খেলোয়াড়ের সংখ্যার যোগফল দ্বারা এবং খেলোয়াড়ের নিজের জন্য একটি দিয়ে দেওয়া হয়। scores অধীনে প্রতিটি সংগ্রহ তিনটি নথি সংরক্ষণ করবে, প্রতিটি একটি পরিসর সহ, প্রতিটি ব্যাপ্তির অধীনে নথির সংখ্যা এবং তারপর তিনটি সংশ্লিষ্ট উপসংগ্রহ। একটি র্যাঙ্ক পড়ার জন্য আমরা একটি স্কোর খুঁজতে এবং বৃহত্তর স্কোরের যোগফলের ট্র্যাক রাখার জন্য এই গাছটি অতিক্রম করব। যখন আমরা আমাদের স্কোর খুঁজে পাই, তখন আমাদের সঠিক যোগফলও থাকবে।

লেখা উল্লেখযোগ্যভাবে আরো জটিল। প্রথমত, একই সময়ে একাধিক লেখা বা পঠন ঘটলে ডেটার অসঙ্গতি রোধ করতে আমাদের সমস্ত লেখা একটি লেনদেনের মধ্যে তৈরি করতে হবে। আমাদের নতুন নথিগুলি লেখার জন্য গাছটি অতিক্রম করার সময় আমাদের উপরে বর্ণিত সমস্ত শর্ত বজায় রাখতে হবে। এবং, পরিশেষে, যেহেতু আমাদের কাছে এই নতুন পদ্ধতির সমস্ত গাছ জটিলতা রয়েছে এবং আমাদের সমস্ত মূল নথি সংরক্ষণ করার প্রয়োজনীয়তার সাথে মিলিত হয়েছে, আমাদের স্টোরেজ খরচ কিছুটা বাড়বে (তবে এটি এখনও রৈখিক)।

functions-helpers.js এ:

async function createScore(playerID, score, firestore) {
  /**
   * This function assumes a minimum score of 0 and that value
   * is between min and max.
   * Returns the expected size of a bucket for a given score
   * so that bucket sizes stay constant, to avoid expensive
   * re-bucketing.
   * @param {number} value The new score.
   * @param {number} min The min of the previous range.
   * @param {number} max The max of the previous range. Must be greater than
   *     min.
   * @return {Object<string, number>} Returns an object containing the new min
   *     and max.
   */
  function bucket(value, min, max) {
    const bucketSize = (max - min) / 3;
    const bucketMin = Math.floor(value / bucketSize) * bucketSize;
    const bucketMax = bucketMin + bucketSize;
    return {min: bucketMin, max: bucketMax};
  }

  /**
   * A function used to store pending writes until all reads within a
   * transaction have completed.
   *
   * @callback PendingWrite
   * @param {admin.firestore.Transaction} transaction The transaction
   *     to be used for writes.
   * @return {void}
   */

  /**
   * Recursively searches for the node to write the score to,
   * then writes the score and updates any counters along the way.
   * @param {number} id The user associated with the score.
   * @param {number} value The new score.
   * @param {admin.firestore.CollectionReference} coll The collection this
   *     value should be written to.
   * @param {Object<string, number>} range An object with properties min and
   *     max defining the range this score should be in. Ranges cannot overlap
   *     without causing problems. Use the bucket function above to determine a
   *     root range from constant values to ensure consistency.
   * @param {admin.firestore.Transaction} transaction The transaction used to
   *     ensure consistency during tree updates.
   * @param {Array<PendingWrite>} pendingWrites A series of writes that should
   *     occur once all reads within a transaction have completed.
   * @return {void} Write error/success is handled via the transaction object.
   */
  async function writeScoreToCollection(
      id, value, coll, range, transaction, pendingWrites) {
    const snapshot = await transaction.get(coll);
    if (snapshot.empty) {
      // This is the first score to be inserted into this node.
      for (const write of pendingWrites) {
        write(transaction);
      }
      const docRef = coll.doc();
      transaction.create(docRef, {exact: {score: value, user: id}});
      return;
    }

    const min = range.min;
    const max = range.max;

    for (const node of snapshot.docs) {
      const data = node.data();
      if (data.exact !== undefined) {
        // This node held an exact score.
        const newRange = bucket(value, min, max);
        const tempRange = bucket(data.exact.score, min, max);

        if (newRange.min === tempRange.min &&
          newRange.max === tempRange.max) {
          // The scores belong in the same range, so we need to "demote" both
          // to a lower level of the tree and convert this node to a range.
          const rangeData = {
            range: newRange,
            count: 2,
          };
          for (const write of pendingWrites) {
            write(transaction);
          }
          const docReference = node.ref;
          transaction.set(docReference, rangeData);
          transaction.create(docReference.collection("scores").doc(), data);
          transaction.create(
              docReference.collection("scores").doc(),
              {exact: {score: value, user: id}},
          );
          return;
        } else {
          // The scores are in different ranges. Continue and try to find a
          // range that fits this score.
          continue;
        }
      }

      if (data.range.min <= value && data.range.max > value) {
        // The score belongs to this range that may have subvalues.
        // Increment the range's count in pendingWrites, since
        // subsequent recursion may incur more reads.
        const docReference = node.ref;
        const newCount = node.get("count") + 1;
        pendingWrites.push((t) => {
          t.update(docReference, {count: newCount});
        });
        const newRange = bucket(value, min, max);
        return writeScoreToCollection(
            id,
            value,
            docReference.collection("scores"),
            newRange,
            transaction,
            pendingWrites,
        );
      }
    }

    // No appropriate range was found, create an `exact` value.
    transaction.create(coll.doc(), {exact: {score: value, user: id}});
  }

  const scores = firestore.collection("scores");
  const players = firestore.collection("players");
  return firestore.runTransaction((transaction) => {
    return writeScoreToCollection(
        playerID, score, scores, {min: 0, max: 1000}, transaction, [],
    ).then(() => {
      transaction.create(players.doc(), {
        user: playerID,
        score: score,
      });
    });
  });
}

এটি অবশ্যই আমাদের শেষ বাস্তবায়নের চেয়ে আরও জটিল, যা একটি একক পদ্ধতি কল এবং কোডের মাত্র ছয় লাইন ছিল। একবার আপনি এই পদ্ধতিটি প্রয়োগ করার পরে, ডাটাবেসে কয়েকটি স্কোর যোগ করার চেষ্টা করুন এবং ফলস্বরূপ গাছের গঠন পর্যবেক্ষণ করুন। আপনার JS কনসোলে:

leaderboard.addScores();

ফলস্বরূপ ডাটাবেস গঠনটি দেখতে এইরকম কিছু হওয়া উচিত, গাছের গঠন স্পষ্টভাবে দৃশ্যমান এবং গাছের পাতাগুলি পৃথক স্কোরের প্রতিনিধিত্ব করে।

scores
  - document
    range: 0-333.33
    count: 2
    scores:
      - document
        exact:
          score: 18
          user: 1
      - document
        exact:
          score: 22
          user: 2

এখন যেহেতু আমাদের কঠিন অংশটি পথের বাইরে রয়েছে, আমরা আগে বর্ণিত হিসাবে গাছটি অতিক্রম করে স্কোর পড়তে পারি।

async function readRank(playerID, firestore) {
  const players = await firestore.collection("players")
      .where("user", "==", playerID).get();
  if (players.empty) {
    throw Error(`Player not found in leaderboard: ${playerID}`);
  }
  if (players.size > 1) {
    console.info(`Multiple scores with player ${playerID}, fetching first`);
  }
  const player = players.docs[0].data();
  const score = player.score;

  const scores = firestore.collection("scores");

  /**
   * Recursively finds a player score in a collection.
   * @param {string} id The player's ID, since some players may be tied.
   * @param {number} value The player's score.
   * @param {admin.firestore.CollectionReference} coll The collection to
   *     search.
   * @param {number} currentCount The current count of players ahead of the
   *     player.
   * @return {Promise<number>} The rank of the player (the number of players
   *     ahead of them plus one).
   */
  async function findPlayerScoreInCollection(id, value, coll, currentCount) {
    const snapshot = await coll.get();
    for (const doc of snapshot.docs) {
      if (doc.get("exact") !== undefined) {
        // This is an exact score. If it matches the score we're looking
        // for, return. Otherwise, check if it should be counted.
        const exact = doc.data().exact;
        if (exact.score === value) {
          if (exact.user === id) {
            // Score found.
            return currentCount + 1;
          } else {
            // The player is tied with another. In this case, don't increment
            // the count.
            continue;
          }
        } else if (exact.score > value) {
          // Increment count
          currentCount++;
          continue;
        } else {
          // Do nothing
          continue;
        }
      } else {
        // This is a range. If it matches the score we're looking for,
        // search the range recursively, otherwise, check if it should be
        // counted.
        const range = doc.data().range;
        const count = doc.get("count");
        if (range.min > value) {
          // The range is greater than the score, so add it to the rank
          // count.
          currentCount += count;
          continue;
        } else if (range.max <= value) {
          // do nothing
          continue;
        } else {
          const subcollection = doc.ref.collection("scores");
          return findPlayerScoreInCollection(
              id,
              value,
              subcollection,
              currentCount,
          );
        }
      }
    }
    // There was no range containing the score.
    throw Error(`Range not found for score: ${value}`);
  }

  const rank = await findPlayerScoreInCollection(playerID, score, scores, 0);
  return {
    user: playerID,
    rank: rank,
    score: score,
  };
}

আপডেট একটি অতিরিক্ত ব্যায়াম হিসাবে বাকি আছে. leaderboard.addScore(id, score) এবং leaderboard.getRank(id) পদ্ধতির সাহায্যে আপনার JS কনসোলে স্কোর যোগ করার এবং আনার চেষ্টা করুন এবং দেখুন কিভাবে Firebase কনসোলে আপনার লিডারবোর্ড পরিবর্তন হয়।

এই বাস্তবায়নের সাথে, যাইহোক, লগারিদমিক পারফরম্যান্স অর্জনের জন্য আমরা যে জটিলতা যোগ করেছি তা একটি খরচে আসে।

  • প্রথমত, এই লিডারবোর্ড বাস্তবায়ন লক বিতর্কের সমস্যায় পড়তে পারে, যেহেতু লেনদেনের জন্য দস্তাবেজগুলি সামঞ্জস্যপূর্ণ থাকে তা নিশ্চিত করতে লকিং রিড এবং লেখার প্রয়োজন হয়।
  • দ্বিতীয়ত, ফায়ারস্টোর 100 এর একটি সাব-কলেকশন গভীরতার সীমা আরোপ করে, যার অর্থ 100 টি স্কোরের পরে আপনাকে সাবট্রি তৈরি করা এড়াতে হবে, যা এই বাস্তবায়ন করে না।
  • এবং সবশেষে, এই লিডারবোর্ডটি শুধুমাত্র আদর্শ ক্ষেত্রেই লগারিদমিকভাবে স্কেল করে যেখানে গাছটি ভারসাম্যপূর্ণ-যদি এটি ভারসাম্যহীন হয়, এই লিডারবোর্ডের সবচেয়ে খারাপ কেস কর্মক্ষমতা আবার রৈখিক।

একবার আপনার হয়ে গেলে, Firebase কনসোলের মাধ্যমে scores এবং players সংগ্রহ মুছে ফেলুন এবং আমরা আমাদের শেষ লিডারবোর্ড বাস্তবায়নে চলে যাব।

6. একটি স্টোকাস্টিক (সম্ভাব্য) লিডারবোর্ড প্রয়োগ করুন

সন্নিবেশ কোড চালানোর সময়, আপনি লক্ষ্য করতে পারেন যে আপনি এটিকে সমান্তরালভাবে অনেকবার চালালে আপনার ফাংশনগুলি লেনদেন লক বিতর্ক সম্পর্কিত একটি ত্রুটি বার্তার সাথে ব্যর্থ হতে শুরু করবে। এর আশেপাশে এমন উপায় রয়েছে যা আমরা এই কোডল্যাবে অন্বেষণ করব না, তবে আপনার যদি সঠিক র‌্যাঙ্কিংয়ের প্রয়োজন না হয় তবে আপনি সহজ এবং দ্রুত উভয় কিছুর জন্য আগের পদ্ধতির সমস্ত জটিলতা বাদ দিতে পারেন। আসুন আমরা কীভাবে সঠিক র‌্যাঙ্কিংয়ের পরিবর্তে আমাদের খেলোয়াড়দের স্কোরের জন্য একটি আনুমানিক র‌্যাঙ্ক ফেরত দিতে পারি এবং কীভাবে এটি আমাদের ডাটাবেসের যুক্তি পরিবর্তন করে তা দেখে নেওয়া যাক।

এই পদ্ধতির জন্য, আমরা আমাদের লিডারবোর্ডকে 100টি বালতিতে ভাগ করব, প্রতিটি স্কোরের প্রায় এক শতাংশ প্রতিনিধিত্ব করে যা আমরা পাওয়ার আশা করছি। এই পদ্ধতিটি আমাদের স্কোর বিতরণের জ্ঞান ছাড়াই কাজ করে, এই ক্ষেত্রে আমাদের বালতি জুড়ে স্কোরগুলির মোটামুটি সমান বিতরণের গ্যারান্টি দেওয়ার কোনও উপায় নেই, তবে আমরা যদি জানি যে আমাদের স্কোরগুলি কীভাবে বিতরণ করা হবে তবে আমরা আমাদের অনুমানে আরও নির্ভুলতা অর্জন করব .

আমাদের পদ্ধতি নিম্নরূপ: আগের মতো, প্রতিটি বালতি ভিতরে স্কোরের সংখ্যা এবং স্কোরের পরিসরের সংখ্যা সংরক্ষণ করে। একটি নতুন স্কোর সন্নিবেশ করার সময়, আমরা স্কোরের জন্য বালতি খুঁজে বের করব এবং এর সংখ্যা বৃদ্ধি করব। একটি র‍্যাঙ্ক আনার সময়, আমরা কেবল এটির আগে বালতিগুলি যোগ করব এবং তারপরে আরও অনুসন্ধান করার পরিবর্তে আমাদের বালতির মধ্যে আনুমানিক করব। এটি আমাদের খুব সুন্দর ধ্রুবক সময় লুকআপ এবং সন্নিবেশ দেয় এবং অনেক কম কোডের প্রয়োজন হয়।

প্রথম, সন্নিবেশ:

// Add this line to the top of your file.
const admin = require("firebase-admin");

// Implement this method (again).
async function createScore(playerID, score, firestore) {
  const scores = await firestore.collection("scores").get();
  if (scores.empty) {
    // Create the buckets since they don't exist yet.
    // In a real app, don't do this in your write function. Do it once
    // manually and then keep the buckets in your database forever.
    for (let i = 0; i < 10; i++) {
      const min = i * 100;
      const max = (i + 1) * 100;
      const data = {
        range: {
          min: min,
          max: max,
        },
        count: 0,
      };
      await firestore.collection("scores").doc().create(data);
    }
    throw Error("Database not initialized");
  }

  const buckets = await firestore.collection("scores")
      .where("range.min", "<=", score).get();
  for (const bucket of buckets.docs) {
    const range = bucket.get("range");
    if (score < range.max) {
      const writeBatch = firestore.batch();
      const playerDoc = firestore.collection("players").doc();
      writeBatch.create(playerDoc, {
        user: playerID,
        score: score,
      });
      writeBatch.update(
          bucket.ref,
          {count: admin.firestore.FieldValue.increment(1)},
      );
      const scoreDoc = bucket.ref.collection("scores").doc();
      writeBatch.create(scoreDoc, {
        user: playerID,
        score: score,
      });
      return writeBatch.commit();
    }
  }
}

আপনি লক্ষ্য করবেন যে এই সন্নিবেশ কোডটির উপরে আপনার ডাটাবেস স্থিতি শুরু করার জন্য কিছু যুক্তি রয়েছে যাতে উত্পাদনে এরকম কিছু না করার জন্য একটি সতর্কতা রয়েছে। আরম্ভ করার জন্য কোডটি জাতিগত অবস্থার বিরুদ্ধে মোটেও সুরক্ষিত নয়, তাই আপনি যদি এটি করতে চান, একাধিক সমসাময়িক লেখা আপনাকে একগুচ্ছ ডুপ্লিকেট বালতি দিয়ে আপনার ডাটাবেসকে দূষিত করবে।

এগিয়ে যান এবং আপনার ফাংশন স্থাপন করুন এবং তারপর শূন্য গণনা সহ সমস্ত বালতি শুরু করতে একটি সন্নিবেশ চালান। এটি একটি ত্রুটি ফিরিয়ে দেবে, যা আপনি নিরাপদে উপেক্ষা করতে পারেন।

leaderboard.addScore(999, 0); // The params aren't important here.

এখন যেহেতু ডাটাবেস সঠিকভাবে শুরু হয়েছে, আমরা addScores চালাতে পারি এবং Firebase কনসোলে আমাদের ডেটার গঠন দেখতে পারি। ফলস্বরূপ কাঠামোটি আমাদের শেষ বাস্তবায়নের তুলনায় অনেক বেশি চাটুকার, যদিও তারা অতিমাত্রায় একই রকম।

leaderboard.addScores();

এবং, এখন, স্কোর পড়তে:

async function readRank(playerID, firestore) {
  const players = await firestore.collection("players")
      .where("user", "==", playerID).get();
  if (players.empty) {
    throw Error(`Player not found in leaderboard: ${playerID}`);
  }
  if (players.size > 1) {
    console.info(`Multiple scores with player ${playerID}, fetching first`);
  }
  const player = players.docs[0].data();
  const score = player.score;

  const scores = await firestore.collection("scores").get();
  let currentCount = 1; // Player is rank 1 if there's 0 better players.
  let interp = -1;
  for (const bucket of scores.docs) {
    const range = bucket.get("range");
    const count = bucket.get("count");
    if (score < range.min) {
      currentCount += count;
    } else if (score >= range.max) {
      // do nothing
    } else {
      // interpolate where the user is in this bucket based on their score.
      const relativePosition = (score - range.min) / (range.max - range.min);
      interp = Math.round(count - (count * relativePosition));
    }
  }

  if (interp === -1) {
    // Didn't find a correct bucket
    throw Error(`Score out of bounds: ${score}`);
  }

  return {
    user: playerID,
    rank: currentCount + interp,
    score: score,
  };
}

যেহেতু আমরা addScores ফাংশনটি স্কোরগুলির একটি অভিন্ন বন্টন তৈরি করেছি এবং আমরা বালতির মধ্যে লিনিয়ার ইন্টারপোলেশন ব্যবহার করছি, আমরা খুব সঠিক ফলাফল পাব, ব্যবহারকারীর সংখ্যা বাড়ালে আমাদের লিডারবোর্ডের কর্মক্ষমতা হ্রাস পাবে না, এবং গণনা আপডেট করার সময় আমাদের লক বিতর্ক (যতটা) নিয়ে চিন্তা করতে হবে না।

7. সংযোজন: প্রতারণা

অপেক্ষা করুন, আপনি হয়তো ভাবছেন, আমি যদি ব্রাউজার ট্যাবের JS কনসোলের মাধ্যমে আমার কোডল্যাবে মান লিখি, তাহলে কি আমার কোনো খেলোয়াড় লিডারবোর্ডে মিথ্যা বলতে পারে না এবং বলতে পারে যে তারা উচ্চ স্কোর পেয়েছে যা তারা করেনি মোটামুটি অর্জন?

হ্যাঁ, তারা পারে। আপনি যদি প্রতারণা রোধ করতে চান, তা করার সবচেয়ে শক্তিশালী উপায় হল নিরাপত্তা নিয়মের মাধ্যমে আপনার ডাটাবেসে ক্লায়েন্টের লেখা অক্ষম করা, আপনার ক্লাউড ফাংশনে সুরক্ষিত অ্যাক্সেস যাতে ক্লায়েন্টরা তাদের সরাসরি কল করতে না পারে, এবং তারপরে আপনার সার্ভারে ইন-গেম অ্যাকশন যাচাই করে। লিডারবোর্ডে স্কোর আপডেট পাঠানো হচ্ছে।

এটা মনে রাখা গুরুত্বপূর্ণ যে এই কৌশলটি প্রতারণার বিরুদ্ধে একটি প্রতিষেধক নয় – যথেষ্ট পরিমাণে প্রণোদনা দিয়ে, প্রতারকরা সার্ভার-সাইড বৈধতা এড়ানোর উপায় খুঁজে পেতে পারে এবং অনেক বড়, সফল ভিডিও গেম ক্রমাগত তাদের প্রতারকদের সাথে বিড়াল-মাউস খেলছে। নতুন প্রতারণা এবং প্রসারিত থেকে তাদের থামান. এই ঘটনার একটি কঠিন পরিণতি হল যে প্রতিটি গেমের জন্য সার্ভার-সাইড বৈধতা সহজাতভাবে নির্ধারিত হয়; যদিও ফায়ারবেস অ্যাপ চেকের মতো অ্যান্টি-ব্যবহার সরঞ্জাম সরবরাহ করে যা একজন ব্যবহারকারীকে একটি সাধারণ স্ক্রিপ্টেড ক্লায়েন্টের মাধ্যমে আপনার গেমটি অনুলিপি করতে বাধা দেয়, তবে ফায়ারবেস এমন কোনও পরিষেবা সরবরাহ করে না যা একটি সামগ্রিক অ্যান্টি-চিটের পরিমাণ।

সার্ভার-সাইড যাচাইকরণের কম কিছু, যথেষ্ট জনপ্রিয় গেমের জন্য বা প্রতারণার জন্য যথেষ্ট কম বাধা, একটি লিডারবোর্ডে পরিণত হবে যেখানে শীর্ষ মানগুলি সমস্ত প্রতারক।

8. অভিনন্দন

অভিনন্দন, আপনি সফলভাবে Firebase-এ চারটি ভিন্ন লিডারবোর্ড তৈরি করেছেন! সঠিকতা এবং গতির জন্য আপনার গেমের প্রয়োজনীয়তার উপর নির্ভর করে, আপনি যুক্তিসঙ্গত খরচে আপনার জন্য কাজ করে এমন একটি বেছে নিতে সক্ষম হবেন।

পরবর্তীতে, গেমগুলির জন্য শেখার পথগুলি দেখুন৷