Partage
  • Partager sur Facebook
  • Partager sur Twitter

La belote en Python

Poser les bases avec les classes + mypy + y aller petit à petit

12 mai 2019 à 23:43:41

Bonjour à tous,

Je réponds souvent aux sujets du forum (pour le meilleur ou pour le pire ? :p), mais avec la vague des sujets orientés ISN qui demandent de l'aide en Tkinter que je ne maitrise pas (n'ayant pas fait moi même de projet Tkinter), je me suis dit : pourquoi ne pas faire un petit projet moi même en Tkinter ? ça peut également être un bon exercice d'écriture, qui pourrait peut-être évoluer vers un tutoriel, ou un article de blog.. Je ne sais pas trop quoi en faire encore.

Mais quittons l'introduction des motivations : ce que je vais vous présenter maintenant n'a en fait rien à voir avec Tkinter. Parce que j'ai pris le choix de développer mon jeu en interface pûrement textuelle avant d'en faire une interface graphique, afin de maitriser les éléments logiques de mon jeu avant de me lancer dans l'inconnu des boutons.

Ce que je vous écris là est le résultat de 2-3 heures de code parsemés de bonnes doses de procrastination, et de recherches perso sur les outils existants (surtout mypy).

Je pré-suppose que vous savez déjà comment jouer à la Belote. Si vous ne savez pas, pour la partie que j'ai écrite, il faut juste savoir : ça se joue avec un jeu de 32 cartes, la phase initiale de distribution consiste à piocher 3 cartes à chaque joueur (4 joueurs), puis 2 cartes. Puis on retourne la première carte du paquet, et les joueurs à tour de rôle choisissent de "prendre" ou non la carte. Ensuite, on redistribue les cartes : 2 cartes pour celui qui a déjà pris la première carte, 3 cartes pour les autres.

La totalité du projet est disponible à cette adresse : https://github.com/potterman28wxcv/python-belote

J'ai découpé mon projet en plusieurs fichiers, chacun s'occupant d'un aspect particulier :

  • cards.py : gère les cartes et classes associées (Deck, Hand)
  • player.py : gère les joueurs en terme d'information, c'est à dire stocker leur nom, et leur Hand. C'est aussi ici que je mettrai plus tard les "prises de décision" : IA ou input de la ligne de commande
  • table.py : contient la classe Table qui stocke tout ce qui est "visible" sur la table : les joueurs, le deck, le "trick" (anglais pour "pli" - je vais surement remplacer trick par pli plus tard). Plus tard, il faudra sûrement rajouter les piles de cartes à côté de chaque joueur etc..
  • utils.py : des fonctions annexes qui ne rentrent dans aucune catégorie
  • game.py : la logique de mon jeu. En particulier, la classe Gamestate qui contient "l'état" de mon jeu, c'est à dire : la Table, mais aussi les infos "physiquement invisibles" nécessaires au déroulement de la partie comme : l'atout en jeu, qui est le preneur, qui est le "premier joueur" (celui qui démarre le tour)
  • main.py : contient un bête appel à game.game_loop(). Plus tard, je rajouterais sûrement des options lignes de commande, et le lien entre la partie graphique et le jeu (à décider)

Si on commence par cards.py, j'ai commencé par définir ce que c'est qu'une couleur (trèfle, pique, ..) et un rang (10, valet, ..). Il y a plusieurs façons de faire ça, j'ai choisi d'utiliser les Enum de python3

@unique
class Color(Enum):
  CLUBS, DIAMONDS, HEARTS, SPADES = range(4)

@unique
class Rank(Enum):
  ACE, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING = range(8)

Le @unique est une décoration du module Enum permettant de s'assurer que deux éléments de l'énumération n'ont pas la même valeur. ça permet de s'assurer que, par exemple, Color.CLUBS et Color.DIAMONDS n'ont pas la même valeur - permettant d'éviter des incidents facheux tel que Color.CLUBS == Color.DIAMONDS qui vaudrait True.

Normalement, utiliser range suffit à satisfaire la propriété, mais on n'est jamais trop sûr, je préfère vérifier trop que pas assez.

