TP Puissance 4

Appel à commentaires

a marqué ce sujet comme résolu.

Bonjour,

J’ai réalisé la première partie du TP Puissance 4 du cours sur le langage C (sans IA donc). J’aimerais avoir des avis sur le programme que j’ai écrit et qui est découpé en 3 fichiers présentés ci-dessous.

main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "connect_four.h"

int main(void)
{
    init_board();

    const struct square *played_square;

    do
    {
        display_board();
        played_square = play_move();
        update_game_state(played_square);
    } while (strncmp(game_state, "ONGOING", sizeof game_state) == 0);

    display_board();

    if (strncmp(game_state, "VICTORY", STATE_LEN) == 0)
    {
        if (printf("Victoire du joueur %d !\n", turn) < 0)
        {
            fputs("printf failed when displaying the victorious player.\n", stderr);
            return EXIT_FAILURE;
        }
    }
    else
    {
        if (puts("Match nul.") == EOF)
        {
            fputs("puts failed when displaying the draw.\n", stderr);
            return EXIT_FAILURE;
        }
    }

    return EXIT_SUCCESS;
}
connect_four.h
#ifndef CONNECT_FOUR_H
#define CONNECT_FOUR_H

#include <stdbool.h>
#include <stdint.h>

#define LINES 6
#define COLUMNS 7

#define STATE_LEN 8

#define P1_TOKEN 'O'
#define P2_TOKEN 'X'

struct square
{
    uint_fast8_t line, column;
    char value;
};

extern struct square board[LINES][COLUMNS];
extern char game_state[STATE_LEN];
extern uint_fast8_t turn;

extern void init_board(void);

extern void display_column_numbers(void);
extern void display_board(void);

extern const struct square *play_move(void);
extern uint_fast8_t select_column(void);

extern bool playable_column(const uint_fast8_t column);
extern bool filled_column(const uint_fast8_t column);
extern bool filled_board(void);

extern void update_game_state(const struct square *const LAST_PLAYED_SQUARE);
extern bool four_tokens_aligned(const struct square *const LAST_PLAYED_SQUARE);
extern bool four_ascending_diag_aligned(const struct square *const LAST_PLAYED_SQUARE);
extern bool four_descending_diag_aligned(const struct square *const LAST_PLAYED_SQUARE);
extern bool four_horizontal_aligned(const struct square *const LAST_PLAYED_SQUARE);
extern bool four_vertical_aligned(const struct square *const LAST_PLAYED_SQUARE);

extern void clean_stdin(void);

#endif
connect_four.c
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "connect_four.h"

struct square board[LINES][COLUMNS];
char game_state[STATE_LEN] = "ONGOING";
uint_fast8_t turn = 1;

void init_board(void)
{
    for (uint_fast8_t i = 0; i < LINES; i++)
        for (uint_fast8_t j = 0; j < COLUMNS; j++)
            board[i][j] = (struct square){.line = i, .column = j, .value = ' '};
}

void display_column_numbers(void)
{
    for (uint_fast8_t j = 1; j < COLUMNS; j++)
        if (printf("  %" PRIuFAST8 " ", j) < 0)
        {
            fputs("printf failed when displaying the board.\n", stderr);
            exit(EXIT_FAILURE);
        }

    if (printf("  %d\n", COLUMNS) < 0)
    {
        fputs("printf failed when displaying the board.\n", stderr);
        exit(EXIT_FAILURE);
    }
}

