ক্লাউড স্প্যানার গেম ডেভেলপমেন্টের সাথে শুরু হচ্ছে

১. ভূমিকা

ক্লাউড স্প্যানার হলো একটি সম্পূর্ণভাবে পরিচালিত, হরাইজন্টালি স্কেলেবল, বিশ্বব্যাপী বিতরণযোগ্য রিলেশনাল ডাটাবেস পরিষেবা, যা পারফরম্যান্স ও উচ্চ প্রাপ্যতা বজায় রেখে ACID ট্রানজ্যাকশন এবং SQL সিম্যান্টিকস প্রদান করে।

এই বৈশিষ্ট্যগুলোর কারণে, স্প্যানার এমন গেমের আর্কিটেকচারের জন্য দারুণ উপযোগী, যেগুলো বিশ্বব্যাপী প্লেয়ার বেস তৈরি করতে চায় অথবা ডেটার সামঞ্জস্যতা নিয়ে চিন্তিত।

এই ল্যাবে, আপনি দুটি Go সার্ভিস তৈরি করবেন যা একটি আঞ্চলিক স্প্যানার ডেটাবেসের সাথে যোগাযোগ করে খেলোয়াড়দের সাইন আপ করতে এবং খেলা শুরু করতে সক্ষম করবে।

413fdd57bb0b68bc.png

এরপর আপনি পাইথন লোড ফ্রেমওয়ার্ক Locust.io ব্যবহার করে ডেটা তৈরি করবেন, যা খেলোয়াড়দের সাইন আপ করা এবং গেমটি খেলার বিষয়টি সিমুলেট করবে। এবং তারপর আপনি স্প্যানারকে কোয়েরি করে নির্ধারণ করবেন কতজন খেলোয়াড় খেলছে, এবং খেলোয়াড়দের জেতা গেম বনাম খেলা গেমের পরিসংখ্যান জানবেন।

অবশেষে, এই ল্যাবে তৈরি করা রিসোর্সগুলো আপনি পরিষ্কার করবেন।

আপনি যা তৈরি করবেন

এই ল্যাবের অংশ হিসেবে, আপনি যা করবেন:

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

আপনি যা শিখবেন

  • কীভাবে একটি ক্লাউড স্প্যানার ইনস্ট্যান্স সেটআপ করবেন
  • কীভাবে একটি গেম ডাটাবেস এবং স্কিমা তৈরি করবেন
  • ক্লাউড স্প্যানারের সাথে কাজ করার জন্য কীভাবে গো অ্যাপস স্থাপন করবেন
  • লোকাস্ট ব্যবহার করে কীভাবে ডেটা তৈরি করবেন
  • গেম এবং খেলোয়াড়দের সম্পর্কে প্রশ্নের উত্তর পেতে ক্লাউড স্প্যানারে কীভাবে ডেটা কোয়েরি করবেন।

আপনার যা যা লাগবে

  • একটি গুগল ক্লাউড প্রজেক্ট যা একটি বিলিং অ্যাকাউন্টের সাথে সংযুক্ত।
  • একটি ওয়েব ব্রাউজার, যেমন ক্রোম বা ফায়ারফক্স

২. সেটআপ এবং প্রয়োজনীয়তা

একটি প্রকল্প তৈরি করুন

আপনার যদি আগে থেকে কোনো গুগল অ্যাকাউন্ট (জিমেইল বা গুগল অ্যাপস) না থাকে, তবে আপনাকে অবশ্যই একটি তৈরি করতে হবে। গুগল ক্লাউড প্ল্যাটফর্ম কনসোলে ( console.cloud.google.com ) সাইন-ইন করুন এবং একটি নতুন প্রজেক্ট তৈরি করুন।

আপনার যদি আগে থেকেই কোনো প্রজেক্ট থাকে, তাহলে কনসোলের উপরের বাম দিকের প্রজেক্ট সিলেকশন পুল-ডাউন মেনুতে ক্লিক করুন:

6c9406d9b014760.png

এবং একটি নতুন প্রজেক্ট তৈরি করতে, প্রাপ্ত ডায়ালগ বক্সে থাকা 'NEW PROJECT' বোতামটিতে ক্লিক করুন:

949d83c8a4ee17d9.png

আপনার যদি আগে থেকে কোনো প্রজেক্ট না থাকে, তাহলে আপনার প্রথম প্রজেক্টটি তৈরি করার জন্য এইরকম একটি ডায়ালগ বক্স দেখতে পাবেন:

870a3cbd6541ee86.png

পরবর্তী প্রজেক্ট তৈরির ডায়ালগ বক্সে আপনি আপনার নতুন প্রজেক্টের বিবরণ লিখতে পারবেন:

6a92c57d3250a4b3.png

প্রজেক্ট আইডিটি মনে রাখবেন, যা সমস্ত গুগল ক্লাউড প্রজেক্ট জুড়ে একটি অনন্য নাম (উপরের নামটি ইতিমধ্যে ব্যবহৃত হয়েছে এবং আপনার জন্য কাজ করবে না, দুঃখিত!)। এই কোডল্যাবে এটিকে পরবর্তীতে PROJECT_ID হিসাবে উল্লেখ করা হবে।

এরপরে, যদি আপনি আগে থেকে তা না করে থাকেন, তাহলে Google Cloud রিসোর্স ব্যবহার করতে এবং Cloud Spanner API সক্রিয় করতে আপনাকে ডেভেলপার কনসোলে বিলিং চালু করতে হবে।

15d0ef27a8fbab27.png

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

গুগল ক্লাউড প্ল্যাটফর্মের নতুন ব্যবহারকারীরা ৩০০ ডলারের একটি ফ্রি ট্রায়ালের জন্য যোগ্য, যার ফলে এই কোডল্যাবটি সম্পূর্ণ বিনামূল্যে পাওয়া যাবে।

গুগল ক্লাউড শেল সেটআপ

যদিও গুগল ক্লাউড এবং স্প্যানার আপনার ল্যাপটপ থেকে দূরবর্তীভাবে পরিচালনা করা যায়, এই কোডল্যাবে আমরা গুগল ক্লাউড শেল ব্যবহার করব, যা ক্লাউডে চালিত একটি কমান্ড লাইন পরিবেশ।

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

  1. ক্লাউড কনসোল থেকে ক্লাউড শেল সক্রিয় করতে, কেবল 'Activate Cloud Shell'-এ ক্লিক করুন। gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A (পরিবেশের জন্য ব্যবস্থা করতে এবং সংযোগ স্থাপন করতে মাত্র কয়েক মুহূর্ত সময় লাগা উচিত)।