Vient ensuite la définition de ce que c'est qu'une carte, assez naturelle :

class Card:
  def __init__(self, color: Color, rank: Rank) -> None:
    self.color = color
    self.rank = rank

  def __str__(self) -> str:
    return "(" + str(self.color) + ", " + str(self.rank) + ")"

Je définis donc une Card comme étant un Color et un Rank. Et je définis également une méthode __str__ pour que ça affiche du contenu joli quand je fais print(carte) ;)

J'utilise la notation (: Color) pour annoter mon code avec les types (l'annotation signifie que color doit avoir le type Color ; quant à "-> None", ça signifie que la fonction ne retourne rien). ça permet, par la suite, en utilisant mypy (un checkeur statique), que je n'ai pas fait de bêtises en écrivant du code. Par exemple, écrire le code suivant retournerait une erreur lors du check de mypy :

fausse_carte = Card("Bidule", "Toto")

Parce que "Bidule" n'est pas un objet Color, mais un string.

A première vue ça parait lourd à écrire.. Mais ça a l'énorme avantage d'éliminer nombre de bugs que je n'aurais trouvé qu'en exécutant mon programme, au lieu de les trouver avant même de l'exécuter. Etant très étourdi moi même, je trouve cet outil mypy très utile.

Sauf que voilà, écrire mypy à chaque fois en ligne de commande avant chaque exécution c'est fastidieux.. Surtout que je suis du genre à tester mes programmes très souvent et à y aller incrémentalement. J'ai donc écrit un petit Makefile :

MAIN=main

PYFILES=$(wildcard *.py)

all: $(MAIN).tok

%.tok: $(MAIN).py $(PYFILES)
	@mypy $< && touch $@

.PHONY:
run: $(MAIN).tok
	@./$(MAIN).py

Je ne vais pas trop aller dans les détails : mais dès que l'un des fichiers Python a été modifié, le Makefile va appeler mypy et générer un fichier main.tok (c'est la règle qui commence par %.tok). La règle "run" va exécuter le programme main.py que si le main.tok est à jour. Pour lancer mon programme, je fais juste un simple

make run

Et si je veux juste faire le check sans le lancer, je fais un simple "make", qui ne fera rien si il détecte que les fichiers Python ont déjà été checkés. J'aime bien transmettre ma fainéantise aux programmes que j'écris en général. Pourquoi refaire la vaisselle quand elle a déjà été faite ? :)

Si je continue le contenu de cards.py :

class ListCards:
  def __init__(self, cards: List[Card] = []) -> None:
    self.cards = cards.copy()

  def __str__(self) -> str:
    return list_str(self.cards)


class Deck(ListCards):
  def __init__(self, cards: List[Card] = []) -> None:
    ListCards.__init__(self, cards)
    self.topcard : Optional[Card] = None

  def draw(self) -> Card:
    self.topcard = None
    return self.cards.pop()

  def shuffle(self) -> None:
    random.shuffle(self.cards)

  def reveal(self) -> None:
    self.topcard = self.cards[-1]

  def __str__(self) -> str:
    return "{ \ncards: " + ListCards.__str__(self) + ", \ntopcard: " + str(self.topcard) + "}"


class Hand(ListCards):
  def __init__(self, cards: List[Card] = []) -> None:
    ListCards.__init__(self, cards)

  def add(self, card: Card) -> None:
    self.cards.append(card)


class Trick(ListCards):
  pass


if __name__ == "__main__":
  card = Card(Color.CLUBS, Rank.ACE)
  print(card)

  card2 = Card(Color.HEARTS, Rank.TEN)
  card3 = Card(Color.DIAMONDS, Rank.TEN)

  deck = Deck([card, card2, card3])
  print(deck)

J'ai une classe ListCards un peu moche qui est là pour avoir un socle commun entre Deck et Hand (Deck et Hand sont tous les deux des listes de cartes en essence). Mais c'est pas idéal écrit comme ça, sûrement que dans le futur j'enlèverai ListCards, ça embrouille l'esprit plus qu'autre chose.

