Hello 🤗
Today, I will explain how to create a guessing number game using Javascript, NodeJS, and Hyperswam. I will also talk about this package, how it works, and what it is for.
I will leave the table content here to help you understand how this post's topics are divided more easily.
Table Of Contents
HyperSwarm
What is Hyperswarm? What is it for?
Well, first of all, I wanted to talk about this package that Tether created. Hyperswam is a package that helps you create peers to find and connect between them, announcing a common 'topic.'
What is a peer? It is the way we call the participants in a P2P network.
If you want to know more about this, you can check my post, where I go through the topic :D
These are some important links.
Github => https://github.com/holepunchto/hyperswarm
Documentation => https://docs.pears.com/building-blocks/hyperswarm
The structure of the game
Hyperswam is an excellent tool for creating P2P networks, BUT this game will use peers in a server-client architecture. It is easy to understand how the peers interact in this type of architecture if client-server is the only architecture you know.
The flow is very simple:
- The server will randomly choose a number between 1 and 100.
- The first user that connects to the game will start the game.
- The following users can join and play in the same party.
- Every time a player guesses a number, the server makes a check and updates the game's status, then sends the updated status to all the connected players.
- When someone guesses the number, the server notifies the players, and the game starts again.
Code
Let's go
Prepare environment
Before starting, you should have Node installed on your computer.
You can download it from here.
After this, open the terminal in the folder where you want to start your project. Run
npm init
and complete all the steps to initiate your node project.
Then, you have to install two packages to start to work.
npm install hyperswarm readline
Readline will provide an interface for reading data from the console. This means we can guess the number using the console without creating a front end.
Index.js - Main function
Once we have installed the packages, we will start to work on our index.js
const { CLI } = require("./cli");
const { Server } = require("./server");
const { Client } = require("./client");
async function main() {
const cli = new CLI();
const nickname = await cli.askTerminal("What is your nickname? ");
const lowerCaseNickname = nickname.toLowerCase();
if (lowerCaseNickname === "server") {
//const server = new Server();
} else {
//const client = new Client(nickname);
}
console.log(`Welcome to the game, ${nickname}!`);
}
main();
Also, we need to create a module for reading that gets exported as CLI. This code creates the interface in the console.
cli.js
const rl = require("readline");
let readline;
class CLI {
constructor() {}
async askTerminal(question) {
if (!readline) {
readline = rl.createInterface({
input: process.stdin,
output: process.stdout,
});
}
return new Promise((resolve) => {
readline.question(question, (input) => resolve(input));
});
}
}
module.exports = { CLI };
Then, run your project with npm run start
The console will ask you for your nickname; if you type "server," you will start the peer that will work as the server in charge of managing the game. Otherwise, you will start the peer that acts as a client (or player) to join the game.
We haven't added the code to create the client and the server yet, so you will just see a console log with your nickname.
Server
The server is a class. I will create a new instance of the server module when the user types "server" in the console after running npm run start.
This is a basic UML of the class.
+---------------------+
| Server |
+---------------------+
| -server: Hyperswarm |
| -players: Map |
| -clients: Array |
| -game: Game |
+---------------------+
| +constructor() |
| +handleConnection( |
| Socket, PeerInfo) |
| +isValidGuess(int) |
| +respontToClients( |
| String) |
| +initializeGame() |
+---------------------+
Now, there are some lines that I want to comment on. Server is an instance of Hyperswarm.
So, as I mentioned before, the peers need a topic to connect with each other. To join the topic, we need to use
swarm.join(topic, [options])
topic is a 32-byte Buffer, and options is an object that contains both options server and client and a boolean to indicate how the peer should act.
In this case,
{
server: true,
client: false,
}
For the topic, I will create a doc called constant.js
const GAME_CHANNEL = "hyperswarm-guessing-game";
module.exports = {
GAME_CHANNEL,
};
Once the peer connects to the network, we must indicate how to react to some events. In this case, we will use
swarm.on('connection', (socket, peerInfo) => {})
Emitted whenever the swarm connects to a new peer. This means the server will do something every time a new peer (a player) connects with them.
Inside that function, I will handle the connections to store the data from the players. I will use that information to determine which connections the servers had and where I have to send messages with the game's status.
Also, we will have a socket.on()
, which is the server receiving and showing the messages from players.
const Hyperswarm = require("hyperswarm");
const { GAME_CHANNEL } = require("./constants");
const { Game } = require("./game");
class Server {
constructor() {
this.server = new Hyperswarm();
this.players = new Map();
this.clients = [];
this.game = new Game();
const topic = Buffer.alloc(32).fill(GAME_CHANNEL);
this.server.join(topic, {
server: true,
client: false,
});
this.handleConnection = this.handleConnection.bind(this);
this.server.on("connection", this.handleConnection);
}
handleConnection(socket, peerInfo) {
console.log("New connection ");
const publicKey = peerInfo.publicKey.toString("hex");
if (this.players.size) {
if (!this.players.has(publicKey)) {
console.log("New player ");
this.players.set(publicKey, true);
this.clients.push(socket);
}
this.respontToClients(
this.game.lastClue ?? "Guess a number between 1 and 100:"
);
} else {
console.log("First player");
this.players.set(publicKey, true);
this.clients.push(socket);
this.initializeGame();
}
socket.on("data", (data) => {
const jsonData = JSON.parse(data.toString());
console.log(`Server: ${jsonData.nickname} guessed ${jsonData.guess}`);
const guessedNumber = parseInt(jsonData.guess);
if (this.isValidGuess(guessedNumber)) {
if (this.game.isStarted) {
if (guessedNumber === this.game.numberToGuess) {
const message = `User ${jsonData.nickname} guessed ${jsonData.guess} and it's correct!\n The game is over! \n A new game will start soon.`;
this.respontToClients(message);
this.game.isEnded = true;
this.initializeGame();
} else {
if (guessedNumber > this.game.numberToGuess) {
this.game.lastClue = `User ${jsonData.nickname} guessed ${jsonData.guess} and it's too high!`;
} else if (guessedNumber < this.game.numberToGuess) {
this.game.lastClue = `User ${jsonData.nickname} guessed ${jsonData.guess} and it's too low!`;
}
this.respontToClients(this.game.lastClue);
}
}
} else {
const message = `User ${jsonData.nickname} guessed ${jsonData.guess} and it's not a valid guess. Please guess a number between 1 and 100.`;
this.respontToClients(message);
}
});
}
isValidGuess(guess) {
if (guess < 1 || guess > 100) {
return false;
}
return true;
}
respontToClients(message) {
for (const client of this.clients) {
client.write(
JSON.stringify({
type: "game-update",
message,
})
);
}
}
initializeGame() {
this.game.startGame();
this.respontToClients("Game started! Guess a number between 1 and 100:");
}
}
module.exports = { Server };
Client
+---------------------+
| Client |
+---------------------+
| -nickname: String |
| -client: Hyperswarm |
| -cli: CLI |
| -topic: Buffer |
| -connection: Socket |
| -handleConnection: |
| Function |
+---------------------+
| +constructor(String)|
| +handleConnection( |
| Socket, PeerInfo) |
| +askGuess() |
+---------------------+
In the client, I will use swarm.join(topic, [options])
where
{
server: false,
client: true,
}
And swarm.on('connection', (socket, peerInfo) => {})
will inform us when we connect with the server.
const Hyperswarm = require("hyperswarm");
const { GAME_CHANNEL } = require("./constants");
const { CLI } = require("./cli");
class Client {
constructor(nickname) {
this.nickname = nickname;
this.client = new Hyperswarm();
this.cli = new CLI();
this.topic = Buffer.alloc(32).fill(GAME_CHANNEL);
this.client.join(this.topic, {
server: false,
client: true,
});
this.handleConnection = this.handleConnection.bind(this);
this.client.on("connection", this.handleConnection);
}
handleConnection(socket, peerInfo) {
console.log("Client connected to server!");
this.connection = socket;
socket.on("data", (data) => {
const jsonData = JSON.parse(data.toString());
if (jsonData.type === "game-update") {
console.log(jsonData.message);
this.askGuess();
}
});
}
askGuess() {
this.cli.askTerminal("> ").then((number) => {
this.connection.write(
JSON.stringify({
nickname: this.nickname,
guess: number,
})
);
});
}
}
module.exports = { Client };
Game
+---------------------+
| Game |
+---------------------+
| -isStarted: Boolean |
| -numberToGuess: int |
| -isEnded: Boolean |
| -lastGuess: int |
| -lastClue: String |
+---------------------+
| +constructor() |
| +getNumberToGuess() |
| +startGame() |
+---------------------+
In the server, we make an instance of the game. This is the code to initialize the game and the logic to choose the number.
class Game {
constructor() {
this.isStarted = false;
this.numberToGuess = 0;
this.isEnded = false;
this.lastGuess = null;
this.lastClue = null;
}
getNumberToGuess() {
return Math.floor(Math.random() * 100) + 1;
}
startGame() {
this.numberToGuess = this.getNumberToGuess();
this.isStarted = true;
console.log("Number to guess: ", this.numberToGuess);
}
}
module.exports = { Game };
Demo
Before running the code, you must remember to uncomment the lines in index.js
.
This is the link to the GitHub repo.
Now, ✨we have a game working.✨
2 - The first player joins the game
3 - The server starts the game
4 - The player guesses a number
5 - Another player joins the game
6 - The server is notified about the new player
7 - A resume of the game and the end.
Bye Bye
I hope you enjoyed creating this game and have understood more about how hyperSwarm works.
I want to mention that I took the main idea from here.
Thank you so much for reading. 💖
Please let me know your thoughts and if you would add something else to the game.
See you in the next posts. 🚀