Jeu du Tic-Tac-Toe

Principe

Voici un exemple de projet qu'il est possible de développer en 1ère NSI : le jeu du Tic-Tac-Toe (ou morpion) n'a que très peu d'intérêt en lui-même (sauf peut-être pour de jeunes enfants) mais bien entendu ici nous recherchons un intérêt pédagogique pour l'apprentissage des langages Python ou Javascript.

Ce jeu a d'abord été créé en mode console en Python, puis adapté en version graphique Web en adaptant le code Python en Javascript. L'utilisation d'un canvas permet de gérer l'interface de jeu graphique.

Jouer

Lancer le jeu pour une démo.

Le code

Fichier HTML


<!DOCTYPE html>
<html lang="fr">
<head>
    <title>Tic Tac Toe</title>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
    <h1>Jeu du Tic-Tac-Toe</h1>
    <div id="jeu">
        <h4 id="status">Cliquer sur Commencer pour démarrer la partie</h4>
        <canvas id="plateau">
            <p>Votre navigateur ne prend pas en charge les Canvas HTML5... Dommage !</p>
        </canvas>
        <p><button onclick="partie()" id="bouton">Commencer</button></p>
    </div>

    <script src="script.js"></script>
</body>
</htmL>

Fichier Javascript


        "use strict";

// paramètres du jeu 
const DIM = 3;
const VIDE = 0;
const CASE = 150;
const RAYON = 0.7 * CASE/2;
const JOUEURS = new Map([[1 , ['blue', 'bleu'] ],  [-1 , ['green', 'vert']]]); // dictionnaire
const NB_COUPS_MAX = DIM * DIM;
//****** fin paramètres  

//* Variables globales : pour faciliter la communication au seind des fonctions 
let status = document.getElementById('status');
let bouton = document.getElementById('bouton');
let canvas = document.querySelector('#plateau');
let plateau = canvas.getContext('2d'); // conserve une référence (Context) à la zone graphique pour 1 graphe 2D 
const largeur = canvas.width = CASE * DIM;
const hauteur = canvas.height = CASE * DIM;
let joueur = 1;
let grille = [];
let nb_coups = 0;
//******* fin variables globales 

function caseCliquee(evt){
    // récupère la position du clic sur le plateau et appelle la fonction jouer si la case est libre 
    let x = evt.offsetX; // Position de la souris par rapport à l'élément appelant
    let y = evt.offsetY;
    let c = Math.trunc(x / CASE);
    let l = Math.trunc(y / CASE);
    if (grille[l][c] == VIDE){
        jouer(l, c);
    }
}

function dessinerPion(l, c, couleur){
    // Dessine un pion dans la case (l, c) de la bonne couleur 
    plateau.fillStyle = couleur;
    plateau.beginPath();
    plateau.arc(CASE/2 + c*CASE, CASE/2 + l*CASE, RAYON, 0, 2*Math.PI, true);
    plateau.fill();
}

function jouer(l, c){
    // moteur principal du jeu 
    nb_coups++;
    let couleur = JOUEURS.get(joueur)[0];
    grille[l][c] = joueur; // maj de la grille
    dessinerPion(l, c, couleur);
    afficherGrille(); // retour console facultatif 
    if (gagnant()){
        canvas.removeEventListener("click", caseCliquee);
        status.textContent = "Gagnant : joueur "+ JOUEURS.get(joueur)[1]+" !!!";
        bouton.textContent = "Rejouer";
        bouton.setAttribute("onclick", "partie()");
        return
    }
    if (nb_coups == NB_COUPS_MAX){
        canvas.removeEventListener("click", caseCliquee);
        status.textContent = "Fin du jeu ! Match nul...";
        bouton.textContent = "Rejouer";
        bouton.setAttribute("onclick", "partie()");
        return
    }
    joueur = - joueur; // changement de joueur
}

function partie(){
    // Initialisation d'une nouvelle partie
    nb_coups = 0;
    bouton.textContent = "Partie en cours...";
    bouton.removeAttribute("onclick");
    status.textContent = "Cliquer sur une case pour jouer";
    grille = creerGrille(DIM, VIDE);
    canvas.addEventListener("click", caseCliquee);
    dessinerPlateau();
    afficherGrille(); // retour console facultatif 
}

function dessinerPlateau(evt) {
    plateau.clearRect(0, 0, largeur, hauteur); // nettoie tout le canvas 
    plateau.strokeStyle = 'red';
    for (let i = 0; i < DIM; i++) {
        plateau.beginPath();
        plateau.moveTo(0, CASE * i);
        plateau.lineTo(CASE * DIM, CASE * i);
        plateau.stroke();

        plateau.beginPath();
        plateau.moveTo(CASE * i, 0);
        plateau.lineTo(CASE * i, CASE * DIM);
        plateau.stroke();
    }
    plateau.strokeStyle = 'black';
    plateau.strokeRect(0, 0, largeur, hauteur);
}

function creerGrille(dim = 3, vide = 0) {
    // Renvoie une grille carrée de dimension 'dim', remplie de 'vide' 
    let grille = [];
    for (let i = 0; i < dim; i++) {
        let ligne = [];
        for (let j = 0; j < dim; j++) {
            ligne.push(vide);
        }
        grille.push(ligne);
    }
    return grille
}

function afficherGrille(){
    // Rendu graphique en console de la grille 
    console.log(grille);
}

dessinerPlateau();