JjEuRXGg0AYYIY6QZ8d-66gx_Mtc-_jDE9ijmbXLJSAXFvJt-qUpNtsBsYjNpv2W6BQSr Dc1D-ARINNQ-1EkwUhz-iUK-FUCZhJ-NtjvIEx9pIkE-246DomWuCfiGHK78DgoeWkHRw

Screen Shot 2017-06-14 at 10.13.43 PM.png

ক্লাউড শেলে সংযুক্ত হওয়ার পর আপনি দেখতে পাবেন যে, আপনাকে ইতিমধ্যেই প্রমাণীকৃত করা হয়েছে এবং প্রজেক্টটি আপনার PROJECT_ID-তে সেট করা আছে।

gcloud auth list

কমান্ড আউটপুট

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

কমান্ড আউটপুট

[core]
project = <PROJECT_ID>

যদি কোনো কারণে প্রজেক্টটি সেট করা না থাকে, তাহলে নিম্নলিখিত কমান্ডটি দিন:

gcloud config set project <PROJECT_ID>

আপনার PROJECT_ID খুঁজছেন? সেটআপের ধাপগুলিতে আপনি কোন আইডি ব্যবহার করেছিলেন তা দেখে নিন অথবা ক্লাউড কনসোল ড্যাশবোর্ডে এটি খুঁজে দেখুন:

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

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

echo $GOOGLE_CLOUD_PROJECT

কমান্ড আউটপুট

<PROJECT_ID>

কোডটি ডাউনলোড করুন

ক্লাউড শেলে আপনি এই ল্যাবের কোডটি ডাউনলোড করতে পারবেন। এটি v0.1.0 রিলিজের উপর ভিত্তি করে তৈরি, তাই ওই ট্যাগটি দেখে নিন:

git clone https://github.com/cloudspannerecosystem/spanner-gaming-sample.git
cd spanner-gaming-sample/

# Check out v0.1.0 release
git checkout tags/v0.1.0 -b v0.1.0-branch

কমান্ড আউটপুট

Cloning into 'spanner-gaming-sample'...
*snip*
Switched to a new branch 'v0.1.0-branch'

লোকাস্ট লোড জেনারেটর সেটআপ করুন

লোকাস্ট হলো একটি পাইথন লোড টেস্টিং ফ্রেমওয়ার্ক যা REST API এন্ডপয়েন্ট পরীক্ষা করার জন্য উপযোগী। এই কোডল্যাবে, 'generators' ডিরেক্টরিতে আমাদের দুটি ভিন্ন লোড টেস্ট রয়েছে যা আমরা তুলে ধরব:

  • authentication_server.py : এতে প্লেয়ার তৈরি করার এবং সিঙ্গেল পয়েন্ট লুকআপ অনুকরণের জন্য র‍্যান্ডমভাবে একজন প্লেয়ার পাওয়ার টাস্ক রয়েছে।
  • match_server.py : এতে গেম তৈরি এবং বন্ধ করার টাস্ক রয়েছে। গেম তৈরি করলে বর্তমানে কোনো গেম খেলছে না এমন ১০০ জন র‍্যান্ডম প্লেয়ারকে গেমে যুক্ত করা হবে। গেম বন্ধ করলে games_played এবং games_won পরিসংখ্যান আপডেট হবে এবং ওই প্লেয়ারদের ভবিষ্যতের কোনো গেমে যুক্ত করার সুযোগ পাওয়া যাবে।

ক্লাউড শেলে লোকাস্ট চালু করতে আপনার পাইথন ৩.৭ বা তার উচ্চতর সংস্করণ প্রয়োজন হবে। ক্লাউড শেলের সাথে পাইথন ৩.৯ দেওয়া থাকে, তাই সংস্করণটি যাচাই করা ছাড়া আর কিছু করার নেই।

python -V

কমান্ড আউটপুট

Python 3.9.12

এখন, আপনি Locust-এর জন্য প্রয়োজনীয় জিনিসগুলো ইনস্টল করতে পারেন।

pip3 install -r requirements.txt

কমান্ড আউটপুট

Collecting locust==2.11.1
*snip*
Successfully installed ConfigArgParse-1.5.3 Flask-BasicAuth-0.2.0 Flask-Cors-3.0.10 brotli-1.0.9 gevent-21.12.0 geventhttpclient-2.0.2 greenlet-1.1.3 locust-2.11.1 msgpack-1.0.4 psutil-5.9.2 pyzmq-22.3.0 roundrobin-0.0.4 zope.event-4.5.0 zope.interface-5.4.0

এখন, PATH আপডেট করুন যাতে নতুন ইনস্টল করা locust বাইনারিটি খুঁজে পাওয়া যায়:

PATH=~/.local/bin":$PATH"
which locust

কমান্ড আউটপুট

/home/<user>/.local/bin/locust

সারসংক্ষেপ

এই ধাপে আপনি আপনার প্রজেক্টটি (যদি আগে থেকে তৈরি না থাকে) তৈরি করেছেন, ক্লাউড শেল সক্রিয় করেছেন এবং এই ল্যাবের জন্য কোড ডাউনলোড করেছেন।

সবশেষে, আপনি ল্যাবে পরবর্তীতে লোড জেনারেশনের জন্য লোকাস্ট সেট আপ করবেন।

এরপরে

এরপরে, আপনি ক্লাউড স্প্যানার ইনস্ট্যান্স এবং ডেটাবেস সেট আপ করবেন।

৩. একটি স্প্যানার ইনস্ট্যান্স এবং ডাটাবেস তৈরি করুন

স্প্যানার ইনস্ট্যান্স তৈরি করুন

এই ধাপে আমরা কোডল্যাবের জন্য আমাদের স্প্যানার ইনস্ট্যান্স সেট আপ করব। স্প্যানার এন্ট্রিটি অনুসন্ধান করুন। 1a6580bd3d3e6783.png বাম উপরের হ্যামবার্গার মেনুতে 3129589f7bc9e5ce.png অথবা "/" চেপে "Spanner" টাইপ করে স্প্যানার অনুসন্ধান করুন।

36e52f8df8e13b99.png

এরপর, ক্লিক করুন 95269e75bc8c3e4d.png এবং আপনার ইনস্ট্যান্সের জন্য ইনস্ট্যান্সের নাম cloudspanner-gaming লিখে, একটি কনফিগারেশন বেছে নিয়ে (যেমন us-central1 এর মতো একটি আঞ্চলিক ইনস্ট্যান্স নির্বাচন করুন), এবং নোডের সংখ্যা নির্ধারণ করে ফর্মটি পূরণ করুন। এই কোডল্যাবের জন্য আমাদের কেবল 500 processing units প্রয়োজন হবে।