Sinon, j'ai un Deck et un Hand : avec le Deck, je peux faire des draw pour piocher carte par carte, shuffle pour le mélanger, reveal pour dévoiler la première carte du deck (qui est la carte de l'atout à la belote). Quant à la classe Hand, pour l'instant j'ai juste une méthode pour rajouter des cartes à la main - mais on pourrait imaginer aussi une méthode qui trie les cartes par valeur.

Et j'ai aussi un mini test unitaire à partir du if __name__ == "__main__" : cette construction bizarroide me permet de lancer cards.py directement et d'exécuter le code qu'il y a, sans qu'il soit exécuté quand j'importe le module. Je peux donc faire :

python3.6 cards.py

Qui affiche :

(Color.CLUBS, Rank.ACE)
{
cards: [(Color.CLUBS, Rank.ACE), (Color.HEARTS, Rank.TEN), (Color.DIAMONDS, Rank.TEN)],
topcard: None}

cards.py étant le tout premier fichier que j'ai écris, je voulais m'assurer que le peu de fonction que j'avais écris jusqu'alors marchait comme il faut. Je ne teste pas grand chose d'ailleurs - mais je ne me sentais pas de continuer à écrire d'autres fichiers sans m'assurer d'abord que ce fichier fonctionnait correctement. Moins j'en teste à la fois, mieux je me porte ;) Encore la fainéantise..

Sur le même principe, j'ai aussi les fichiers table.py et player.py qui sont très simples. Mais sans test unitaire, parce que itérativement je testais avec mon main.py

from player import *

class Table:
  def __init__(self, players: List[Player], deck: Deck) -> None:
    self.players = players
    self.deck = deck
    self.trick = Trick()

  def __str__(self) -> str:
    return ("{ players: " + list_str(self.players) + ", \ntrick: " + str(self.trick) +
              ", \ndeck: " + str(self.deck) + "}")
from cards import *

class Player:
  def __init__(self, name: str) -> None:
    self.name = name
    self.hand: Hand = Hand()

  def __str__(self) -> str:
    return "{ name: " + str(self.name) + ", \nhand: " + str(self.hand) + "}"

  def draw(self, deck: Deck) -> None:
    card = deck.draw()
    self.hand.add(card)


class Team:
  def __init__(self, name: str, players: List[Player]) -> None:
    self.name = name

    if len(players) != 2:
      raise Exception("Expected a team of 2 players, got {} instead".format(len(players)))
    self.players = players.copy()
    self.score = 0

  def __str__(self) -> str:
    return ("{ name: " + str(self.name) + ", players: " + list_str(self.players)
              + ", score: " + str(self.score) + "}")

(la classe Team n'est pas encore utilisée)

Le gros du gros que je vais présenter maintenant, c'est game.py. C'est là où il y a toute la logique du jeu. C'est à dire que mon main.py est extrèmement court :

#!/usr/bin/python3.6

from table import Table
from player import Player
import game
import random

random.seed(0) # same seed for now
game.game_loop()

Tout ce que fait mon main : j'initialise ma seed à 0 afin d'avoir tout le temps le même résultat aléatoire. ça a l'avantage de pouvoir lancer plusieurs fois de suite le programme et ça produira les mêmes résultats "aléatoires" - ce qui est beaucoup plus facile pour déboguer.

Et je rentre dans game.game_loop().. Allons voir ce qu'il y a dans game.py

class Gamestate:
  def __init__(self, table: Table, first_player: Player) -> None:
    self.table = table
    self.first_player = first_player
    self.atout : Optional[Color] = None
    self.preneur : Optional[Player] = None

  @property
  def iter_players(self) -> List[Player]:
    first_player_index = self.table.players.index(self.first_player)
    return list_rotate(self.table.players, first_player_index)

  def __str__(self) -> str:
    return ("{ table: " + str(self.table)
            + ", \nfirst player: " + str(self.first_player.name)
            + ", \natout: " + str(self.atout)
            + ", \npreneur: " + (str(self.preneur.name) if self.preneur else "None") + "}")

Là j'ai une classe un tout petit peu plus compliquée que les autres.

Déjà, le __init__ : Optional[Color] c'est pour indiquer que le membre peut être à None, ou être de type Color. Le très gros avantage de ça, est que par la suite, si j'ai le malheur d'accéder au membre state.atout sans d'abord tester que state.atout ne vaut pas None, mypy va me renvoyer une erreur. C'est à dire que le code suivant sera rejeté par mypy :

state = Gamestate(...)
print(state.atout.name)

Par contre, le code suivant sera accepté par mypy :

state = Gamestate(...)
assert(state.atout is not None)
print(state.atout.name)

Utiliser mypy me force donc à écrire des bons assert si jamais je les oublie ;) Pour ceux qui ne connaissent pas, assert est une fonction très utilisée en Python qui va renvoyer une erreur si la condition n'est pas vraie. Ainsi, si il y a un bug (par exemple, state.atout qui vaudrait None), je m'en apercevrai dès le assert, au lieu d'avoir une erreur qui se propage dans le reste du code.

