Liemiers
Jeu d'énigmes
Présentation

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.

Socket

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>`;
            });
        
            ...
        
        }
        
        ...
        
    }
            
Screenshots