সবশেষে, 'Create'-এ ক্লিক করুন এবং কয়েক সেকেন্ডের মধ্যেই আপনার ব্যবহারের জন্য একটি ক্লাউড স্প্যানার ইনস্ট্যান্স পেয়ে যাবেন।

4457c324c94f93e6.png

ডাটাবেস এবং স্কিমা তৈরি করুন

আপনার ইনস্ট্যান্সটি চালু হয়ে গেলে, আপনি ডেটাবেস তৈরি করতে পারবেন। স্প্যানার একটিমাত্র ইনস্ট্যান্সে একাধিক ডেটাবেস ব্যবহারের সুযোগ দেয়।

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

মাল্টি-রিজিওনাল ইনস্ট্যান্সগুলিতে, আপনি ডিফল্ট লিডারও কনফিগার করতে পারেন। স্প্যানারে ডাটাবেস সম্পর্কে আরও পড়ুন

এই কোড-ল্যাবের জন্য, আপনি ডিফল্ট অপশনগুলো ব্যবহার করে ডাটাবেস তৈরি করবেন এবং তৈরির সময় স্কিমা সরবরাহ করবেন।

এই ল্যাবে দুটি টেবিল তৈরি করা হবে: খেলোয়াড় এবং খেলা

77651ac12e47fe2a.png

খেলোয়াড়রা সময়ের সাথে সাথে অনেক খেলায় অংশগ্রহণ করতে পারে, কিন্তু একবারে কেবল একটি খেলাই খেলতে পারে। খেলোয়াড়দের 'games_played' এবং 'games_won'-এর মতো আকর্ষণীয় পরিসংখ্যানের হিসাব রাখার জন্য JSON ডেটা টাইপ হিসেবে 'stats'- ও থাকে। যেহেতু পরবর্তীতে অন্যান্য পরিসংখ্যান যোগ করা হতে পারে, তাই এটি খেলোয়াড়দের জন্য কার্যত একটি স্কিমাবিহীন কলাম।

গেমগুলো স্প্যানারের ARRAY ডেটা টাইপ ব্যবহার করে অংশগ্রহণকারী খেলোয়াড়দের হিসাব রাখে। গেমটি বন্ধ না হওয়া পর্যন্ত এর বিজয়ী এবং সমাপ্ত অ্যাট্রিবিউটগুলো পূরণ করা হয় না।

খেলোয়াড়ের বর্তমান গেমটি একটি বৈধ গেম কিনা তা নিশ্চিত করার জন্য একটি ফরেন কী রয়েছে।

এখন ইনস্ট্যান্স ওভারভিউতে 'Create Database'-এ ক্লিক করে ডেটাবেসটি তৈরি করুন:

a820db6c4a4d6f2d.png

এরপর বিস্তারিত তথ্য পূরণ করুন। গুরুত্বপূর্ণ অপশনগুলো হলো ডাটাবেসের নাম এবং ডায়ালেক্ট। এই উদাহরণে, আমরা ডাটাবেসটির নাম দিয়েছি sample-game এবং Google Standard SQL ডায়ালেক্টটি বেছে নিয়েছি।

স্কিমার জন্য, এই DDL-টি কপি করে বক্সে পেস্ট করুন:

CREATE TABLE games (
  gameUUID STRING(36) NOT NULL,
  players ARRAY<STRING(36)> NOT NULL,
  winner STRING(36),
  created TIMESTAMP,
  finished TIMESTAMP,
) PRIMARY KEY(gameUUID);

CREATE TABLE players (
  playerUUID STRING(36) NOT NULL,
  player_name STRING(64) NOT NULL,
  email STRING(MAX) NOT NULL,
  password_hash BYTES(60) NOT NULL,
  created TIMESTAMP,
  updated TIMESTAMP,
  stats JSON,
  account_balance NUMERIC NOT NULL DEFAULT (0.00),
  is_logged_in BOOL,
  last_login TIMESTAMP,
  valid_email BOOL,
  current_game STRING(36),
  FOREIGN KEY (current_game) REFERENCES games (gameUUID),
) PRIMARY KEY(playerUUID);

CREATE UNIQUE INDEX PlayerAuthentication ON players(email) STORING (password_hash);

CREATE INDEX PlayerGame ON players(current_game);

CREATE UNIQUE INDEX PlayerName ON players(player_name);

এরপর, ক্রিয়েট বাটনে ক্লিক করুন এবং আপনার ডাটাবেসটি তৈরি হওয়ার জন্য কয়েক সেকেন্ড অপেক্ষা করুন।

ডেটাবেস তৈরির পৃষ্ঠাটি দেখতে এইরকম হওয়া উচিত:

d39d358dc7d32939.png

এখন, কোড ল্যাবে পরে ব্যবহারের জন্য আপনাকে ক্লাউড শেলে কিছু এনভায়রনমেন্ট ভেরিয়েবল সেট করতে হবে। তাই ইনস্ট্যান্স-আইডিটি নোট করে নিন এবং ক্লাউড শেলে INSTANCE_ID ও DATABASE_ID সেট করুন।

f6f98848d3aea9c.png

export SPANNER_PROJECT_ID=$GOOGLE_CLOUD_PROJECT
export SPANNER_INSTANCE_ID=cloudspanner-gaming
export SPANNER_DATABASE_ID=sample-game

সারসংক্ষেপ

এই ধাপে আপনি একটি স্প্যানার ইনস্ট্যান্স এবং স্যাম্পল-গেম ডেটাবেস তৈরি করেছেন। আপনি এই স্যাম্পল গেমটি যে স্কিমা ব্যবহার করে, সেটিও সংজ্ঞায়িত করেছেন।

এরপরে

এরপরে, খেলোয়াড়দের গেমটি খেলার জন্য সাইন আপ করার সুযোগ দিতে আপনি প্রোফাইল সার্ভিসটি চালু করবেন!

৪. প্রোফাইল পরিষেবাটি স্থাপন করুন

পরিষেবার সংক্ষিপ্ত বিবরণ

প্রোফাইল সার্ভিসটি হলো Go-তে লেখা একটি REST API যা gin ফ্রেমওয়ার্ক ব্যবহার করে।

4fce45ee6c858b3e.png