Le code suivant sera aussi accepté par mypy, parce que on traite les deux cas :

state = Gamestate(...)
print((state.about.name if state.about is not None else "None"))

Un autre détail est le @property:

  @property
  def iter_players(self) -> List[Player]:
    first_player_index = self.table.players.index(self.first_player)
    return list_rotate(self.table.players, first_player_index)

iter_liste est la liste qui représente l'ordre dans lequel les joueurs vont intéragir (ici, piocher). En effet, je voulais une façon pas trop moche d'exprimer : tel joueur commence en premier, et on fait un tour de table. J'ai donc implémenté une fonction list_rotate qui va me rotater la liste que j'indique, ce qui est exactement ce que je veux : la liste des joueurs en partant de celui qui commence.

Par exemple, avec une liste d'entiers, list_rotate([1, 2, 3, 4], 2) me renverra [3, 4, 1, 2].

Le @property permet à Python d'indiquer : "traite iter_players comme si c'était un membre, mais c'est en fait une fonction". C'est à dire que je vais pouvoir utiliser table.iter_players comme si c'était une liste - et en interne, ça va appeler la fonction iter_players() qui renvoie une liste.

L'alternative aurait été d'en faire une méthode - ou alors d'avoir une variable iter_liste que je n'oublie pas de modifier dès que first_player est modifié - mais voilà, "ne pas oublier", c'est un bien grand mot quand on parle de moi. Et ça peut donner des bugs assez difficiles à dénicher - je préfère donc utiliser un @property qui me permet de modifier first_player sans me soucier des autres variables.