void display_board(void)
{
    if (putchar('\n') == EOF)
    {
        fputs("putchar failed when displaying the board.\n", stderr);
        exit(EXIT_FAILURE);
    }

    display_column_numbers();

    for (uint_fast8_t j = 0; j < COLUMNS; j++)
        if (fputs("+---", stdout) == EOF)
        {
            fputs("fputs failed when displaying the board.\n", stderr);
            exit(EXIT_FAILURE);
        }
    if (puts("+") == EOF)
    {
        fputs("puts failed when displaying the board.\n", stderr);
        exit(EXIT_FAILURE);
    }

    for (uint_fast8_t i = LINES; i > 0; i--)
    {
        for (uint_fast8_t j = 0; j < COLUMNS; j++)
            if (printf("| %c ", board[i - 1][j].value) < 0)
            {
                fputs("printf failed when displaying the board.\n", stderr);
                exit(EXIT_FAILURE);
            }

        if (puts("|") == EOF)
        {
            fputs("puts failed when displaying the board.\n", stderr);
            exit(EXIT_FAILURE);
        }

        for (uint_fast8_t j = 0; j < COLUMNS; j++)
            if (fputs("+---", stdout) == EOF)
            {
                fputs("fputs failed when displaying the board.\n", stderr);
                exit(EXIT_FAILURE);
            }

        if (puts("+") == EOF)
        {
            fputs("puts failed when displaying the board.\n", stderr);
            exit(EXIT_FAILURE);
        }
    }

    display_column_numbers();

    if (putchar('\n') == EOF)
    {
        fputs("putchar failed when displaying the board.\n", stderr);
        exit(EXIT_FAILURE);
    }
}

const struct square *play_move(void)
{
    uint_fast8_t i = 0;
    const uint_fast8_t selected_column = select_column();

    while (board[i][selected_column].value != ' ')
        i++;

    board[i][selected_column].value = (turn == 1) ? P1_TOKEN : P2_TOKEN;

    return &board[i][selected_column];
}

uint_fast8_t select_column(void)
{
    uint_fast8_t selected_column;

    while (1)
    {
        if (printf("Joueur %d : ", turn) < 0)
        {
            fputs("printf failed when displaying prompt to the player.\n", stderr);
            exit(EXIT_FAILURE);
        }

        const int scanf_res = scanf("%" SCNuFAST8, &selected_column);
        clean_stdin();

        if (scanf_res == EOF)
        {
            fputs("scanf failed when reading player’s input.\n", stderr);
            exit(EXIT_FAILURE);
        }

        if (scanf_res != 1)
        {
            if (puts("Saisie incorrecte, veuillez saisir un chiffre entre 1 et 7 inclus.") == EOF)
            {
                fputs("puts failed when displaying information to the player.\n", stderr);
                exit(EXIT_FAILURE);
            }
        }
        else
        {
            selected_column--;

            if (!playable_column(selected_column))
            {
                if (puts("Saisie incorrecte, veuillez saisir un chiffre entre 1 et 7 inclus.") == EOF)
                {
                    fputs("puts failed when displaying information to the player.\n", stderr);
                    exit(EXIT_FAILURE);
                }
            }
            else if (filled_column(selected_column))
            {
                if (printf("La colonne %" PRIuFAST8 " est remplie, veuillez en choisir une autre.\n",
                           selected_column + 1) < 0)
                {
                    fputs("printf failed when displaying information to the player.\n", stderr);
                    exit(EXIT_FAILURE);
                }
            }
            else
            {
                if (putchar('\n') == EOF)
                {
                    fputs("putchar failed when displaying newline after player input.\n", stderr);
                    exit(EXIT_FAILURE);
                }

                return selected_column;
            }
        }
    }
}

bool playable_column(const uint_fast8_t column)
{
    return (column < COLUMNS) ? true : false;
}

bool filled_column(const uint_fast8_t column)
{
    return (board[LINES - 1][column].value != ' ') ? true : false;
}

bool filled_board(void)
{
    for (uint_fast8_t i = 0; i < COLUMNS; i++)
        if (!filled_column(i))
            return false;

    return true;
}

void update_game_state(const struct square *const SQUARE)
{
    if (four_tokens_aligned(SQUARE))
        strcpy(game_state, "VICTORY");
    else if (filled_board())
        strcpy(game_state, "DRAW");
    else
        turn = (turn == 1) ? 2 : 1;
}