এই এপিআই-তে খেলোয়াড়রা গেম খেলার জন্য সাইন আপ করতে পারে। এটি একটি সাধারণ POST কমান্ডের মাধ্যমে তৈরি করা হয়, যা খেলোয়াড়ের নাম, ইমেল এবং পাসওয়ার্ড গ্রহণ করে। পাসওয়ার্ডটি bcrypt দিয়ে এনক্রিপ্ট করা হয় এবং এর হ্যাশটি ডেটাবেসে সংরক্ষণ করা হয়।

ইমেলকে একটি অনন্য শনাক্তকারী হিসেবে গণ্য করা হয়, অপরদিকে গেমের প্রদর্শনের উদ্দেশ্যে প্লেয়ারের নাম ব্যবহার করা হয়।

এই এপিআই-টিতে বর্তমানে লগইনের ব্যবস্থা নেই, তবে এটি বাস্তবায়নের কাজটি আপনি একটি অতিরিক্ত অনুশীলন হিসেবে করতে পারেন।

প্রোফাইল সার্ভিসের জন্য ./src/golang/profile-service/main.go ফাইলটি নিম্নরূপ দুটি প্রধান এন্ডপয়েন্ট প্রকাশ করে:

func main() {
   configuration, _ := config.NewConfig()

   router := gin.Default()
   router.SetTrustedProxies(nil)

   router.Use(setSpannerConnection(configuration))

   router.POST("/players", createPlayer)
   router.GET("/players", getPlayerUUIDs)
   router.GET("/players/:id", getPlayerByID)

   router.Run(configuration.Server.URL())
}

এবং সেই এন্ডপয়েন্টগুলোর কোড প্লেয়ার মডেলে রাউট করা হবে।

func getPlayerByID(c *gin.Context) {
   var playerUUID = c.Param("id")

   ctx, client := getSpannerConnection(c)

   player, err := models.GetPlayerByUUID(ctx, client, playerUUID)
   if err != nil {
       c.IndentedJSON(http.StatusNotFound, gin.H{"message": "player not found"})
       return
   }

   c.IndentedJSON(http.StatusOK, player)
}

func createPlayer(c *gin.Context) {
   var player models.Player

   if err := c.BindJSON(&player); err != nil {
       c.AbortWithError(http.StatusBadRequest, err)
       return
   }

   ctx, client := getSpannerConnection(c)
   err := player.AddPlayer(ctx, client)
   if err != nil {
       c.AbortWithError(http.StatusBadRequest, err)
       return
   }

   c.IndentedJSON(http.StatusCreated, player.PlayerUUID)
}

সার্ভিসটির প্রথম কাজগুলোর মধ্যে একটি হলো স্প্যানার কানেকশন সেট করা। সার্ভিসটির জন্য সেশন পুল তৈরি করতে এটি সার্ভিস লেভেলে প্রয়োগ করা হয়।

func setSpannerConnection() gin.HandlerFunc {
   ctx := context.Background()
   client, err := spanner.NewClient(ctx, configuration.Spanner.URL())

   if err != nil {
       log.Fatal(err)
   }

   return func(c *gin.Context) {
       c.Set("spanner_client", *client)
       c.Set("spanner_context", ctx)
       c.Next()
   }
}

Player এবং PlayerStats হলো নিম্নলিখিতভাবে সংজ্ঞায়িত স্ট্রাক্ট:

type Player struct {
   PlayerUUID      string `json:"playerUUID" validate:"omitempty,uuid4"`
   Player_name     string `json:"player_name" validate:"required_with=Password Email"`
   Email           string `json:"email" validate:"required_with=Player_name Password,email"`
   // not stored in DB
   Password        string `json:"password" validate:"required_with=Player_name Email"` 
   // stored in DB
   Password_hash   []byte `json:"password_hash"`                                       
   created         time.Time
   updated         time.Time
   Stats           spanner.NullJSON `json:"stats"`
   Account_balance big.Rat          `json:"account_balance"`
   last_login      time.Time
   is_logged_in    bool
   valid_email     bool
   Current_game    string `json:"current_game" validate:"omitempty,uuid4"`
}

type PlayerStats struct {
   Games_played spanner.NullInt64 `json:"games_played"`
   Games_won    spanner.NullInt64 `json:"games_won"`
}

প্লেয়ার যোগ করার ফাংশনটি একটি ReadWrite ট্রানজ্যাকশনের ভিতরে DML ইনসার্ট ব্যবহার করে, কারণ প্লেয়ার যোগ করা একটি একক স্টেটমেন্ট, ব্যাচ ইনসার্ট নয়। ফাংশনটি দেখতে এইরকম:

func (p *Player) AddPlayer(ctx context.Context, client spanner.Client) error {
   // Validate based on struct validation rules
   err := p.Validate()
   if err != nil {
       return err
   }

   // take supplied password+salt, hash. Store in user_password
   passHash, err := hashPassword(p.Password)

   if err != nil {
       return errors.New("Unable to hash password")
   }

   p.Password_hash = passHash

   // Generate UUIDv4
   p.PlayerUUID = generateUUID()

   // Initialize player stats
   emptyStats := spanner.NullJSON{Value: PlayerStats{
       Games_played: spanner.NullInt64{Int64: 0, Valid: true},
       Games_won:    spanner.NullInt64{Int64: 0, Valid: true},
   }, Valid: true}

   // insert into spanner
   _, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
       stmt := spanner.Statement{
           SQL: `INSERT players (playerUUID, player_name, email, password_hash, created, stats) VALUES
                   (@playerUUID, @playerName, @email, @passwordHash, CURRENT_TIMESTAMP(), @pStats)
           `,
           Params: map[string]interface{}{
               "playerUUID":   p.PlayerUUID,
               "playerName":   p.Player_name,
               "email":        p.Email,
               "passwordHash": p.Password_hash,
               "pStats":       emptyStats,
           },
       }

       _, err := txn.Update(ctx, stmt)
       return err
   })
   if err != nil {
       return err
   }
   // return empty error on success
   return nil
}

UUID-এর ভিত্তিতে কোনো খেলোয়াড়কে খুঁজে বের করতে একটি সাধারণ রিড অপারেশন চালানো হয়। এর মাধ্যমে খেলোয়াড়ের playerUUID, player_name, email এবং stats পাওয়া যায়।

func GetPlayerByUUID(ctx context.Context, client spanner.Client, uuid string) (Player, error) {
   row, err := client.Single().ReadRow(ctx, "players",
       spanner.Key{uuid}, []string{"playerUUID", "player_name", "email", "stats"})
   if err != nil {
       return Player{}, err
   }

   player := Player{}
   err = row.ToStruct(&player)

   if err != nil {
       return Player{}, err
   }
   return player, nil
}