Une autre alternative serait de mettre un setter sur first_player (c'est à dire une fonction appelée automatiquement dès que first_player est assigné), qui est sûrement plus efficicace au lieu de recalculer à chaque fois le iter_players. A creuser.

Ensuite, dans game.py, j'ai une petite fonction qui me remplit mon deck :

def init_deck() -> Deck:
  L : List[Card] = []
  for color in Color:
    for rank in Rank:
      L.append(Card(color, rank))
  return Deck(L)

Rien de bien extraordinaire ici, à part peut-être l'annotation "-> Deck" qui indique que je retourne un Deck.

Puis, j'ai un gros commentaire qui me sert à m'indiquer les différentes étapes que je prends pour faire mon programme :

## PLAN
#
# Common ground: the deck is initialized and randomized
# 
# Situation A:
#   1) Each player draws 5 cards from the deck.
#
# Situation B:
#   1) A player is designed as "first player", and players draw 3+2 cards from the deck
#       in that order
#
# Situation C:
#   1) Situation B
#   2) The top card of the deck is revealed and put faceup
#
# Situation D:
#   1) Situation C
#   2) The first player accepts the card, and gets 2 cards - then the others get 3 cards
#
# --> Situation E: <--
#   1) Situation C
#   2) The first player may accept the card, same for the rest.
#       It goes clockwise until someone accepts
#
# Situation F:
#   1) Situation C
#   2) The first player may accept for the card, same for the rest. If the end is reached,
#       the deck is cut and we start over.
##

Ecrire direct un programme qui fait une belote 100% fonctionnelle serait suicidaire. J'ai donc découpé ça en plusieurs situations - et encore, là, j'en suis qu'à l'étape initiale qui est de piocher les cartes et de définir l'atout ! J'ai commencé tout doucement, avec un programme qui d'abord fait piocher 5 cartes à tout le monde. Puis j'ai découpé en 3+2 cartes, à partir d'un "first_player" choisi aléatoirement. Et etc.. Jusqu'à arriver à la situation E à laquelle je me suis arrêtée pour l'instant : on distribue 3 cartes, puis 2 cartes, puis on choisit qui prend l'atout. A tour de rôle, chaque joueur va aléatoirement dire "oui" ou "non" - si on ne trouve aucun preneur, on refait un tour de table jusqu'à ce qu'on en trouve un.

Et donc, plongeons nous dans cette situation, à commencer par l'initialisation :

def game_loop() -> None:
  deck = init_deck()
  deck.shuffle()
  table = Table([Player("South"), Player("West"), Player("North"), Player("East")], deck)
  state = Gamestate(table, random.choice(table.players))

  print("Initial state:\n", state)
  print(80*"-")

J'initialise mon deck, je le mélange, je créé l'objet Table avec mes 4 joueurs que je nomme "South", "West", "North" et "East" et le deck que je vient de mélanger.

Puis j'initialise mon état de jeu, avec un premier joueur choisi aléatoirement. Et je print tout ça. Printer l'état initial me permettra par la suite de vérifier si tout s'est bien passé comme prévu. A noter que ce print fait ce qu'il faut parce que j'ai implémenté les méthodes __str__ de toutes les classes que j'utilise ;)

On entame la première phase de pioche :

  # Drawing phase
  for i in range(2): # Two draw turns
    for player in state.iter_players:
      n_card = 3 if i == 0 else 2
      for j in range(n_card):
        player.draw(deck)

  # Revealing top card of the deck
  table.deck.reveal()

On pioche 3 cartes, puis 2 cartes. Puis, je révèle la première carte du deck. En interne, dans la classe Deck, ça va affecter la variable deck.topcard

Reste alors le choix de l'atout :

  # Loop until someone takes the first card
  assert table.deck.topcard is not None
  while not state.preneur:
    for player in state.iter_players:
      if maybe():
        state.atout = table.deck.topcard.color
        state.preneur = player
        player.draw(deck)
        break

Dans ce code, je vais devoir assigner la couleur de l'atout à partir du deck.topcard : il faut donc qu'il existe ! D'où mon assert. Sinon, je prends le risque en cas de bug d'obtenir une erreur "Nonetype has no member named color" parce qu'il essaierait de faire "None.color".

La fonction maybe() est une fonction qui a 50% de chance de renvoyer True :

def maybe() -> bool:
  return random.choice([True, False])

Je pourrai raffiner ça plus tard :)

Enfin :

  # Final distribution
  for player in state.iter_players:
    n_cards = 2 if player is state.preneur else 3
    for j in range(n_cards):
      player.draw(deck)

  print("Final state: ", state)
  print(80*"-")

Je distribue les dernières cartes (2 à celui qui "a pris", 3 aux autres). Puis je print l'état final pour vérifier que tout s'est bien passé ! Voilà ce que ça donne :