function gagnant(){
// Renvoie true si le joueur gagne (sinon false)
    // ligne gagnante ?
    for (let l =0 ; l < DIM; l++){
        let c = 0;
        while (c < DIM && grille[l][c] == joueur){
            c++;
        }
        if (c == DIM){
            return true;
        }
    }
    // colonne gagnante ?
    for (let c =0 ; c < DIM; c++){
        let l = 0;
        while (l < DIM && grille[l][c] == joueur){
            l++;
        }
        if (l == DIM){
            return true;
        }
    }
    // diagonale 1 gagnante ?
    let n = 0;
    while (n < DIM && grille[n][n] == joueur){
        n++;
    }
    if (n == DIM){
        return true;
    }
    // diagonale 2 gagnante ?
    let m = 0;
    while (m < DIM && grille[m][DIM - 1 - m] == joueur){
        m++;
    }
    if (m == DIM){
        return true;
    }
    return false; // le joueur n'a pas gagné
}
        

Fichier Python version tkinter


#!/usr/bin/env python
# coding: utf-8

# Version Tkinter
import tkinter as tk

def creer_grille(dim=3, vide=0):
    """ Renvoie une grille carrée de dimension 'dim', remplie de 'vide' """
    return [[vide for i in range(dim)] for j in range(dim)]

def gagnant():
    """ Renvoie True si le joueur gagne (sinon False) """
    # ligne gagnante ?
    for l in range(DIM):
        c = 0
        while c < DIM and grille[l][c] == joueur:
            c = c + 1
        if c == DIM:
            return True
    # colonne gagnante ?
    for c in range(DIM):
        l = 0
        while l < DIM and grille[l][c] == joueur:
            l = l + 1
        if l == DIM:
            return True
    # diagonale 1 gagnante ?
    n = 0
    while n < DIM and grille[n][n] == joueur:
        n = n + 1
    if n == DIM:
        return True
    # diagonale 2 gagnante ?
    n = 0
    while n < DIM and grille[n][DIM - 1 - n] == joueur:
        n = n + 1
    if n == DIM:
        return True
    return False # le joueur n'a pas gagné     

def afficher_grille():
    """ Rendu graphique en console de la grille """
    print("État de la grille :")
    for l in range(DIM):
        for c in range(DIM):
            print(str(grille[l][c]).rjust(5), end='')
        print()
    print()

def dessiner_grille():
    """ Dessine une grille vierge dans le Canvas plateau """
    plateau.delete(tk.ALL)
    for i in range(1, DIM):
        plateau.create_line(0, CASE*i, CASE*DIM, CASE*i, fill='red')
        plateau.create_line(CASE*i, 0,CASE*i, CASE*DIM, fill='red')

def dessiner_pion(l, c, couleur):
    """ Dessine un pion dans le Canvas plateau """
    plateau.create_oval(CASE*c+CASE//2-RAYON, CASE*l+CASE//2-RAYON, CASE*c+CASE//2+RAYON, CASE*l+CASE//2+RAYON, fill=couleur, outline=couleur)

def partie(*evt):
    """ Initialise une partie """
    global grille, joueur, nb_coups, nb_coups_max # ca facilite la communication pour toutes les fonctions 
    info.set("Cliquer sur une case libre pour jouer")
    rejouer.configure(state=tk.DISABLED)
    grille = creer_grille(DIM, VIDE)
    nb_coups = 0
    nb_coups_max = len(grille) ** 2
    joueur = 1 # 1er joueur
    afficher_grille() # retour console facultatif
    dessiner_grille()
    plateau.bind('<Button>', jouer_clic)

def jouer(l, c):
    """ Joue un pion et met la grille à jour  """
    global grille, joueur, nb_coups
    grille[l][c] = joueur # maj de la grille
    couleur = JOUEUR[grille[l][c]][0]
    dessiner_pion(l, c, couleur)
    afficher_grille() # retour console facultatif
    nb_coups = nb_coups + 1
    if gagnant():
        plateau.unbind('<Button>')
        rejouer.configure(state=tk.NORMAL)
        print(f"Bravo {joueur}, tu as gagné !") # retour console facultatif
        info.set(f"Joueur {JOUEUR[joueur][1]} a gagné !")
        return
    if nb_coups == nb_coups_max:
        plateau.unbind('<Button>')
        rejouer.configure(state=tk.NORMAL)
        print("Match nul...")   # retour console facultatif
        info.set(f"Match nul...")
        return
    joueur = - joueur

def jouer_clic(evt):
    """ Convertit le clic en une case : ligne, colonne """
    l, c =  evt.y // CASE, evt.x // CASE
    if grille[l][c] == VIDE: # on vérifie que le joueur a cliqué sur une case libre
        jouer(l, c)

# paramètres du jeu
DIM = 3
VIDE = 0
CASE = 150
RAYON = 0.7 * CASE//2
JOUEUR = {-1 : ('blue', 'bleu'), 1 : ('green', 'vert')}
######################

jeu = tk.Tk()
jeu.title("Jeu Tic Tac Toe")
tk.Button(jeu, text="Quitter", command=jeu.destroy).pack(side=tk.BOTTOM)

rejouer = tk.Button(jeu, text="Rejouer", command=partie, state=tk.DISABLED)
rejouer.pack(side=tk.BOTTOM)

info = tk.StringVar()
tk.Label(jeu, textvariable=info).pack()
plateau = tk.Canvas(jeu, width=CASE*DIM, height=CASE*DIM, bg='white')
plateau.pack()

partie()

jeu.mainloop()