ডিফল্টরূপে, সার্ভিসটি এনভায়রনমেন্ট ভেরিয়েবল ব্যবহার করে কনফিগার করা হয়। ./src/golang/profile-service/config/config.go ফাইলের প্রাসঙ্গিক অংশটি দেখুন।

func NewConfig() (Config, error) {
   *snip*
   // Server defaults
   viper.SetDefault("server.host", "localhost")
   viper.SetDefault("server.port", 8080)

   // Bind environment variable override
   viper.BindEnv("server.host", "SERVICE_HOST")
   viper.BindEnv("server.port", "SERVICE_PORT")
   viper.BindEnv("spanner.project_id", "SPANNER_PROJECT_ID")
   viper.BindEnv("spanner.instance_id", "SPANNER_INSTANCE_ID")
   viper.BindEnv("spanner.database_id", "SPANNER_DATABASE_ID")

   *snip*

   return c, nil
}

আপনি দেখতে পাচ্ছেন যে, ডিফল্ট আচরণ অনুযায়ী সার্ভিসটি localhost:8080-এ চালু হয়।

এই তথ্য দিয়ে এখন পরিষেবাটি চালু করার সময় হয়েছে।

প্রোফাইল পরিষেবাটি চালান

go কমান্ড ব্যবহার করে সার্ভিসটি চালান। এটি ডিপেন্ডেন্সিগুলো ডাউনলোড করবে এবং পোর্ট ৮০৮০-তে সার্ভিসটি চালু করবে:

cd ~/spanner-gaming-sample/src/golang/profile-service
go run . &

কমান্ড আউটপুট:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /players                  --> main.createPlayer (4 handlers)
[GIN-debug] GET    /players                  --> main.getPlayerUUIDs (4 handlers)
[GIN-debug] GET    /players/:id              --> main.getPlayerByID (4 handlers)
[GIN-debug] GET    /players/:id/stats        --> main.getPlayerStats (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8080

একটি curl কমান্ড দিয়ে সার্ভিসটি পরীক্ষা করুন:

curl http://localhost:8080/players \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"email": "test@gmail.com","password": "s3cur3P@ss","player_name": "Test Player"}'

কমান্ড আউটপুট:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <date> 18:55:08 GMT
Content-Length: 38

"506a1ab6-ee5b-4882-9bb1-ef9159a72989"

সারসংক্ষেপ

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

পরবর্তী পদক্ষেপ

পরবর্তী ধাপে, আপনি ম্যাচ-মেকিং সার্ভিসটি ডিপ্লয় করবেন।

৫. ম্যাচ-মেকিং পরিষেবাটি স্থাপন করুন।

পরিষেবার সংক্ষিপ্ত বিবরণ

ম্যাচ-মেকিং সার্ভিসটি হলো Go-তে লেখা একটি REST API, যা gin ফ্রেমওয়ার্ক ব্যবহার করে।

9aecd571df0dcd7c.png

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

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

ম্যাচমেকিং সার্ভিসের জন্য ./src/golang/matchmaking-service/main.go ফাইলটির গঠন ও কোড প্রোফাইল সার্ভিসের মতোই, তাই এখানে তার পুনরাবৃত্তি করা হয়নি। এই সার্ভিসটি নিম্নরূপ দুটি প্রধান এন্ডপয়েন্ট প্রদান করে:

func main() {
   router := gin.Default()
   router.SetTrustedProxies(nil)

   router.Use(setSpannerConnection())

   router.POST("/games/create", createGame)
   router.PUT("/games/close", closeGame)

   router.Run(configuration.Server.URL())
}

এই পরিষেবাটি একটি Game struct-এর পাশাপাশি সংক্ষিপ্ত Player এবং PlayerStats struct-ও প্রদান করে:

type Game struct {
   GameUUID string           `json:"gameUUID"`
   Players  []string         `json:"players"`
   Winner   string           `json:"winner"`
   Created  time.Time        `json:"created"`
   Finished spanner.NullTime `json:"finished"`
}

type Player struct {
   PlayerUUID   string           `json:"playerUUID"`
   Stats        spanner.NullJSON `json:"stats"`
   Current_game string           `json:"current_game"`
}

type PlayerStats struct {
   Games_played int `json:"games_played"`
   Games_won    int `json:"games_won"`
}

একটি গেম তৈরি করার জন্য, ম্যাচমেকিং পরিষেবাটি দৈবচয়নের মাধ্যমে এমন ১০০ জন খেলোয়াড়কে বেছে নেয় যারা বর্তমানে কোনো গেম খেলছেন না।

গেম তৈরি করতে এবং খেলোয়াড়দের নির্ধারণ করতে স্প্যানার মিউটেশন বেছে নেওয়া হয়, কারণ বড় ধরনের পরিবর্তনের ক্ষেত্রে মিউটেশন DML-এর চেয়ে বেশি কার্যকর।

// Create a new game and assign players
// Players that are not currently playing a game are eligble to be selected for the new game
// Current implementation allows for less than numPlayers to be placed in a game
func (g *Game) CreateGame(ctx context.Context, client spanner.Client) error {
   // Initialize game values
   g.GameUUID = generateUUID()

   numPlayers := 10

   // Create and assign
   _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
       var m []*spanner.Mutation

       // get players
       query := fmt.Sprintf("SELECT playerUUID FROM (SELECT playerUUID FROM players WHERE current_game IS NULL LIMIT 10000) TABLESAMPLE RESERVOIR (%d ROWS)", numPlayers)
       stmt := spanner.Statement{SQL: query}
       iter := txn.Query(ctx, stmt)

       playerRows, err := readRows(iter)
       if err != nil {
           return err
       }

       var playerUUIDs []string

       for _, row := range playerRows {
           var pUUID string
           if err := row.Columns(&pUUID); err != nil {
               return err
           }

           playerUUIDs = append(playerUUIDs, pUUID)
       }

       // Create the game
       gCols := []string{"gameUUID", "players", "created"}
       m = append(m, spanner.Insert("games", gCols, []interface{}{g.GameUUID, playerUUIDs, time.Now()}))

       // Update players to lock into this game
       for _, p := range playerUUIDs {
           pCols := []string{"playerUUID", "current_game"}
           m = append(m, spanner.Update("players", pCols, []interface{}{p, g.GameUUID}))
       }

       txn.BufferWrite(m)

       return nil
   })

   if err != nil {
       return err
   }

   return nil
}

