Competitive Real-Time Trivia Game

Multi-threaded client-server app

Project Details

For this project, I collaborated with Timothé Dupuch to create a multi-threaded application based on the French game show "Questions pour un Champion." Our application features a server thread that communicates questions to clients and retrieves their responses and thinking times. Clients compete by buzzing in and providing answers, earning points for correct responses. If a client answers incorrectly, they lose a point and their buzzer is blocked for that question. The server handles game flow, updates scores in real-time, and displays rankings. We employed communication, synchronization, and pseudo-random number generation techniques to ensure a seamless gameplay experience.


Part 1: Project Presentation




Subject


From a technical standpoint, the application is multi-threaded, with a main server thread that communicates questions to clients and retrieves their responses and respective thinking times. On the client side, message reception from the server is handled by a dedicated thread.

Our project consists of recreating a general knowledge game heavily inspired by the game "Questions pour un Champion," specifically its first phase, the "9 points gagnants" (9 winning points).

Game rules


The server establishes a connection and accepts 3 virtual clients (clients that can connect). The choice of 3 clients corresponds to the operating mode of the game "Questions pour un Champion" but can be freely modified using the following line in "server.c":

#define MAX_CLIENTS 3

In our case, we will stick to 3 clients. If a greater number of clients try to connect, an error will be raised. Each of our clients has a predefined set of answers. We have chosen to provide them with a shared set of answers to give them an equal chance of winning the game. These 100 items of knowledge are stored in the text file "reponses.txt" and can be surnames, dates, or places.

When the server asks a question to the clients, they take their time to answer, just like a human would take time to think before buzzing in. Upon receiving the question from the server, each of the 3 clients independently generates a random number. This number represents their own thinking time. They will wait for this duration before buzzing in and providing an answer.

Once all 3 clients have submitted their proposed answers to the server, the server will consider the response of the client who answered first.

If the answer is correct, the client earns 5 points, and the server moves on to the next question. In this case, all buzzers are reset, and the server sends a new question, restarting the previous process.

If the proposed answer is incorrect, the server deducts 1 point from the client who made the mistake, blocks their buzzer (rendering them unable to participate in the current question), and now focuses on the remaining clients.

It would make no sense to consider their previously submitted answer because a real player can be influenced by the incorrect answer provided by the player who made the mistake. Thus, the remaining players will restart their thinking and buzzing process to propose a new answer. The server will wait for their responses in the same manner as before, with the exception that it will now ignore the player who made the mistake (since their buzzer is disabled for this question).

Each time a player submits an answer, whether it is correct or incorrect, the server informs them if they are right or wrong, updates their score, and displays real-time score changes by providing a ranking at time "t."

The number of questions is arbitrary and can be modified in "server.c":

#define NB_QUESTIONS 10

A game consists of a sequence of multiple questions, each of which can end in two different ways:

  • a client gives the correct answer
  • all clients give incorrect answers


  • Program Organization


    Code Structure

    In addition to the `main.c`, we have chosen to take advantage of modularity by segmenting our code as follows:

  • Source files: server.c / client.c / functions.c
  • Header files: server.h / client.h / functions.h

  • We also have two files that contain the questions and the knowledge bases of the clients: `questions.txt` and `reponses.txt`. Finally, the `Makefile` that allows the code to be compiled.

    Used Libraries and Standards

    To avoid excessive dependencies, we decided not to include the header file `pse.h`, which could potentially contain non-essential code libraries for our project. We have chosen to include the following libraries throughout the files or only in some of them:

  • stdio.h
  • stdlib.h
  • string.h
  • unistd.h
  • sys/socket.h
  • arpa/inet.h
  • pthread.h
  • fcntl.h
  • semaphore.h
  • time.h

  • Note: This reduces the dependencies by half compared to the TD sessions.

    Our code complies with the ANSI C (C89 or C90) and POSIX.1b-1993 (POSIX) standards. Our compilation flags are as follows:

    CFLAGS= -D_POSIX_C_SOURCE=199309L -W -Wall -ansi -pedantic
    

    The `-W` and `-Wall` flags enable a wide range of potential programming issues to be detected. `Wall` stands for "all warnings."

    The `-pedantic` flag activates additional warnings to comply with the standards of the C language. It helps detect non-compliant constructions and encourages more rigorous programming.

    Encountered Difficulties



    Communication Between the Server and Clients

    Establishing bidirectional communication between the server and clients was a challenge. It was necessary to read the messages sent by the clients and send other messages to all connected clients. Synchronization using mutexes and semaphores, as well as managing threads for each client, were crucial and particularly delicate steps.

    Synchronization of Client Actions

    When multiple clients were connected, it was essential to synchronize their actions. For example, waiting for all clients to be connected before starting the game or allowing only one client to buzz in at a time during a question. The use of semaphores or mutexes was necessary to ensure consistent and ordered execution. Using a structure to represent a client greatly facilitated our operations:

    Pseudo-Random Number Generation

    An important part of our quiz relies on the concept of pseudo-random number generation. Computers are deterministic machines, and it is not possible to generate true random numbers in programming. Instead, pseudo-random number generators (PRNGs) are used to simulate sequences of numbers that appear random.

    In our code, pseudo-random generation is used in selecting the client's answers. It is not possible to simply use a function like `rand()` because it would generate the same sequence for each client. To differentiate the clients, their Process ID is used with a call to the `getpid()` function.

    Management of the Sequence of Actions

    In terms of gameplay, there is a sequence of instructions that must occur in the correct order (in addition to proper synchronization as mentioned above) for the client-server dialogue to work correctly. First, the server must be ready to receive client requests and send them questions. The clients must connect to the server and wait to receive instructions. Once the questions are sent to the clients, they must propose an answer and transmit their respective thinking times to the server. The server must then collect the responses from all clients, process them to calculate the scores, and finally send the results back to the clients for display.



    Part 2: How to Play




    Installation of the Archive


    Extract the `DUPUCH.GACHET.tar` archive and navigate to the project directory using the following command:
    Execute the `Makefile` and create the executable using the command:
    At this stage, an executable simply named `my_program` has been created by us.

    Running the Programs


    To start a server, you can execute the following line:
    Note: The port number can be modified within the available range.

    Next, you need to connect our three clients on separate terminal instances...

    Connect Client 1:
    Connect Client 2:
    Connect Client 3:


    Gameplay


    During the gameplay, here's what is displayed on the server:
    The game is about to begin.
    The server has received the three answer proposals. Additionally, it has also received the response times of the 3 clients to determine which client was the fastest to buzz in.
    The server has identified that player 2 was the first to press the buzzer and propose the answer "United States". Since client 2 did not provide the correct answer, they lose one point, and their buzzer is now blocked for the rest of the question. For informational purposes, the server displays the scores and the current ranking.

    The question is still active, so the server will continue to wait for a new player to press the buzzer and propose new answers after the elimination of player 2. Obviously, player 2 will no longer be able to buzz for this question and will have to wait for the next one.
    Similar to client 2, client 3 was faster than client 1 to buzz in, but they submitted an incorrect answer. Therefore, they lose 1 point, and their buzzer is blocked. Client 1, with hope in their heart and racing heartbeat, makes their move towards the buzzer...
    Yahouuuu! Client 1 has answered the question correctly. They earn 5 points and take the lead in the game!
    But this is just the beginning, a long series of questions lies ahead. Will they be able to maintain their title of champion? The suspense is at its peak, to find out the outcome of this great adventure, you just have to compile...

    On the client terminals, here's what was displayed throughout the game:

    Terminal Display


    Example of the server part execution: