Real-time Multi-player Terminal Game (Part - 1)

ဒီ Tutorial မှာတော့ ကျနော်တို့ Terminal မှာ ကစားလို့ရတယ့် Game တစ်ခုတည်ဆောက်သွားပါမယ်။ အရင် Post မှာ Prologue ပြခဲ့တာကို တချက်ကြည့်ကြည့်ရင် ဘယ်လိုအလုပ်လုပ်လဲဆိုတာကို သိလောက်မှာပါ။ အဓိကကတော့ Socket.io နဲ့ Real-time Capability ထည့်တာကို ပြချင်တာပါ။ များသောအားဖြင့် Socket.io Tutorial တွေမှာ Chat App ကို Example အနေနဲ့ ပြတတ်ကြတော့ ကျနော်က နည်းနည်းကွဲတဲ့ Game တစ်ခုနဲ့ ပြသွားချင်တာပါ။ ကဲစလိုက်ကြရအောင်။

Setup

အရင်ဆုံး လိုအပ်တဲ့ Library တွေနဲ့ Project Setup လုပ်ပါမယ်။

npm init -y

npm i socket.io socket.io-client blessed chalk figlet clear

Folder structure အနေနဲ့ကတော့

├── client
│   ├── client.js
│   └── utils
│       └── helpers.js
└── server
    ├── app.js
    ├── controllers
    │   ├── room.js
    │   └── user.js
    ├── lib
    │   ├── game.js
    │   └── player.js
    ├── server.js
    └── utils
        ├── constants.js
        └── helpers.js

ကျနော်တို့ Server နဲ့ Game Client ဆိုပြီးတော့ နှစ်ခုရှိပါမယ်။ Server ဘက်မှာ Socket.io Server ကို သုံးပါမယ်။ Game Room နဲ့ Player တွေကို ထိန်းဖို့အတွက် Controller နှစ်ခုရှိပါမယ်။ လောလောဆယ် ဒီ Game မှာ Data layer အတွက် (ဥပမာ - Redis လို့ Database တစ်ခုသုံးတာမျိုး) ထည့်စဥ်းစားသွားမှာ မဟုတ်ပါဘူး။ Util Foler တွေထဲမှာတော့ Helper function တွေ ၊ Constants တွေရှိပါမယ်။

Flow

ဒါကတော့ ဒီ Game ရဲ့ Flow Chart ပါ။ အရင်ဆုံး Client ကို သုံးပြီး User က ဝင်လာတဲ့အခါမှာ Username တစ်ခုတောင်းပါမယ်။ ထပ်နေခဲ့ရင် ပြန်တောင်းတာမျိုး ရှိပါမယ်။ ဒီဝင်လာတဲ့ User ကို Player အနေနဲ့ Queue တစ်ခုထဲမှာ နောက်ထပ် Player မရှိမချင်း စောင့်ခိုင်းထားရပါမယ်။ Game က Player ၂ ယောက်ရှိမှ ဆော့လို့ရမှာကိုး။ Queue ထဲမှာ Player ၂ ယောက်ရှိပြီဆိုတာနဲ့ Server က Room တစ်ခုထောင်ပြီး Match လုပ်ပေးမယ်ပေါ့။ ပြီးရင် Player တွေရဲ့ Move တွေကို validation စစ်တာရှိမယ် (ဥပမာ - အလှည့်မဟုတ်သေးတာမျိုးပေါ့။) နောက်ပြီး တစ်ဖက် Player က ထွက်သွားရင် ဒီဖက် Player ကို Inform လုပ်တာမျိုးရှိမယ်။ စသဖြင့်ပေါ့။

realtime-multiplayer-terminal-game-flowchart.png

Getting Started

အရင်ဆုံး ကျနော်တို့ Server ဘက်မှာ ဘုံသုံးမယ့် Constants တွေ ၊ Helper function တွေ ကြေညာပါမယ်။ combos ဆိုတာကတော့ နိုင်မယ့် Sequence တွေပေါ့။ Tic-Tac-Toe မှာက သုံးခုတန်းရင်နိုင်တာဆိုတော့ အဲဒီ့ အတန်း sequence တွေဖြစ်ပါတယ်။

server/utils/constants.js

"use strict";

// prefix
const roomPrefix = "game_room_";

// Winning sequences
const combos = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8],
  [0, 3, 6],
  [1, 4, 7],
  [2, 5, 8],
  [0, 4, 8],
  [6, 4, 2],
];

// display messages
const messages = {
  msg_tie: "Tied!",
  msg_win: "U Won!",
  msg_lose: "U Lost!",
  msg_resign: "The other player has resigned",
  msg_replay: "Play one more?",
  msg_game_0: "Game has not started yet!",
  msg_game_1: "Game started",
  msg_invalid: "Invalid move",
  msg_not_yet: "It's not your move yet.",
  msg_waiting: "Waiting for another player",
  msg_player_x: "You are 'Player X.",
  msg_player_o: "You are 'Player O.",
  msg_uname_exists: "Username already exists!",
};