গুগলএসকিউএল-এর টেবিলস্পেস রিজার্ভার (TABLESPACE RESERVOIR) সক্ষমতা ব্যবহার করে এসকিউএল (SQL)-এর মাধ্যমে খেলোয়াড়দের দৈবচয়ন করা হয়।

একটি খেলা শেষ করার প্রক্রিয়াটি কিছুটা বেশি জটিল। এর জন্য খেলোয়াড়দের মধ্য থেকে দৈবচয়নের মাধ্যমে একজন বিজয়ী নির্বাচন করতে হয়, খেলা শেষ হওয়ার সময় চিহ্নিত করতে হয় এবং প্রত্যেক খেলোয়াড়ের ' games_played''games_won' পরিসংখ্যান হালনাগাদ করতে হয়।

এই জটিলতা এবং পরিবর্তনের পরিমাণের কারণে, খেলাটি শেষ করার জন্য আবারও মিউটেশনকেই বেছে নেওয়া হয়।

func determineWinner(playerUUIDs []string) string {
   if len(playerUUIDs) == 0 {
       return ""
   }

   var winnerUUID string

   rand.Seed(time.Now().UnixNano())
   offset := rand.Intn(len(playerUUIDs))
   winnerUUID = playerUUIDs[offset]
   return winnerUUID
}

// Given a list of players and a winner's UUID, update players of a game
// Updating players involves closing out the game (current_game = NULL) and
// updating their game stats. Specifically, we are incrementing games_played.
// If the player is the determined winner, then their games_won stat is incremented.
func (g Game) updateGamePlayers(ctx context.Context, players []Player, txn *spanner.ReadWriteTransaction) error {
   for _, p := range players {
       // Modify stats
       var pStats PlayerStats
       json.Unmarshal([]byte(p.Stats.String()), &pStats)

       pStats.Games_played = pStats.Games_played + 1

       if p.PlayerUUID == g.Winner {
           pStats.Games_won = pStats.Games_won + 1
       }
       updatedStats, _ := json.Marshal(pStats)
       p.Stats.UnmarshalJSON(updatedStats)

       // Update player
       // If player's current game isn't the same as this game, that's an error
       if p.Current_game != g.GameUUID {
           errorMsg := fmt.Sprintf("Player '%s' doesn't belong to game '%s'.", p.PlayerUUID, g.GameUUID)
           return errors.New(errorMsg)
       }

       cols := []string{"playerUUID", "current_game", "stats"}
       newGame := spanner.NullString{
           StringVal: "",
           Valid:     false,
       }

       txn.BufferWrite([]*spanner.Mutation{
           spanner.Update("players", cols, []interface{}{p.PlayerUUID, newGame, p.Stats}),
       })
   }

   return nil
}

// Closing game. When provided a Game, choose a random winner and close out the game.
// A game is closed by setting the winner and finished time.
// Additionally all players' game stats are updated, and the current_game is set to null to allow
// them to be chosen for a new game.
func (g *Game) CloseGame(ctx context.Context, client spanner.Client) error {
   // Close game
   _, err := client.ReadWriteTransaction(ctx,
       func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
           // Get game players
           playerUUIDs, players, err := g.getGamePlayers(ctx, txn)

           if err != nil {
               return err
           }

           // Might be an issue if there are no players!
           if len(playerUUIDs) == 0 {
               errorMsg := fmt.Sprintf("No players found for game '%s'", g.GameUUID)
               return errors.New(errorMsg)
           }

           // Get random winner
           g.Winner = determineWinner(playerUUIDs)

           // Validate game finished time is null
           row, err := txn.ReadRow(ctx, "games", spanner.Key{g.GameUUID}, []string{"finished"})
           if err != nil {
               return err
           }

           if err := row.Column(0, &g.Finished); err != nil {
               return err
           }

           // If time is not null, then the game is already marked as finished. 
           // That's an error.
           if !g.Finished.IsNull() {
               errorMsg := fmt.Sprintf("Game '%s' is already finished.", g.GameUUID)
               return errors.New(errorMsg)
           }

           cols := []string{"gameUUID", "finished", "winner"}
           txn.BufferWrite([]*spanner.Mutation{
               spanner.Update("games", cols, []interface{}{g.GameUUID, time.Now(), g.Winner}),
           })

           // Update each player to increment stats.games_played 
           // (and stats.games_won if winner), and set current_game 
           // to null so they can be chosen for a new game
           playerErr := g.updateGamePlayers(ctx, players, txn)
           if playerErr != nil {
               return playerErr
           }

           return nil
       })

   if err != nil {
       return err
   }

   return nil
}

সার্ভিসটির জন্য ./src/golang/matchmaking-service/config/config.go ফাইলে বর্ণিত পদ্ধতি অনুযায়ী এনভায়রনমেন্ট ভেরিয়েবলের মাধ্যমেই কনফিগারেশন সম্পন্ন করা হয়।

   // Server defaults
   viper.SetDefault("server.host", "localhost")
   viper.SetDefault("server.port", 8081)

   // Bind environment variable override
   viper.BindEnv("server.host", "SERVICE_HOST")
   viper.BindEnv("server.port", "SERVICE_PORT")
   viper.BindEnv("spanner.project_id", "SPANNER_PROJECT_ID")
   viper.BindEnv("spanner.instance_id", "SPANNER_INSTANCE_ID")
   viper.BindEnv("spanner.database_id", "SPANNER_DATABASE_ID")

প্রোফাইল-সার্ভিসের সাথে সংঘাত এড়ানোর জন্য, এই সার্ভিসটি ডিফল্টরূপে লোকালহোস্ট:৮০৮১- এ চলে।

এই তথ্যের ভিত্তিতে, এখন ম্যাচমেকিং পরিষেবাটি চালু করার সময় হয়েছে।

ম্যাচ-মেকিং পরিষেবাটি চালান

go কমান্ড ব্যবহার করে সার্ভিসটি চালান। এর ফলে সার্ভিসটি ৮০৮২ পোর্টে চালু হবে। এই সার্ভিসটির অনেকগুলো ডিপেন্ডেন্সি profile-service-এর মতোই, তাই নতুন কোনো ডিপেন্ডেন্সি ডাউনলোড করা হবে না।

cd ~/spanner-gaming-sample/src/golang/matchmaking-service
go run . &

