Liemiers propose aux joueurs d'être immergés dans une histoire dont la progression nécessite la résolution d'énigmes.
Puisque ces dernières peuvent requérir plusieurs heures pour être résolues, il était impératif que l'accès au jeu soit simple.
Aussi, son interface devait pouvoir être aisément manipulée lors de recherches sur internet. Ces différents points ont vite mis en évidence que le jeu se devait d'être présenté sous la forme d'un site internet.
Chaque phase de l'histoire comporte un texte explicatif de la situation ainsi qu'une ressouce (un texte, un son ou une image) sur laquelle les joueurs doivent se baser pour résoudre l'énigme.
Le client front-end est codé sous ReactJs et utilise Material-ui. Sa build est servie par Apache.
Le serveur back-end est codé sous NodeJs. Il est lancé dans un tmux.
Tous deux utilisent Socket.io pour la transmission de messages.
Code du serveur lors de la réception d'une nouvelle connexion.
io.on("connection", (socket) => {
// Server is not reachable
if (!bServerIsReachable)
socket.disconnect();
// Server is reachable
else {
// Store the connecting client
stackSockets[socket.id] = [];
stackSockets[socket.id].chrono = Date.now();
stackSockets[socket.id].registered = false;
stackSockets[socket.id].country = 'XX';
stackSockets[socket.id].name = 'Anonymous';
// Send the welcome message to the client (this will show the registration form)
SendToClient(socket, 'LAUNCH', welcomeMess);
// Register callback
socket.on("REGISTER", (usrnm) => {
if (!stackSockets[socket.id].registered) {
if (!IsOffensive(usrnm)) { // Checks if the username includes any forbidden word
stackSockets[socket.id].registered = true;
stackSockets[socket.id].name = usrnm;
// Push the current socket to the registered room
socket.join('registered');
// Define user's country
GeoIp.open('./GeoLite2-Country.mmdb').then(reader => {
// Get user's country
var strCountry = reader.country(socket.handshake.address).country.isoCode;
// Store the user's country
stackSockets[socket.id]['country'] = (strCountry || 'XX');
// Add 1 to the number of online users coming from this country
listCountry[strCountry] = (listCountry[strCountry] || 0) + 1;
// Send the users' country list to users currently playing
BroadcastToRegisteredClients('ONLINE', JSON.stringify(listCountry));
}).catch((error) => {
// Can't get user's country, set the uknown country flag
stackSockets[socket.id].country = 'XX';
// Add 1 to the number of online users from unknown locations
listCountry['XX'] = (listCountry['XX'] || 0) + 1;
// Send the users' country list to users currently playing
BroadcastToRegisteredClients('ONLINE', JSON.stringify(listCountry));
});
// Store time point (used for the 10 sec cooldown between each submission, also used at start to mitigate cheat)
stackSockets[socket.id].chrono = Date.now();
// Send the trigger to show the game at the client, and current time point for comparison
SendToClient(socket, 'REGISTER', JSON.stringify({ nsr: true, clk: Date.now() }));
// Send progression informations (time elapsed, names of people who solved)
SendToClient(socket, 'PROGRESS', JSON.stringify({0: PuzzleCurrTime, 1: iPuzzleIdCur, 2: arrProgress}));
// Depending on the progression: send the current puzzle or the ending message
if (iPuzzleIdCur < iPuzzleIdMax) {
SendToClient(socket, 'HELPER_RES', JSON.stringify(Helpers[iPuzzleIdCur]));
SendToClient(socket, 'HELPER_PCH', JSON.stringify({0: true, 1: iPuzzleIdMax, 2: Pitchs.slice(0, iPuzzleIdCur + 1)}));
}
else {
SendToClient(socket, 'HELPER_RES', JSON.stringify({ type: 'txt', nchr: 0, rsrc: endingMess }));
SendToClient(socket, 'HELPER_PCH', JSON.stringify({0: true, 1: iPuzzleIdMax, 2: Pitchs.slice(0, iPuzzleIdCur )}));
}
}
// Send trigger to show an error message in the client form
else
SendToClient(socket, 'REGISTER', JSON.stringify({ nsr: false, clk: 0 }));
}
else
socket.disconnect();
});
// Disconnection callback
socket.on("disconnect", () => {
// Remove the user country of the online users list
--listCountry[stackSockets[socket.id].country];
if (listCountry[stackSockets[socket.id].country] <= 0)
delete listCountry[stackSockets[socket.id].country];
// Remove the socket of the online user stack
delete stackSockets[socket.id];
// Send the users' country list to users currently playing
BroadcastToRegisteredClients('ONLINE', JSON.stringify(listCountry));
});
...
}
});
Code du Client pour la réception et l'affichage du fichier de l'énigme (texte, son ou image).
class Client extends Component {
...
componentDidMount() {
...
// Puzzle file reception
this.socket.on('HELPER_RES', data => {
// Parse data to jSon
const jSon = JSON.parse(data);
// Store the character length of the new puzzle keyword
this.setState({ keyLen: jSon.nchr });
// Push the puzzle file to the div depending on its type
if (jSon.type === 'txt') // Encodé en UTF-8
this.refHelperRes.current.innerHTML = `<div style="text-align:left">${jSon.rsrc}</div>`;
else if (jSon.type === 'img') // Encodée en PNG
this.refHelperRes.current.innerHTML = `<img src=${jSon.rsrc} />`;
else if (jSon.type === 'snd') // Encodé en MP3
this.refHelperRes.current.innerHTML = `<audio controls="controls" autoPlay><source src=${jSon.rsrc} /></audio>`;
else if (jSon.type === 'vid') // Encodée en WebM
this.refHelperRes.current.innerHTML = `<video controls autoPlay loop><source type="video/webm" src=${jSon.rsrc}></video>`;
});
...
}
...
}