Initial state:
 { table: { players: [{ name: South, 
hand: []}, { name: West, 
hand: []}, { name: North, 
hand: []}, { name: East, 
hand: []}], 
trick: [], 
deck: { 
cards: [(Color.CLUBS, Rank.NINE), (Color.CLUBS, Rank.KING), (Color.SPADES, Rank.JACK), (Color.HEARTS, Rank.TEN), (Color.HEARTS, Rank.NINE), (Color.CLUBS, Rank.JACK), (Color.CLUBS, Rank.ACE), (Color.DIAMONDS, Rank.EIGHT), (Color.DIAMONDS, Rank.QUEEN), (Color.SPADES, Rank.EIGHT), (Color.HEARTS, Rank.SEVEN), (Color.HEARTS, Rank.JACK), (Color.SPADES, Rank.NINE), (Color.HEARTS, Rank.KING), (Color.CLUBS, Rank.EIGHT), (Color.HEARTS, Rank.QUEEN), (Color.CLUBS, Rank.TEN), (Color.SPADES, Rank.SEVEN), (Color.CLUBS, Rank.QUEEN), (Color.HEARTS, Rank.EIGHT), (Color.DIAMONDS, Rank.NINE), (Color.SPADES, Rank.QUEEN), (Color.DIAMONDS, Rank.SEVEN), (Color.DIAMONDS, Rank.TEN), (Color.DIAMONDS, Rank.KING), (Color.HEARTS, Rank.ACE), (Color.DIAMONDS, Rank.ACE), (Color.CLUBS, Rank.SEVEN), (Color.DIAMONDS, Rank.JACK), (Color.SPADES, Rank.TEN), (Color.SPADES, Rank.KING), (Color.SPADES, Rank.ACE)], 
topcard: None}}, 
first player: North, 
atout: None, 
preneur: None}
--------------------------------------------------------------------------------
Final state:  { table: { players: [{ name: South, 
hand: [(Color.HEARTS, Rank.ACE), (Color.DIAMONDS, Rank.KING), (Color.DIAMONDS, Rank.TEN), (Color.HEARTS, Rank.QUEEN), (Color.CLUBS, Rank.EIGHT), (Color.HEARTS, Rank.JACK), (Color.HEARTS, Rank.NINE), (Color.HEARTS, Rank.TEN)]}, { name: West, 
hand: [(Color.DIAMONDS, Rank.SEVEN), (Color.SPADES, Rank.QUEEN), (Color.DIAMONDS, Rank.NINE), (Color.HEARTS, Rank.KING), (Color.SPADES, Rank.NINE), (Color.SPADES, Rank.JACK), (Color.CLUBS, Rank.KING), (Color.CLUBS, Rank.NINE)]}, { name: North, 
hand: [(Color.SPADES, Rank.ACE), (Color.SPADES, Rank.KING), (Color.SPADES, Rank.TEN), (Color.HEARTS, Rank.EIGHT), (Color.CLUBS, Rank.QUEEN), (Color.HEARTS, Rank.SEVEN), (Color.SPADES, Rank.EIGHT), (Color.DIAMONDS, Rank.QUEEN)]}, { name: East, 
hand: [(Color.DIAMONDS, Rank.JACK), (Color.CLUBS, Rank.SEVEN), (Color.DIAMONDS, Rank.ACE), (Color.SPADES, Rank.SEVEN), (Color.CLUBS, Rank.TEN), (Color.DIAMONDS, Rank.EIGHT), (Color.CLUBS, Rank.ACE), (Color.CLUBS, Rank.JACK)]}], 
trick: [], 
deck: { 
cards: [], 
topcard: None}}, 
first player: North, 
atout: Color.HEARTS, 
preneur: South}
--------------------------------------------------------------------------------

Comment vérifier que tout a l'air de bien s'être passé ?

Déjà, je peux vérifier que mon deck, à la fin, ne contient aucune carte..