কমান্ড আউটপুট:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /games/create             --> main.createGame (4 handlers)
[GIN-debug] PUT    /games/close              --> main.closeGame (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8081

একটি গেম তৈরি করুন

একটি গেম তৈরি করতে পরিষেবাটি পরীক্ষা করুন। প্রথমে, ক্লাউড শেলে একটি নতুন টার্মিনাল খুলুন:

90eceac76a6bb90b.png

এরপর, নিম্নলিখিত কার্ল কমান্ডটি দিন:

curl http://localhost:8081/games/create \
    --include \
    --header "Content-Type: application/json" \
    --request "POST"

কমান্ড আউটপুট:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <date> 19:38:45 GMT
Content-Length: 38

"f45b0f7f-405b-4e67-a3b8-a624e990285d"

খেলাটি বন্ধ করুন

curl http://localhost:8081/games/close \
    --include \
    --header "Content-Type: application/json" \
    --data '{"gameUUID": "f45b0f7f-405b-4e67-a3b8-a624e990285d"}' \
    --request "PUT"

কমান্ড আউটপুট:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: <date> 19:43:58 GMT
Content-Length: 38

"506a1ab6-ee5b-4882-9bb1-ef9159a72989"

সারসংক্ষেপ

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

পরবর্তী পদক্ষেপ

এখন যেহেতু আপনার পরিষেবাগুলো চালু হয়ে গেছে, খেলোয়াড়দের সাইন আপ করিয়ে গেম খেলানোর সময় এসেছে!

৬. খেলা শুরু করুন

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

লোকাস্ট জেনারেটরগুলো চালানোর জন্য একটি ওয়েব-ইন্টারফেস প্রদান করে, কিন্তু এই ল্যাবে আপনি কমান্ড লাইন ( –headless অপশন) ব্যবহার করবেন।

খেলোয়াড়দের নিবন্ধন করুন

প্রথমে, আপনাকে খেলোয়াড় তৈরি করতে হবে।

./generators/authentication_server.py ফাইলে প্লেয়ার তৈরি করার পাইথন কোডটি দেখতে এইরকম:

class PlayerLoad(HttpUser):
   def on_start(self):
       global pUUIDs
       pUUIDs = []

   def generatePlayerName(self):
       return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))

   def generatePassword(self):
       return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))

   def generateEmail(self):
       return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32) + ['@'] +
           random.choices(['gmail', 'yahoo', 'microsoft']) + ['.com'])

   @task
   def createPlayer(self):
       headers = {"Content-Type": "application/json"}
       data = {"player_name": self.generatePlayerName(), "email": self.generateEmail(), "password": self.generatePassword()}

       with self.client.post("/players", data=json.dumps(data), headers=headers, catch_response=True) as response:
           try:
               pUUIDs.append(response.json())
           except json.JSONDecodeError:
               response.failure("Response could not be decoded as JSON")
           except KeyError:
               response.failure("Response did not contain expected key 'gameUUID'")

খেলোয়াড়দের নাম, ইমেল এবং পাসওয়ার্ড এলোমেলোভাবে তৈরি করা হয়।

সফলভাবে সাইন আপ করা খেলোয়াড়দের রিড লোড তৈরি করার জন্য একটি দ্বিতীয় টাস্কের মাধ্যমে সংগ্রহ করা হবে।

   @task(5)
   def getPlayer(self):
       # No player UUIDs are in memory, reschedule task to run again later.
       if len(pUUIDs) == 0:
           raise RescheduleTask()

       # Get first player in our list, removing it to avoid contention from concurrent requests
       pUUID = pUUIDs[0]
       del pUUIDs[0]

       headers = {"Content-Type": "application/json"}

       self.client.get(f"/players/{pUUID}", headers=headers, name="/players/[playerUUID]")

নিম্নলিখিত কমান্ডটি ./generators/authentication_server.py ফাইলটিকে কল করে, যা একই সময়ে দুটি থ্রেডের (u=2) কনকারেন্সিতে ৩০ সেকেন্ডের ( t=30s) জন্য নতুন প্লেয়ার তৈরি করবে:

cd ~/spanner-gaming-sample
locust -H http://127.0.0.1:8080 -f ./generators/authentication_server.py --headless -u=2 -r=2 -t=30s

খেলোয়াড়রা গেমে যোগ দেয়

এখন যেহেতু খেলোয়াড়রা সাইন আপ করেছে, তারা খেলা শুরু করতে চায়!

./generators/match_server.py ফাইলে গেম তৈরি এবং বন্ধ করার পাইথন কোডটি দেখতে এইরকম:

from locust import HttpUser, task
from locust.exception import RescheduleTask

import json

class GameMatch(HttpUser):
   def on_start(self):
       global openGames
       openGames = []

   @task(2)
   def createGame(self):
       headers = {"Content-Type": "application/json"}

       # Create the game, then store the response in memory of list of open games.
       with self.client.post("/games/create", headers=headers, catch_response=True) as response:
           try:
               openGames.append({"gameUUID": response.json()})
           except json.JSONDecodeError:
               response.failure("Response could not be decoded as JSON")
           except KeyError:
               response.failure("Response did not contain expected key 'gameUUID'")


   @task
   def closeGame(self):
       # No open games are in memory, reschedule task to run again later.
       if len(openGames) == 0:
           raise RescheduleTask()

       headers = {"Content-Type": "application/json"}

       # Close the first open game in our list, removing it to avoid 
       # contention from concurrent requests
       game = openGames[0]
       del openGames[0]

       data = {"gameUUID": game["gameUUID"]}
       self.client.put("/games/close", data=json.dumps(data), headers=headers)

যখন এই জেনারেটরটি চালানো হবে, তখন এটি ২:১ অনুপাতে (খোলা:বন্ধ) গেম খুলবে এবং বন্ধ করবে। এই কমান্ডটি জেনারেটরটি ১০ সেকেন্ডের জন্য চালাবে (-t=10s) :

locust -H http://127.0.0.1:8081 -f ./generators/match_server.py --headless -u=1 -r=1 -t=10s

সারসংক্ষেপ

এই ধাপে, আপনি খেলোয়াড়দের গেম খেলার জন্য সাইন আপ করার সিমুলেশন করেছেন এবং তারপর ম্যাচমেকিং পরিষেবা ব্যবহার করে খেলোয়াড়দের গেম খেলার সিমুলেশন চালিয়েছেন। এই সিমুলেশনগুলো আমাদের পরিষেবাগুলোর REST API-তে অনুরোধ পাঠানোর জন্য Locust পাইথন ফ্রেমওয়ার্ক ব্যবহার করেছে।