module.exports = { combos, roomPrefix, messages };

ဒါကတော့ Logging နဲ့ တခြား helper function တွေပါ။ Log ကို colorized ဖြစ်စေချင်တဲ့အတွက် ဒီမှာ Chalk ဆိုတဲ့ library ကို သုံးပါတယ်။

server/utils/helpers.js

"use strict";

const chalk = require("chalk");

// normalize user inputs
const normalize = (str = "") => str.replace(/[\s\n]/g, "");

// logging
const log = {
  error: (msg) => console.log(chalk.red(msg)),
  info: (msg) => console.log(chalk.blue(msg)),
  warn: (msg) => console.log(chalk.yellow(msg)),
  success: (msg) => console.log(chalk.green(msg)),
};

// generate a random number < 1000
const genKey = () => Math.round(Math.random() * 1000).toString();

module.exports = {
  normalize,
  log,
  genKey,
};

Real-time Support

အခုကစပြီး ကျနော်တို့ Real-time အတွက် Socket.io သုံးပြီး implement လုပ်ပါမယ်။ အရင်ဆုံး Socket.io နဲ့ ရင်းနှီးသွားအောင် Crash Course မြန်မြန်ကြည့်လိုက်ရအောင်။

Socket.io Crash Course

  1. Creating the Socket Server

Socket Server တည်ဆောက်မယ်ဆိုရင် Server API ကိုသုံးရပါတယ်။ အောက်ကလိုပေါ့။

const io = require('socket.io')();
// or
const { Server } = require('socket.io');
const io = new Server();

ကျနော်တို့က သူ့ကို HTTP Server တစ်ခုနဲ့ bind ပေးပါမယ်။ ဒီအတွက်ကို Server() က Parameter တစ်ခုအနေနဲ့ လက်ခံပါတယ်။

2. Listening Events

Socket မှာ communication လုပ်ပုံက Event-based ဖြစ်ပါတယ်။ Socket Server ကနေ Socket Connection ဖြစ်တာတွေ ၊ Disconnect ဖြစ်တာတွေကို Listen လုပ်လို့ရပါတယ်။ Socket တစ်ခုချင်းကတော့ ကျနော်တို့ပို့တဲ့ Event တွေကို သီးခြား handle လုပ်လို့ရပါတယ်။

io.on('connection', (socket) => {
    // Connection ချိတ်သွားတိုင်းမှာ ဒီ Callback ကို ဝင်မှာပါ။
    // ဝင်လာတဲ့ Socket ကတော့ Client နဲ့ Server ကြားက connection ပေါ့။


    socket.on('hello', (message) => {
        // Client ဘက်က hello ဆိုတဲ့ event နာမည်နဲ့
        // message ဆိုတဲ့ Data Payload တစ်ခုပို့တဲ့အခါတိုင်းမှာ
        // ဒီ Callback ကို ဝင်ပါတယ်။
    });
})

3. Emitting Events

Server API ကို သုံးတယ်ဆိုရင်တော့ Event က

  • Server ကနေလည်း ပို့လို့ရသလို
  • လက်ရှိ Connection ရှိနေတဲ့ Socket ကို သုံးပြီးတော့လည်း

Event တွေ ပို့လို့ရပါတယ်။ ပို့တဲ့ ပုံစံကတော့ မျိုးစုံရှိပါတယ်။

// လက်ရှိ Server မှာ Connection ရှိသမျှ Client တွေကို ပို့တာပါ။
io.emit(DATA)

socket.emit(EVENT_NAME, DATA)

4**. Namespaces and Rooms**

Socket.io မှာ Namespace နဲ့ Room ဆိုတာ ရှိပါတယ်။

Namespace ဆိုတာကတော့ Shard Server Connection ပေါ်ကနေ သီးသန့် Server Logic အနေနဲ့ ခွဲထုတ်ထားချင်တဲ့အခါမှာ သုံးတာဖြစ်ပါတယ်။ ဥပမာပြောရရင် ကျနော်တို့ရဲ့ App မှာ Admin နဲ့ ရိုးရိုး User ဆိုပြီး ရှိတယ်ဆိုရင် Admin Namespace နဲ့ User namespace ဆိုပြီး Seprate လုပ်ထားပြီး Server Logic ကို ခွဲထုတ်လိုက်လို့ရပါတယ်။ Default အားဖြင့်တော့ Connection တစ်ခုချိတ်တာနဲ့ / ဆိုတဲ့ namespace ထဲကို သွား Attach လုပ်ပါတယ်။