deck: { 
cards: [], 

ça a l'air bon ! :)

Je peux aussi vérifier que la 6ème carte du preneur correspond bien à la couleur de l'atout..

Final state:  { table: { players: [{ name: South, 
hand: [(Color.HEARTS, Rank.ACE), (Color.DIAMONDS, Rank.KING), (Color.DIAMONDS, Rank.TEN), (Color.HEARTS, Rank.QUEEN), (Color.CLUBS, Rank.EIGHT), (Color.HEARTS, Rank.JACK), (Color.HEARTS, Rank.NINE), (Color.HEARTS, Rank.TEN)]}, { name: West, 
/* ... */
atout: Color.HEARTS, 
preneur: South}

La 6ème carte est le Valet de Coeur. ça tombe bien, c'est la couleur de l'atout !

On peut aussi vérifier (mais c'est plus fastidieux) que chaque personne a bien pioché les bonnes cartes à partir du deck de départ.

Et voilà.. J'ai donc une petite base pour commencer, qui gère la pioche à peu près bien. ça m'aura pris un peu de temps de tout bien écrire avec les types, le découpage en classe etc.., mais le temps que j'ai perdu en écriture, je l'économise largement sur le débogage. J'ai passé à peu près 5 minutes cumulées de débogage - et c'était parce que j'avais oublié qu'un jeu de belote c'était 32 cartes et non 52 (le deck n'était pas vide à la fin, il restait des cartes).

Si vous m'avez lu jusque là, j'espère vous avoir convaincu des bienfaits de mypy, et peut-être que ça vous donne un exemple de comment on peut structurer les choses en Python, tout en ayant un code relativement facile à lire, à maintenir et à déboguer.

De mon côté, je vais continuer tranquillement ce projet.. si il y en a que ça intéresse, je posterai de temps en temps des mises à jour avec les points un peu techniques que j'ai pu aborder.

Les prochaines grandes étapes de mon projet :
  • 1) Coder système de pli, et déroulement de la partie : notamment, des règles indiquant ce qui est autorisé ou non (et l'IA choisira aléatoirement une carte de sa main pour jouer) ; puis décompte des points ; + possibilité de présence d'un joueur humain. Cela demandera sûrement d'élaborer la classe Joueur afin qu'elle puisse prendre des décisions - soit par IA, soit par input du clavier
  • 2) Coder une petite interface graphique avec Tkinter
  • 3) Elaborer l'IA du jeu pour qu'elle soit solide
Merci à tous de votre attention :) Bien entendu si vous avez des suggestions d'amélioration de mon code, n'hésitez pas - je suis un éternel débutant ;)






-
Edité par potterman28wxcv 13 mai 2019 à 0:02:12

  • Partager sur Facebook
  • Partager sur Twitter
6 octobre 2023 à 16:45:45

Hello,

J'ai moi aussi pour projet de créer une belote avec interface visuelle sur Python. Ton code est un super point de départ !! :)

Je ne vois aucun update donc je me demandais si au final tu as réussi à tout coder ou pas. En tout cas si tu as fait quelques avancées, je suis preneuse !

Merci d'avance pour ta réponse ;)

Juliette

  • Partager sur Facebook
  • Partager sur Twitter
7 octobre 2023 à 10:46:22

@JulietteTudoce,

Beau déterrage !

À mon sens dans ses avancées possibles, il y avait principalement la mise en place d'une interface graphique. En visualisant son code source, je ne vois rien qui indique une quelconque interface, donc j'en conclue que non !

  • Partager sur Facebook
  • Partager sur Twitter

Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

19 février 2024 à 20:43:31

Bonjour,

C'est un bon début. Vous en êtes où actuellement?

  • Partager sur Facebook
  • Partager sur Twitter
19 février 2024 à 21:07:44

YannickWEIBEL a écrit:

Bonjour,

C'est un bon début. Vous en êtes où actuellement?

Bonjour, merci de consulter le github du projet, vous y verrez que le dernier commit date d'il y à 5 ans.

Merci de ne pas déterrer d'ancien sujet.

Déterrage

Citation des règles générales du forum :

Avant de poster un message, vérifiez la date du sujet dans lequel vous comptiez intervenir.

Si le dernier message sur le sujet date de plus de deux mois, mieux vaut ne pas répondre.
En effet, le déterrage d'un sujet nuit au bon fonctionnement du forum, et l'informatique pouvant grandement changer en quelques mois il n'est donc que rarement pertinent de déterrer un vieux sujet.

Au lieu de déterrer un sujet il est préférable :

  • soit de contacter directement le membre voulu par messagerie privée en cliquant sur son pseudonyme pour accéder à sa page profil, puis sur le lien "Ecrire un message"
  • soit de créer un nouveau sujet décrivant votre propre contexte
  • ne pas répondre à un déterrage et le signaler à la modération

Liens conseillés

Je ferme ici.

  • Partager sur Facebook
  • Partager sur Twitter