bool four_tokens_aligned(const struct square *const SQUARE)
{
    return (four_ascending_diag_aligned(SQUARE) ||
            four_descending_diag_aligned(SQUARE) ||
            four_horizontal_aligned(SQUARE) ||
            four_vertical_aligned(SQUARE));
}

bool four_ascending_diag_aligned(const struct square *const SQUARE)
{
    uint_fast8_t counter = 1;

    for (uint_fast8_t i = SQUARE->line, j = SQUARE->column; i > 0 && j > 0; i--, j--)
        if (board[i - 1][j - 1].value == SQUARE->value)
        {
            if (++counter == 4)
                return true;
        }
        else
            break;

    for (uint_fast8_t i = SQUARE->line + 1, j = SQUARE->column + 1; i < LINES && j < COLUMNS; i++, j++)
        if (board[i][j].value == SQUARE->value)
        {
            if (++counter == 4)
                return true;
        }
        else
            break;

    return false;
}

bool four_descending_diag_aligned(const struct square *const SQUARE)
{
    uint_fast8_t counter = 1;

    for (uint_fast8_t i = SQUARE->line + 1, j = SQUARE->column; i < LINES && j > 0; i++, j--)
        if (board[i][j - 1].value == SQUARE->value)
        {
            if (++counter == 4)
                return true;
        }
        else
            break;

    for (uint_fast8_t i = SQUARE->line, j = SQUARE->column + 1; i > 0 && j < COLUMNS; i--, j++)
        if (board[i - 1][j].value == SQUARE->value)
        {
            if (++counter == 4)
                return true;
        }
        else
            break;

    return false;
}

bool four_horizontal_aligned(const struct square *const SQUARE)
{
    uint_fast8_t counter = 1;

    for (uint_fast8_t j = SQUARE->column; j > 0; j--)
        if (board[SQUARE->line][j - 1].value == SQUARE->value)
        {
            if (++counter == 4)
                return true;
        }
        else
            break;

    for (uint_fast8_t j = SQUARE->column + 1; j < COLUMNS; j++)
        if (board[SQUARE->line][j].value == SQUARE->value)
        {
            if (++counter == 4)
                return true;
        }
        else
            break;

    return false;
}

bool four_vertical_aligned(const struct square *const SQUARE)
{
    if (SQUARE->line < 3)
        return false;

    uint_fast8_t counter = 1;

    for (uint_fast8_t i = SQUARE->line; i > 0; i--)
        if (board[i - 1][SQUARE->column].value == SQUARE->value)
        {
            if (++counter == 4)
                return true;
        }
        else
            break;

    return false;
}

void clean_stdin(void)
{
    int c;

    do
    {
        c = getchar();
    } while (c != '\n' && c != EOF);

    if (ferror(stdin))
    {
        fputs("getchar failed while cleaning stdin.\n", stderr);
        exit(EXIT_FAILURE);
    }
}