နောက်တစ်ခုကတော့ Room ပေါ့။ Room က Namespace ရဲ့ Subdivision ပါပဲ။ ကျနော်တို့ လက်ရှိ Game မှာတော့ Room ကို သုံးပြီး Game Room တွေကို သီးခြားဖြစ်အောင် လုပ်သွားမှာပါ။

// Socket တစ်ခုက Room တစ်ခုက Join တဲ့အခါ
socket.join(ROOM)

// ထွက်တဲ့အခါ
socket.leave(ROOM)

ဒီလောက်ဆိုရင်တော့ Socket.io နဲ့ တော်တော်ရင်းနှီးသွားလောက်ပါပြီ။

Server

ကျနော်တို့ရဲ့ Game Server ကို စ implement လုပ်ရအောင်။ Server ဘက်မှာ Handle လုပ်မယ့် Event ၅ ခုရှိပါတယ်။

  • socket connection
  • enter (User Server ထဲ ဝင်လာတဲ့အခါ)
  • move (အကွက်ရွေ့တဲ့အခါ)
  • replayConfirm (နောက်ထပ် ထပ် Play တဲ့အခါ)
  • disconnect (Game ကနေ ထွက်တဲ့အခါ)

server/server.js

"use strict";
const { createServer } = require("http");
const { Server } = require("socket.io");
const clear = require("clear");
const { log } = require("../utils/helpers");

// server port
const PORT = process.argv[2] || 3000;

const httpServer = createServer();
const io = new Server(httpServer);

clear();

io.on("connection", (socket) => {
  log.info(`New user connected to the server: ${socket.id}`);

    socket.on("enter", (uname) => {
    log.info(`${socket.id} has entered.`);
  });
    
    socket.on("move", (move) => {
    log.info(`${socket.id} has made move.`);
  });
    
    socket.on("replayConfirm", (confirmed) => {
    log.info(`${socket.id} has confirmed replay.`);
  });
    
    socket.on("disconnect", () => {
    log.info(`${socket.id} is disconnected.`);
  });
});

httpServer.listen(PORT, () => {
  log.success(`Game server listening on PORT:${PORT}`);
  log.warn("----------------------------------------");
});

Server Port ကို Argument အနေနဲ့ လက်ခံထားတာကြောင့် PORT ကို ကိုယ်ကြိုက်သလိုထားနိုင်ပါတယ်။ ပြီးတော့ Terminal ကို ရှင်းပစ်ဖို့အတွက် ဒီမှာ clear ဆိုတဲ့ Library ကို သုံးထားတာကို တွေ့မှာပါ။

ဒါဆို အခု Run ကြည့်ရင်

node server/server.js

ဒီ Output ကို မြင်ရမှာပါ။

Game server listening on PORT:3000
----------------------------------------

Client

အခု Server ဘက်က Socket Connection လက်ခံနိုင်ပြီဆိုတော့ ကျနော်တို့ ချိိတ်ဖို့အတွက် Client ဆောက်ရအောင်။ ဒီမှာလည်း ကျနော်တို့ Server URL ကို argument အနေနဲ့ လက်ခံနိုင်ရပါလိမ့်မယ်။ Client ဘက်မှာလည်း Server ဘက်က Emit လုပ်မယ့် Event တွေရှိတာဆိုတော့ သူတို့ကို Handle လုပ်ရပါလိမ့်မယ်။ ဒီမှာတော့ Socket.io Client API ကို သုံးသွားမှာပါ။

client/client.js

"use strict";
const clear = require("clear");
const socket = require("socket.io-client")(
  process.argv[2] || "http://localhost:3000"
);

socket.on("connect", () => {
  clear();
  console.log("Connected");
});

socket.on("uname-exists", (msg) => {});

socket.on("progress", (msg) => {});

socket.on("info", (msg) => {});

socket.on("over", (msg) => {});

socket.on("replay", (msg) => {});

socket.on("scoreboard", (msg) => {});

socket.on("disconnect", () => {
  // disconnected
  process.exit();
});

အခု Client ကို Terminal Window နောက်တစ်ခုမှာ Run လိုက်ရင်

node client/client.js

Connected

Server run ထားတဲ့ Terminal ဘက်မှာတော့ အောက်ကလို မြင်ရပါလိမ့်မယ်။

node server/server.js

Game server listening on PORT:3000
----------------------------------------

New user connected to the server: s4ZD9R7H9YuZVQShAAAB

ဒါဆိုရင်တော့ ကျနော်တို့ Socket နဲ့ ပတ်သတ်လို့ setup လုပ်စရာရှိတာအကုန်ပြီးသွားပါပြီ။ ကျန်တာကတော့ တကယ့် Game Logic, UI နဲ့ Event အပြန်အလှန်ပို့တာတွေကို Handle လုပ်တာပဲ ကျန်ပါတော့မယ်။