প্লেয়ার তৈরি করতে ও গেম খেলতে ব্যয়িত সময়, সেইসাথে একযোগে ব্যবহারকারীর সংখ্যা ( -u) ইচ্ছামতো পরিবর্তন করুন।

পরবর্তী পদক্ষেপ

সিমুলেশনের পরে, আপনি স্প্যানার কোয়েরি করে বিভিন্ন পরিসংখ্যান যাচাই করতে চাইবেন।

৭. খেলার পরিসংখ্যান পুনরুদ্ধার করুন

এখন যেহেতু সিমুলেটেড প্লেয়াররা সাইন আপ করে গেম খেলতে পারছে, আপনার পরিসংখ্যান দেখে নেওয়া উচিত।

এটি করার জন্য, ক্লাউড কনসোল ব্যবহার করে স্প্যানারে কোয়েরি অনুরোধ পাঠান।

b5e3154c6f7cb0cf.png

খোলা বনাম বন্ধ খেলা পরীক্ষা করা

একটি বন্ধ গেম হলো সেটি, যার সমাপ্তির টাইমস্ট্যাম্পটি পূরণ করা থাকে, অন্যদিকে একটি খোলা গেমের সমাপ্তির মান NULL থাকে। গেমটি বন্ধ করার সময় এই মানটি সেট করা হয়।

সুতরাং এই কোয়েরিটি আপনাকে দেখতে সাহায্য করবে যে কতগুলি গেম খোলা এবং কতগুলি বন্ধ আছে:

SELECT Type, NumGames FROM
(SELECT "Open Games" as Type, count(*) as NumGames FROM games WHERE finished IS NULL
UNION ALL
SELECT "Closed Games" as Type, count(*) as NumGames FROM games WHERE finished IS NOT NULL
)

ফলাফল:

Type

NumGames

Open Games

0

Closed Games

175

কতজন খেলোয়াড় খেলছে এবং কতজন খেলছে না তা যাচাই করা হচ্ছে

কোনো খেলোয়াড়ের 'current_game' কলামে যদি তথ্য দেওয়া থাকে, তাহলে তিনি একটি গেম খেলছেন। অন্যথায়, তিনি বর্তমানে কোনো গেম খেলছেন না।

সুতরাং, বর্তমানে কতজন খেলোয়াড় খেলছে এবং কতজন খেলছে না তা তুলনা করতে, এই কোয়েরিটি ব্যবহার করুন:

SELECT Type, NumPlayers FROM
(SELECT "Playing" as Type, count(*) as NumPlayers FROM players WHERE current_game IS NOT NULL
UNION ALL
SELECT "Not Playing" as Type, count(*) as NumPlayers FROM players WHERE current_game IS NULL
)

ফলাফল:

Type

NumPlayers

Playing

0

Not Playing

310

শীর্ষ বিজয়ীদের নির্ধারণ করুন

যখন কোনো খেলা বন্ধ করা হয়, তখন খেলোয়াড়দের মধ্য থেকে দৈবচয়নের মাধ্যমে একজনকে বিজয়ী হিসেবে নির্বাচন করা হয়। খেলাটি বন্ধ করার সময় সেই খেলোয়াড়ের ‘ games_won’ পরিসংখ্যানটি এক বৃদ্ধি পায়।

SELECT playerUUID, stats
FROM players
WHERE CAST(JSON_VALUE(stats, "$.games_won") AS INT64)>0
LIMIT 10;

ফলাফল:

প্লেয়ারইউইউআইডি

পরিসংখ্যান

07e247c5-f88e-4bca-a7bc-12d2485f2f2b

{"খেলা_সংখ্যা":৪৯,"জয়ী_সংখ্যা":১}

09b72595-40af-4406-a000-2fb56c58fe92

{"খেলা_সংখ্যা":৫৬,"জয়ী_সংখ্যা":১}

1002385b-02a0-462b-a8e7-05c9b27223aa

{"খেলা_সংখ্যা":৬৬,"জয়ী_সংখ্যা":১}

13ec3770-7ae3-495f-9b53-6322d8e8d6c3

{"খেলা_সংখ্যা":৪৪,"জয়ী_সংখ্যা":১}

15513852-3f2a-494f-b437-fe7125d15f1b

{"খেলা_সংখ্যা":৪৯,"জয়ী_সংখ্যা":১}

17faec64-4f77-475c-8df8-6ab026cf6698

{"খেলা_সংখ্যা":৫০,"জয়ী_সংখ্যা":১}

1abfcb27-037d-446d-bb7a-b5cd17b5733d

{"খেলা_সংখ্যা":৬৩,"জয়ী_সংখ্যা":১}

2109a33e-88bd-4e74-a35c-a7914d9e3bde

{"খেলা_সংখ্যা":৫৬,"জয়ী_সংখ্যা":২}

222e37d9-06b0-4674-865d-a0e5fb80121e

{"খেলা_সংখ্যা":৬০,"জয়ী_খেলা":১}

22ced15c-0da6-4fd9-8cb2-1ffd233b3c56

{"খেলা_সংখ্যা":৫০,"জয়ী_সংখ্যা":১}

সারসংক্ষেপ

এই ধাপে, আপনি ক্লাউড কনসোল ব্যবহার করে স্প্যানারকে কোয়েরি করার মাধ্যমে খেলোয়াড় এবং গেমের বিভিন্ন পরিসংখ্যান পর্যালোচনা করেছেন।

পরবর্তী পদক্ষেপ

এরপর, পরিষ্কার করার পালা!

৮. পরিষ্কার করা (ঐচ্ছিক)

পরিষ্কার করার জন্য, ক্লাউড কনসোলের ক্লাউড স্প্যানার বিভাগে যান এবং "সেটআপ এ ক্লাউড স্প্যানার ইনস্ট্যান্স" নামক কোডল্যাব ধাপে আমাদের তৈরি করা 'cloudspanner-gaming' ইনস্ট্যান্সটি মুছে ফেলুন।

৯. অভিনন্দন!

অভিনন্দন, আপনি স্প্যানারে সফলভাবে একটি নমুনা গেম স্থাপন করেছেন।

এরপর কী?

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

  • স্কিমা ডিজাইন
  • ডিএমএল বনাম মিউটেশন
  • গোলাং নিয়ে কাজ করা

আপনার গেমের ব্যাকএন্ড হিসেবে স্প্যানার ব্যবহারের আরেকটি উদাহরণের জন্য ক্লাউড স্প্যানার গেম ট্রেডিং পোস্ট কোডল্যাবটি অবশ্যই দেখে নেবেন!