Quelques remarques préliminaires.

  1. La fonction main() contient peut-être encore « trop » de logique, j’hésite à ajouter une fonction play() séparée, mais je ne sais pas si cela améliore vraiment le découpage ou non. Si c’est le cas, quelle serait la fonction de main() ?
  2. Vous remarquerez sans doute que j’« abuse » de vérifications sur le retour des fonctions d’affichage sur la sortie standard. Je l’ai fait à titre d’exercice pour évaluer la lourdeur que cela engendre (et c’est effectivement très lourd à faire) mais une question m’est venue à l’esprit en le faisant : existe-t-il des cas (mêmes rares) où la sortie standard serait indisponible mais pas l’erreur standard ?
  3. Je me demande si la structure struct square est vraiment la plus adéquate ici ? J’ai lu d’autres messages sur le forum commentant des TP de Puissance 4 d’autres membres, et j’ai vu que l’on suggérait notamment d’utiliser plutôt une structure position. Quels en serait les avantages ? Je pense que cela serait utile si l’on cherchait à échanger de place deux jetons, mais ce n’est pas pertinent pour ce jeu.
  4. Je pense que les fonctions vérifiant s’il y a 4 jetons alignés sont simplifiables, mais je ne sais pas vraiment comment m’y prendre. Je les trouve un peu compliquées et redondantes (même leurs noms ne me satisfont pas).
  5. D’ailleurs, dans les fonctions vérifiant l’alignement de 4 jetons en diagonales, j’utilise l’opérateur , dans les boucles for pour parcourir les lignes et les colonnes en même temps. Je pense que le résultat est lisible, mais il y a peut-être une meilleure alternative ?
  6. Je ne suis pas sûr de bien utiliser le mot-clé extern dans l’entête connect_four.h. En particulier, je me dis que je pourrais contrôler plus finement la portée des variables et fonctions déclarées à l’intérieur, de sorte que seules celles effectivement utilisées par main.c lui soient visibles, tout en gardant les déclarations des fonctions « internes » à connect_four.c dans l’entête.
  7. J’ai décidé en outre de rendre les variables stockant le plateau du jeu, l’état de la partie et le tour de la partie globales, car il me semble que cela simplifie grandement l’écriture des fonctions et que c’est plus adéquat (il n’y a bien qu’un seul plateau dans tout le programme). Y voyez-vous un inconvénient ? En bonus, je me suis demandé s’il existait une fonctionnalité d’un langage qui permette de préciser que telle fonction n’a pas accès aux variables globales ? Je ne pense pas que ce soit le cas dans le langage C, mais je me posais la question aussi pour les autres langages de programmation.
  8. Dans le fichier connect_four.c à la ligne 198, je modifie la variable turn pour représenter le changement de tour de jeu (et ainsi que c’est à l’autre joueur de jouer). Je suis quasiment sûr qu’il est possible de faire cela sans utiliser de condition, étant donné que je représente le tour de jeu par 1 (c’est au joueur 1 de jouer) ou 2 (c’est au joueur 2 de jouer), mais je n’arrive pas à trouver comment. Pouvez-vous m’aider ?
+0 -0

Salut,

Je n’ai le temps de répondre qu’aux premières questions de la liste, mais :

  1. Je n’ai pas d’avis sur la question, pour moi c’est clair et concis comme ça.
  2. C’est possible si la sortie standard est redirigée (via un shell par exemple) vers un fichier et qu’il n’y a plus d’espace sur la partition qui le contient pour tout écrire ; ou si la sortie standard est redirigée vers l’entrée d’une commande et que cette commande termine brutalement (à vérifier).
  3. Je ne vois pas trop en quoi consisterait une structure position. Mais il me semble redondant de stocker la position dans chaque square, puisque vu que tu as une matrice de square, la matrice « sait » déjà à quelle position sont ses éléments.
  4. Il n’y a pas d’amélioration qui me vienne à l’esprit (mais je ne prétends pas y avoir réfléchi plus d’une minute).
  5. Non, c’est idiomatique.

Quand à la question 8, tu peux remplacer turn par un booléen player1 qui est vrai si c’est le tour de 1, faux sinon, et il te suffit de faire player1 = !player1. Tu peux aussi garder un entier, mais plutôt entre 0 et 1, et faire turn = (turn + 1) % 2. Mais ce serait encore mieux avec une énumération.

En parlant d’énumérations, je suppose que tu n’es pas encore arrivé à ce point du cours mais quand ce sera le cas, tu verras que tu peux les utiliser à la place de chaînes de caractères pour game_state.

J’ai décidé d’implémenter une structure position puisque cela évite effectivement une certaine redondance.

struct position
{
    uint_fast8_t line, column;
};

Merci pour l’idée pour le changement de tour. Il n’y a en réalité pas besoin de changer les valeurs 1 et 2, il suffit d’utiliser la formule turn = turn % 2 + 1.

On peut en effet utiliser les énumérations ici, mais je me suis astreint à ne pas utiliser les concepts non encore vus dans le cours (même si je fais quelques exceptions, comme pour les types de l’entête stdint).

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte