Page 1 of 3

Résultats finale individuelle concours IA 1000 Bornes Python

Unread postPosted: 17 Jun 2024, 16:04
by critor

Voici enfin venu aujourd'hui le jour trop longtemps attendu des résultats de la finale individuelle concours d'intelligence artificielle Python 1000 Bornes.

Je présente toutes mes excuses pour tous ces mois de retard, découlant d'un incident de santé dans la famille début mars.
J'en profite pour remercier les candidats pour leur patience admirable, ainsi que ceux qui ont mis la main à la pâte pour faire avancer la gestion de l'événement alors que j'étais indisponible, notamment Afyu et également cent20.


Nos remerciements une fois encore tous ceux qui ont rendu possible cet événement auquel nous pensions déjà depuis plusieurs années et qui nous tenait tant à cœur :



Sommaire :





1) Les finalistes et la finale

Go to top

Sur les 42 participants et participantes cette année, nous avions retenu invité les 3 premiers au classement à participer à une finale pour un lot supplémentaire :



2) La finale

Go to top

La finale en question était l'occasion de faire évoluer le code de son IA si cela n'avait pas été anticipé, car le format des combats changeait exprès.

Etaient prévus ici non plus des duels, mais un truel entre les IAs de nos 3 champions.

Les IAs gérant et optimisées pour ce format devaient nous être téléversées jusqu'au dimanche 11 mars 2024 23h59 GMT+1.



3) Les IAs

Go to top

Tous nos champions ont bien téléversé une ou plusieurs IAs avant la date en question. Sauf indication contraire de leur part (il y avait la possibilité de cocher l'IA à prendre en compte), c'est l'IA soumise en dernier qui va participer au truel.

  • SlyVTT va combattre avec son IA SLYnapse_finale, après nous avoir envoyé 4 autres tentatives d'IA.
    Lorsque qu'elle gagne, son IA s'esclame : "Je suis SLYnapse-11.0 ... Moi en mode Truelle, je Maçonne :D"
  • Afyu va quant à lui se battre avec son IA Triteleia.
    En cas de victoire, son IA s'écrie : "Et BIM, c'est qui le meilleur ?"
  • Yaya.Cout pour sa part va leur opposer son IA 42_2-5, après nous avoir soumis 1 autre tentative d'IA.
    Lorsqu'elle remporte le truel, son IA sait faire preuve de modestie : "I don't have the driving license, btw"



4) Modalités d'évaluation

Go to top

Afin d'éliminer tout facteur aléatoire, nous avons fait s'affronter ces 3 IAs lors d'une grande série de truels, très exactement 388'888 truels.

Rappelons le 1000 Bornes est un jeu se jouant à tour de rôle. Pour 3 joueurs, pourront alors jouer dans l'ordre : joueur1, joueur2, joueur3, joueur1, joueur2, joueur3, joueur1, …
Selon la façon dont ont été codées les différentes IAs, certaines positions pourraient être avantagées ou désavantagées (si par exemple dans le cas où plusieurs adversaires sont vulnérables, une IA attaque toujours la position de plus bas numéro).
Pour n'avantager et de désavantager personne, nous changeons l'ordre des adversaires à chaque nouveau truel.

Voici le détail des truels joués par chaque IA dans les différentes positions possibles :



Joueur 1
Joueur 2
Joueur 3
SlyVTT
129'628
129'630
129'630
Afyu
129'628
129'630
129'630
Yaya.Cout
129'632
129'628
129'628




5) Vainqueurs et derniers

Go to top

Intéressons nous maintenant au classement des IAs, en terme de scores, pour chaque truel.

Voici toutes les fois où chaque IA est dernière d'un truel (pire des 3 scores) en fonction des différentes positions :



Joueur 1
Joueur 2
Joueur 3
TOTAL
SlyVTT
42'091
(32,47%)
48'295
(37,26%)
73'286
(56,53%)
163'672
(42,09%)
Afyu
23'829
(18,38%)
23'383
(18,04%)
8'258
(6,37%)
55'470
(14,26%)
Yaya.Cout
61'701
(47,60%)
64'643
(49,87%)
45'424
(35,04%)
171'768
(44,17%)


Voici maintenant toutes les fois où chaque IA est victorieuse d'un truel (meilleur des 3 scores) en fonction des différentes positions :



Joueur 1
Joueur 2
Joueur 3
TOTAL
SlyVTT
27'504
(21,22%)
24'799
(19,13%)
13'018
(10,04%)
65'321
(16,80%)
Afyu
68'311
(52,70%)
69'746
(53,80%)
94'721
(73,07%)
112'904
(59,77%)
Yaya.Cout
24'651
(19,02%)
24'187
(18,66%)
43'232
(33,35%)
92'070
(23,68%)

On peut effectivement remarquer de grosses disparités en fonction des positions, Afyu bondissant par exemple de 52-53% à 73% de victoires si son IA joue en position 3 !
Mais la question n'est pas de savoir ce qu'il fait de spécial dans cette configuration (probablement rien vu le cadre imposé par le script réalisant les truels), mais plutôt de savoir ce que les autres IAs ou plutôt l'une d'entre elles ne font pas ou font moins bien dans cette configuration (comme du code non entièrement adapté à ce nouveau format et ne tenant pas compte du joueur3 pour certaines actions : qui est le plus proche de la ligne d'arrivée, qui attaquer, etc.). Si dans ce cas on oublie de tenir compte pour certaines décisions de ce qui semble être l'IA la plus dangereuse, effectivement cela explose son taux de victoires…



6) Scores

Go to top

Si le vainqueur de la finale semble certes évident au vu des résultats précédents, reste encore à départager les deux autres qui sont très proches.

Pour cela rappelons que l'objectif n'était pas simplement de gagner, mais de gagner en réalisant le meilleur score possible.

Voici donc les scores cumulés (victoires et défaites confondues) lors des truels, en fonction ici encore des différentes positions de jeu :



Joueur 1
Joueur 2
Joueur 3
TOTAL
SlyVTT
1'020'631'950
1'010'858'425
936'257'475
2'967'747'850
Afyu
1'188'828'550
1'190'191'275
1'268'778'075
3'647'797'900
Yaya.Cout
965'444'275
942'549'825
1'079'499'375
2'987'493'475

Décidément, ici encore les deux IAs concernées sont extrêmement proches.



7) Résultats

Go to top

Pour départager nos 3 candidats, nous faisons appel à notre version modifiée de l'algorithme de la méthode Elo déjà utilisée pour le classement individuel format duels, classant les IAs justement non pas en fonction du résultat binaire victoire/défaite de chaque truel, mais en fonction des scrores atteints en fin de partie.

Il est temps de proclamer les résultats :
  • 3e avec 174,061 points : Yaya.Cout
  • 2e avec 176,714 points : SlyVTT
  • 1er avec 212,213 points : Afyu
Un énorme merci pour votre patience infinie et un grand bravo à tous, vous allez maintenant pouvoir compléter vos choix de lots avant expédition !


Les résultats des truels peuvent être téléchargés et sont accompagnés du script de classement.

Voici la sortie du script d'évaluation :
Code: Select all
>python3 ./finale_truel_elo.py findiv1.json findiv2.json findiv5.json findiv10.json findiv20.json findiv50.json findiv100.json findiv200.json findiv500.json findiv1000.json findiv2000.json findiv5000.json findiv10000.json findiv20000.json findiv50000.json findiv100000.json findiv200000.json
Traitement des 388888 truels...
[====================================================================================================] 100%
         TOUR1            TOUR2            TOUR3

064814x  SlyVTT           Afyu             Yaya.Cout
1er      016036 (24.74)%  026928 (41.55)%  022118 (34.13)%
dernier  024964 (38.52)%  017719 (27.34)%  022532 (34.76)%
scores   524855550        566602300        543311050

064814x  SlyVTT           Yaya.Cout        Afyu
1er      011468 (17.69)%  003970 (6.13)%  049518 (76.40)%
dernier  017127 (26.42)%  045103 (69.59)%  002872 (4.43)%
scores   495776400        403851500        640704400

064814x  Afyu             SlyVTT           Yaya.Cout
1er      030566 (47.16)%  013371 (20.63)%  021114 (32.58)%
dernier  014846 (22.91)%  027421 (42.31)%  022892 (35.32)%
scores   580178600        509152675        536188325

064814x  Afyu             Yaya.Cout        SlyVTT
1er      037745 (58.24)%  020217 (31.19)%  007068 (10.91)%
dernier  008983 (13.86)%  019540 (30.15)%  036614 (56.49)%
scores   608649950        538698325        473423275

064816x  Yaya.Cout        SlyVTT           Afyu
1er      008416 (12.98)%  011428 (17.63)%  045203 (69.74)%
dernier  038858 (59.95)%  020874 (32.21)%  005386 (8.31)%
scores   449862800        501705750        628073675

064816x  Yaya.Cout        Afyu             SlyVTT
1er      016235 (25.05)%  042818 (66.06)%  005950 (9.18)%
dernier  022843 (35.24)%  005664 (8.74)%  036672 (56.58)%
scores   515581475        623588975        462834200

-------------------------------------------------------------------------------------------

NOM       CRITERE     TOTAL           = TOUR1           + TOUR2           + TOUR3

SlyVTT    truels    : 388888          = 129628          + 129630          + 129630
Afyu      truels    : 388888          = 129628          + 129630          + 129630
Yaya.Cout truels    : 388888          = 129632          + 129628          + 129628

SlyVTT    1er       : 065321 (16.80%) = 027504 (21.22%) + 024799 (19.13%) + 013018 (10.04%)
Afyu      1er       : 232778 (59.86%) = 068311 (52.70%) + 069746 (53.80%) + 094721 (73.07%)
Yaya.Cout 1er       : 092070 (23.68%) = 024651 (19.02%) + 024187 (18.66%) + 043232 (33.35%)

SlyVTT    dernier   : 163672 (42.09%) = 042091 (32.47%) + 048295 (37.26%) + 073286 (56.53%)
Afyu      dernier   : 055470 (14.26%) = 023829 (18.38%) + 023383 (18.04%) + 008258 (6.37%)
Yaya.Cout dernier   : 171768 (44.17%) = 061701 (47.60%) + 064643 (49.87%) + 045424 (35.04%)

SlyVTT    scores    : 2967747850      = 1020631950      + 1010858425      +  936257475
Afyu      scores    : 3647797900      = 1188828550      + 1190191275      + 1268778075
Yaya.Cout scores    : 2987493475      =  965444275      +  942549825      + 1079499375
-------------------------------------------------------------------------------------------

RANG  NOM             GROUP              IA                   ELO     FREQUENCE
1     Afyu            br.AI.n all.IA.ge  Triteleia            212.213  100.00% top1
2     SlyVTT          br.AI.n all.IA.ge  SLYnapse_finale      176.714  99.93% top2
3     Yaya.Cout                          42_2-5               174.061  99.93% top3

Re: Résultats finale individuelle concours IA 1000 Bornes Py

Unread postPosted: 18 Jun 2024, 08:27
by SlyVTT
Bravo à Afyu et à Yaya.Cout, c'est un joli combat.

Afyu nous a mis une déculottée :troll: super prestation de sa part.
Je suis très content que mon IA se soit bien sortie du combat, quoique un peu surpris in fine de passer devant Yaya.Cout.
Je savais pour avoir testé via le site web que nos IA avait un niveau à peu près équivalent car d'un sur l'autre, j'avais grosso modo du 50/50 de victoire.
Ca s'est donc joué dans un mouchoir de poche.

Critor, je ne sais pas si tu as vu les messages des diverses IA en cas de victoire, ce serait peut être intéressant de les mettre comme pour la première phase du tournoi.

Merci beaucoup à tous encore une fois pour ce tournoi fort sympathique. J'ai hâte de lire la démarche suivie par Afyu dans son IA pour nous atomiser, et ensuite voir le tournoi par équipe.

@+

Re: Résultats finale individuelle concours IA 1000 Bornes Py

Unread postPosted: 19 Jun 2024, 10:08
by critor
Merci @SlyVTT pour ton retour et ta suggestion.
SlyVTT wrote:Critor, je ne sais pas si tu as vu les messages des diverses IA en cas de victoire, ce serait peut être intéressant de les mettre comme pour la première phase du tournoi.

Justement c'est rajouté, mais je crois que les messages ne changent pas ou très peu par rapport à la première phase.

Re: Résultats finale individuelle concours IA 1000 Bornes Py

Unread postPosted: 19 Jun 2024, 22:19
by Afyu
Bonjour à toutes et à tous !

Je vais tenter d'expliquer ma démarche et de présenter l'arbre de décision de mon IA.

D'abord, j'ai lu l'annonce du concours, un peu en diagonale, j'avoue, parce que c'était un article vraiment très complet et un peu long à lire.

Je suis ensuite aller chercher un moyen de tester le script pour pouvoir faire quelques parties, à la main, et ainsi me remettre en tête les règles du jeu.
J'ai opté pour la version Workshop Upsilon et le projet suivant dont le lien était donné dans l'article.

Après avoir fait quelques parties, j'ai tenté quelques petites améliorations de l'IA, directement dans le fichier IA_random.py du projet précédemment cité. L'inconvénient est que ça ne donne pas immédiatement un score pour savoir si les améliorations apportées sont pertinentes ou non.

J'ai alors téléchargé la version "ordi" des scripts de participation donnée dans l'article de présentation pour pouvoir lancer des parties avec mon IA contre l'IA random. La commande pour lancer une telle partie est python kb.py ia_perso ia_random et après quelques secondes d'affichage de la partie, on obtient le nom du gagnant ainsi que son score.

Pour ne pas avoir à répéter plusieurs (dizaines de) parties avant d'avoir une idée du score moyen obtenu par chaque IA, j'ai modifié le nombre de bornes d'une partie dans le fichier kblibprv.py à la ligne 747 en remplaçant la valeur 9000 par 900 000 ou même 9 000 000.
J'ai pris soin de tester des parties en appelant ia_perso en premier puis en appelant ia_perso en deuxième, pour garder une certaine équité dans ces affrontements.
Pour vérifier l'efficacité de chaque nouvelle amélioration de mon IA, j'ai gardé différents fichiers et j'ai lancé des affrontements ia_perso_18 contre ia_perso_19 pour voir si la nouvelle IA faisait un meilleur score que la précédente.

Pour gagner un peu (beaucoup) de temps dans le déroulement des parties (surtout pour des parties à 9 000 000 de points !), j'ai modifié le fichier kblibprv.py pour en retirer tout ce qui est graphique. J'ai réussi à fortement alléger et gagner du temps d'exécution mais je n'ai pas réussi à me débarrasser de l'ouverture de la fenêtre SDL, alors je suis finalement aller chercher le fichier kblibprv.py de la version NumWorks Upsilon et là j'ai réussi à supprimer tout ce qui est graphique.

Ce fichier, dans sa version allégée, est devenu :
Code: Select all
# les scripts d'IA ne doivent en aucun cas acceder au contenu de ce script

from kblibpub import *
from random import choice, randint, seed


#-----------------
# classe jeujoueur
#-----------------
class c_jeujoueur(c_infos_jeu_et_main):
  def nouvelle_manche(self, nbr_cartes, taille_main, bornes_arrivee):
    self.main_affichable = [None] * taille_main
    super().nouvelle_manche(nbr_cartes, taille_main, bornes_arrivee)

  def main_append(self, carte):
    for k in range(TAILLE_MAIN):
      if self.main_affichable[k] == None:
        self.main_affichable[k] = carte
        return k

  def main_remove(self, carte):
    for pos in range(TAILLE_MAIN):
      if self.main_affichable[pos] == carte:
        self.main_affichable[pos] = None
        return pos

  def traite_coup(self, coup, redraw=0, posaff=-1):
    points_old = self.points
    cf = len(self.coups_fourres_autorises) and coup.destination == self.num_joueur and coup.origine != SABOT \
      and coup.carte in self.coups_fourres_autorises
    super().traite_coup(coup)
    if coup.origine == SABOT and coup.destination == self.num_joueur:
      pos = self.main_append(coup.carte)
    elif coup.origine == self.num_joueur:
      pos = self.main_remove(coup.carte)
      if posaff >= 0 and pos != posaff and self.main_affichable[posaff] == coup.carte:
        self.main_affichable[posaff], self.main_affichable[pos] = self.main_affichable[pos], self.main_affichable[posaff]
        pos = posaff
      #if redraw and self.num_joueur == show_main_joueur: redraw_main(None, pos, self.num_joueur)
    #if coup.destination == SABOT: draw_carte_defausse(coup.carte)
    #if coup.destination == self.num_joueur and coup.origine != SABOT:


#-------------------------
# fonctions pour la pioche
#-------------------------
def melange_liste(lst):
  n = len(lst)
  for i in range(n - 1):
    j = randint(i, n - 1)
    lst[i], lst[j] = lst[j], lst[i]

def permute_liste(lst, d):
  n = len(lst)
  for istart in range(0, n, d):
    iend = min(istart + d, n)
    lst[istart:iend] = [lst[iend - 1]] + lst[istart:iend - 1]

#----------------------
# fonctions pour les IA
#----------------------
def traite_coup_all(coup, num_joueur, joueurs, infos_joueurs, pos = -1):
  for i in range(nbr_joueurs):
    # censure de la carte tiree pour les autres IAs
    tcoup = coup.origine == SABOT and coup.destination != i and c_coup(coup.origine, None, coup.destination) or coup
    joueurs[i].traite_coup(tcoup)
    infos_joueurs[i].traite_coup(tcoup, 1, pos)

#--------------------
# fonction principale
#--------------------
def kb_partie(classes_joueurs, show_help=0):
  #global col_w, y1, n_cartes_lgn, n_cartes_col, carte_w, carte_h, cartebox_w, cartebox_h, i_display_mode, ysep, xsep
  #global joueurs_col, show_main_joueur, pioche_len
  global main_xm, jeu_xm, pioche_xm, TAILLE_MAIN, nbr_joueurs
  TAILLE_MAIN = 7
  POINTS_OBJECTIF = 9000*100
  sep1, sep2 = "=" * 21, "-" * 20
  #if show_help:
  #  draw_help()
  nbr_joueurs = len(classes_joueurs)
  show_main_joueur = 0


  joueurs = []
  for num_joueur in range(nbr_joueurs):
    c_joueur = classes_joueurs[num_joueur]
    joueurs.append(c_joueur(num_joueur, nbr_joueurs, POINTS_OBJECTIF))

  infos_joueurs = [c_jeujoueur(num_joueur, nbr_joueurs, POINTS_OBJECTIF, 1) for num_joueur in range(nbr_joueurs)]
  fin_de_partie = 0
  i_manche = 0

  for joueur in joueurs:
    if joueur.nom_groupe:
      for obj in joueurs + infos_joueurs:
        obj.rejoint_groupe(joueur.num_joueur, joueur.nom_groupe)

  print(sep1)
  print("Debut de partie a {:d}pts".format(POINTS_OBJECTIF))
  print(sep2)
  for joueur in joueurs: print(joueur.id_str())
  print(sep1)


  pioche_r = [I_CAMION_CITERNE, I_VEHICULE_INCREVABLE, I_AS_VOLANT, I_VEHICULE_PRIORITAIRE]
  pioche_r += [I_PANNE_ESSENCE, I_CREVAISON, I_ACCIDENT]*3 + [I_LIMITATION,]*4 + [I_FEU_ROUGE,]*5
  pioche_r += [I_POMPE_ESSENCE, I_ROUE_SECOURS, I_REPARATIONS, I_FIN_LIMITATION]*6 + [I_FEU_VERT,]*14
  pioche_r += [I_25_BORNES, I_50_BORNES, I_75_BORNES]*10 + [I_100_BORNES,]*12 + [I_200_BORNES,]*4

  while not fin_de_partie: # nouvelle partie
    i_manche += 1
    bornes_arrivee_allonge = 1000
    bornes_arrivee = nbr_joueurs <= 3 and 700 or bornes_arrivee_allonge
    allonge_demandee = 0
    melange_liste(pioche_r)

    for i_pioche in range(nbr_joueurs):
      if fin_de_partie: break

      print("Manche {:d} Pioche {:d}".format(i_manche, i_pioche + 1))
      print(sep2)

      permute_liste(pioche_r, nbr_joueurs)
      pioche = list(pioche_r)
      pioche_len = len(pioche)

      for obj in joueurs + infos_joueurs: obj.nouvelle_manche(pioche_len, TAILLE_MAIN, bornes_arrivee)

      #show(infos_joueurs)
      #clear_main(show_main_joueur)

      for k in range(TAILLE_MAIN - 1):
        for num_joueur in range(nbr_joueurs):
          carte = pioche.pop()
          pioche_len -= 1
          coup = c_coup(SABOT, carte, num_joueur)
          traite_coup_all(coup, num_joueur, joueurs, infos_joueurs)

      fin_de_manche = 0
      while pioche_len and not fin_de_manche and not fin_de_partie: # nouvelle manche

        for num_joueur in range(nbr_joueurs):
          if fin_de_manche or fin_de_partie: break
          joueur = joueurs[num_joueur]
          infos_joueur = infos_joueurs[num_joueur]
          joueur_dest = None
          infos_joueur_dest = None

          tour_termine = 0
          tour_commence = 0

          while not tour_termine and (pioche_len or len(infos_joueur.main) >= TAILLE_MAIN):
            useposmain = 0
            if tour_commence and len(infos_joueur.main) < TAILLE_MAIN:
              coup = c_coup(SABOT, None, num_joueur)
            else:
              coup = joueur.decide_coup()
              if coup == None:
                if num_joueur != show_main_joueur:
                  show_main_joueur = num_joueur
                  #draw_main(infos_joueur.main_affichable, num_joueur)
                #PC_wnokey()
                #coup = choix_clavier_carte(infos_joueur.main_affichable, num_joueur, infos_joueurs)
                useposmain = 1
                if coup == None:
                  fin_de_partie = 1
                  break

              tour_commence = 1

            coup_invalide = coup.invalide(nbr_joueurs)

            if not coup_invalide:
              if coup.destination != SABOT:
                joueur_dest = joueurs[coup.destination]
                #ATTAQUES[(joueur.num_joueur,joueur_dest.num_joueur)]+=1
                infos_joueur_dest = infos_joueurs[coup.destination]
              coup_invalide = infos_joueur.coup_invalide(coup)

            if not coup_invalide and joueur_dest:
              coup_invalide = infos_joueur_dest.coup_invalide(coup)


            if coup_invalide:
              print("Coup invalide : ", coup.to_str())
              print(infos_joueur.to_str())
              if coup.destination >= 0 and coup.destination != num_joueur:
                print(infos_joueurs[coup.destination].to_str())
              coup.destination = SABOT
              coup.origine = num_joueur
              if coup.carte == None or not coup.carte in infos_joueur.main:
                coup.carte = choice(infos_joueur.main)
            else:
              if coup.origine == SABOT:
                coup.carte = pioche.pop()
                pioche_len -= 1

            if coup.destination != SABOT:
              joueur_dest = joueurs[coup.destination]
              infos_joueur_dest = infos_joueurs[coup.destination]

            if coup.destination != SABOT and est_carte_botte(coup.carte):
              back_vitesse = list(infos_joueur.pile_vitesse)
              back_bataille = list(infos_joueur.pile_bataille)
            # information toutes IA du dernier coup
            traite_coup_all(coup, num_joueur, joueurs, infos_joueurs, not useposmain and -1 or pos_choix)

            if coup.destination != SABOT and est_carte_botte(coup.carte):
              if len(infos_joueur.pile_vitesse) < len(back_vitesse):
                infos_joueur.traite_coup(c_coup(None, back_vitesse[-1], SABOT))
              if len(infos_joueur.pile_bataille) < len(back_bataille):
                infos_joueur.traite_coup(c_coup(None, back_bataille[-1], SABOT))

            if not allonge_demandee and bornes_arrivee < bornes_arrivee_allonge and joueur_dest \
              and infos_joueur_dest.bornes == bornes_arrivee:
              allonge_demandee = 1
              pari_allonge = joueur_dest.pari_allonge(bornes_arrivee_allonge)

              if pari_allonge == None:
                if num_joueur != show_main_joueur:
                  show_main_joueur = num_joueur
                  #draw_main(infos_joueur.main_affichable, num_joueur)
                #PC_wnokey()
                pari_allonge = choix_allonge(num_joueur, infos_joueur)
                if pari_allonge == None: fin_de_partie = 1
              if pari_allonge:
                bornes_arrivee = bornes_arrivee_allonge
                print(infos_joueur.id_str() + " allonge {:04d}km".format(bornes_arrivee))
                for obj in joueurs + infos_joueurs: obj.allonge(joueur_dest, bornes_arrivee)
              else: break

            fin_de_manche = infos_joueur.victoire_manche()
            tour_termine = coup.origine != SABOT and not est_carte_botte(coup.carte)

          for infos_joueur2 in infos_joueurs:
            if infos_joueur2.victoire_manche():
              fin_de_manche = 1
              break

      #print(sep2)
      #for infos_joueur in infos_joueurs:
        #print(infos_joueur.id_str() + " {:04d}km".format(infos_joueur.bornes) +
          #(infos_joueur.victoire_manche() and " Victoire" or ""))
      #print(sep2)
      for obj in joueurs + infos_joueurs: obj.fin_manche([infos_joueurs[i].bornes for i in range(nbr_joueurs)], pioche_len)
      #print(sep2)

      score_gagnant, n_gagnants = 0, 0
      for infos_joueur in infos_joueurs:
        t = infos_joueur.victoire_tournoi()
        if t and i_pioche + 1 == nbr_joueurs: fin_de_partie = 1
        if infos_joueur.points > score_gagnant:
          score_gagnant = infos_joueur.points
          n_gagnants = 1
        elif infos_joueur.points == score_gagnant: n_gagnants += 1
        print(joueurs[infos_joueur.num_joueur].id_str() + " {:05d}pt".format(infos_joueur.points) +
          " s"[infos_joueur.points > 1] + (t and " It's over {:d}".format(POINTS_OBJECTIF) or ""))
        #print(infos_joueur.id_str() + " {:05d}pt".format(infos_joueur.points) +
        #  " s"[infos_joueur.points > 1] + (t and " It's over {:d}".format(POINTS_OBJECTIF) or ""))
      print(sep1)
  exaequo = n_gagnants > 1
  print("Fin de partie")

#  for joueur in joueurs: print(joueur.id_str())
#  print(sep1)

  for k in range(nbr_joueurs):
    infos_joueur = infos_joueurs[k]
    if infos_joueur.points == score_gagnant:
      print(exaequo and "Ex aequo" or "Gagnant", joueurs[k].id_str())
      #print(exaequo and "Ex aequo" or "Gagnant", infos_joueur.id_str() + joueurs[k].id_str())
      if not exaequo: print('Le mot du gagnant:\n"' + joueurs[k].msg_joueur + '"')

Je n'ai quasiment pas modifié le fichier kblibpub.py si ce n'est pour modifier "R" et "V" du Feu Rouge et du Feu Vert pour les remplacer par "FR" et "FR". En effet, le Feu Rouge et la Réparation partageaient la même lettre "R" et ça ne facilitait pas la lecture du contenu de la main affiché dans le console en cas de coup invalide.

L'essentiel de mon temps passé sur ce concours a servi à améliorer mon IA, en détaillant et étoffant toujours plus l'arbre de décision qui la compose. Il n'y a pas de deep-learning, de réseau de neurones ou d'analyse de la stratégie employée par les adversaires.

Au début de chaque tour, mon IA met à jour plusieurs listes en fonction des cartes piochées ou posées sur le plateau (le contenu de la défausse étant affiché mais inaccessible par l'IA). Ces listes permettent de classer les attaques suivant le nombre de parades correspondantes piochées et les parades suivant le nombre d'attaques correspondantes piochées, ce qui permet de classer les cartes d'attaque par probabilité croissante d'être contrées et les parades par probabilité croissante d'être util(isé)es.

J'utilise très fréquemment la plupart des fonctions pré-existantes et définies dans kblibpub.py qui permettent de déterminer si une carte est une carte de bornes, d'attaque, de parade, de botte ou qui permettent de déterminer la parade ou la botte correspondant à une attaque donnée et réciproquement.

Par ailleurs, la fonction fin_manche() du même fichier donne de précieuses informations sur le calcul des scores (bonus pour victoire par capot ou par épuisement de la pioche, bonus pour ne pas avoir posé de carte de 200 bornes, bonus pour avoir gagné après avoir choisi l'allonge à 1000 bornes...).

Pour le reste, l'arbre de décision de mon IA est une longue succession de conditions if qui n'ont quasiment jamais de else associé. En effet, chaque condition if est suivie d'un return ce qui met immédiatement fin à la lecture et au parcours de la fonction decide_coup() lorsqu'une des conditions est vérifiée.

Je vous joins un petit logigramme qui montre cet arbre de décision.

Image


Les différentes parties pourraient se nommer ainsi :
  • En orange : je joue un éventuel coup fourré
  • En blanc : j'ai bientôt gagné
  • En rouge : j'attaque
  • En vert : je pose des bornes
  • En bleu : je répare ou pose un Feu Vert
  • En violet : je jette une carte

Mais le petit logigramme précédent n'est qu'une version compacte du logigramme complet de mon IA que voici :
Image

Dans le fichier de mon IA, à la ligne 154, il y a un if self.infos_joueur.bornes_arrivee - max(liste_bornes_adversaires) <= 2000: qui n'était pas réglé sur 2000 au départ et qui prévoyait que mon IA lance des attaques uniquement sur les adversaires proches de gagner. Finalement, mon IA fait de bien meilleurs scores en étant tout le temps agressive. En effet, une fois que l'adversaire est en panne, il reste du temps pour poser des bornes.

Par ailleurs, dans le logigramme, on peut voir que mon IA attaque d'abord avec des attaques autres qu'un Feu Rouge, puis avec un Feu Rouge s'il n'y a que ça de disponible. Le Feu Rouge se répare avec un Feu Vert qui permet par la même occasion de se remettre en route, tandis que les autres attaques nécessitent d'être réparées avant de poser un Feu Vert et il faut donc au moins 2 tours et 2 cartes pour se remettre en route. Donc un Feu Rouge est moins contraignant qu'une autre attaque, d'où cette distinction. :)

Il y a une grosse boulette dans ce logigramme (et dans mon IA), sauras-tu la retrouver ? xD

Il y a également un choix peu judicieux à la ligne 301 du fichier de mon IA. En effet, lorsque je ne peux rien jouer et que je suis contraint de jeter une carte, je choisis de jeter la carte d'attaque que j'ai en main dont il est le moins probable de piocher une parade. C'est pourtant bien une attaque qui a peu de chance d'être contrée qui est la plus pertinente de jouer pour attaquer un adversaire ! Il aurait été bien plus pertinent de jeter la carte d'attaque dont la parade a le plus de chance d'être piochée.

Et qu'en est-il de l'allonge ?
Après avoir testé de nombreuses conditions sur les différents paramètres de la partie pour déterminer s'il est judicieux d'allonger à 1000 bornes ou pas, j'ai retenu la condition très restrictive suivante :
J'allonge si : j'ai au moins 500 bornes d'avance sur chacun des adversaires ET il reste au moins 10 cartes dans la pioche ET j'ai exactement 300 bornes dans ma main ET j'ai posé au moins 3 bottes.
En effet, après de nombreux essais j'ai constaté qu'il était très rarement judicieux d'accepter l'allonge à 1000 bornes alors j'ai restreint cette validation d'allonge aux cas très favorables où il est quasiment certain de pouvoir gagner l'allonge. (Mais j'imagine que ces conditions sont très rarement remplies ^^)

Qu'est-ce que j'aurais pu faire de mieux ?

Eh bien, j'aurais peut-être pu ré-adapter la condition sur le fait de poser une carte de 200 bornes ou pas suivant le nombre de bornes posées, l'avance sur l'adversaire et le fait de ne pas en avoir déjà posée, pour avoir le bonus de victoire sans avoir posé de carte de 200 bornes. J'avais mis cette condition dans les premières versions de mon IA mais je l'ai modifiée après m'être rendu compte qu'il était plus judicieux de poser les cartes de 200 bornes qu'on a dans la main, dans la limite imposée de 2 cartes par manche et pas uniquement lorsque l'adversaire est en passe de gagner et que cette carte de 200 permettrait de gagner immédiatement la manche.

Je me suis bien amusé à essayer d'améliorer mon IA, encore et encore, par petits pas. :)
Je remercie immensément les organisateurs de ce concours, qui ont, une fois encore, fait un travail de préparation titanesque et remarquable, allant même cette année jusqu'à l'automatisation de l'évaluation des IA lors d'affrontements entre IA des participants !!! :#tritop#:


Edit du 20/06 : Les deux logigrammes ont été réalisés sur le site draw.io et je vous joins le fichier qui est à ouvrir depuis le site, après avoir remplacé l'extension .txt par .drawio. Il y a alors deux pages, une page par logigramme.

Re: Résultats finale individuelle concours IA 1000 Bornes Py

Unread postPosted: 20 Jun 2024, 00:54
by Afyu
Pour le choix du lot, je souhaiterais :
1 lot Casio Women Do Science Integral : 1 calculatrice Casio Graph 90+E édition limitée Clara Grima + 5 autocollants Women Do Science + 1 cahier Casio Sophie Germain + 1 sac Casio Clara Grima + 1 clé USB d'émulation Casio + les 4 stylos Casio + 1 batterie USB Casio + 1 clé USB Casio + 1 housse Casio FX-CASE + 1 goodie Casio premium : le gobelet + 1 stylo HP + 1 aimantin Xcas + 1 autocollant Xcas + 2 autocollants TI-Planet : un de chaque + 1 autocollant Planète Casio + 1 aimantin TI-Planet : Noël en blanc + 1 autocollant 1000 Bornes commémoratif : l'Escargot de Course
1795317946179431794217949146391298717947179481795217951181741817317928161141322811614116151792117919

J'en profite pour remercier une nouvelle fois toutes les personnes qui ont contribué à la bonne organisation et au bon déroulement de ce concours, ce qui inclut donc l'équipe organisatrice ainsi que l'ensemble des participants ! (Merci également au serveur qui a tenu le coup ! :p )

Merci !! :favorite:

Re: Résultats finale individuelle concours IA 1000 Bornes Py

Unread postPosted: 20 Jun 2024, 12:50
by critor
Merci beaucoup @Afyu.

J'ai préparé ton double lot.
La Graph 90+E a été mise à jour vers la dernière version 3.80.1.
La clé USB d'émulation était d'origine préchargée avec 3 émulateurs d'anciens modèles, je te l'ai complétée avec les 7 émulateurs disponibles à ce jour, tous dans leurs dernières versions.

Est-ce que j'ai oublié quelque chose ?
20102
Edit : la clé USB et la batterie Casio ne sont pas sur la photo, mais ont été rajoutées.

Sinon, @SlyVTT, c'est maintenant à toi de compléter ton lot.

Re: Résultats finale individuelle concours IA 1000 Bornes Py

Unread postPosted: 20 Jun 2024, 12:52
by Afyu
critor wrote:Merci beaucoup @Afyu.

J'ai préparé ton double lot.
La Graph 90+E a été mise à jour vers la dernière version 3.80.1.
La clé USB d'émulation était d'origine préchargée avec 3 émulateurs d'anciens modèles, je te l'ai complétée avec les 7 émulateurs disponibles à ce jour, tous dans leurs dernières versions.

Merci pour tes petites attentions ! :favorite:

critor wrote:Est-ce que j'ai oublié quelque chose ?
20102

J'ai vu un autocollant sauvage sur Clara Grima, mais je n'ai pas vu la clé USB Casio et la batterie USB Casio. Où les as-tu cachées ?

Edit : je viens de voir ton "Edit" :D
Donc c'est complet ! Merci beaucoup !! :favorite:

Re: Résultats finale individuelle concours IA 1000 Bornes Py

Unread postPosted: 20 Jun 2024, 12:57
by critor
Afyu wrote:J'ai vu un autocollant sauvage sur Clara Grima, mais je n'ai pas vu la clé USB Casio et la batterie USB Casio. Où les as-tu cachées ?

Il s'est envolé oui, mais tu y as droit, 2 autocollants Planète Casio puisque 2 lots.
Afyu wrote:Donc c'est complet ! Merci beaucoup !! :favorite:

Tu plaisantes, c'est moi qui te remercie infiniment.

Re: Résultats finale individuelle concours IA 1000 Bornes Py

Unread postPosted: 20 Jun 2024, 13:03
by Afyu
critor wrote:Il s'est envolé oui[...]

Trop de vitesse dans un virage. Ça va finir en Accident et j'ai pas choisi l'autocollant As du Volant. ><

Re: Résultats finale individuelle concours IA 1000 Bornes Py

Unread postPosted: 20 Jun 2024, 20:41
by SlyVTT
Hello à Toutes et Tous,

Je me joins à Afyu pour féliciter encore une fois les organisateurs du concours car cette année nous avons eu une épreuve vraiment passionnante. Le terme concours prenant alors tout son sens, vu que cette année il s'agit bien d'un combat les uns contre les autres, un peu comme des arts martiaux.

Ma démarche de résolution ressemble beaucoup à celle d'Afyu, à la différence que j'avais cette année relativement peu de temps à consacrer au concours à cause du boulot. Je n'ai donc pas forcément chercher à optimiser à outrance mon algorithme ou mes outils comme les années précédentes. En particulier, je me suis bien accommodé de la version PC des scripts que je faisais combattre en tâche de fond pendant que je faisais autre chose en parallèle. Donc hormis quelques petites astuces pour faire sortir des messages en couleur dans la console, partie que j'ai partagé directement avec les autres concurrents, rien de transcendant sous le soleil au niveau du développement des outils.

J'ai commencé par relire les règles du jeux que j'ai trouvé sous forme d'un scan PDF du jeu original (https://www.hasbro.com/common/instruct/MilleBorne(French).pdf), car il faut être honnête, cela faisait quelques dizaines d'années que je n'avais pas refait de partie. Il y avait donc quelques subtilités qui étaient sorties de ma mémoire.

J'ai ensuite fait un certain nombre de partie "à la main" pour vérifier que le programme réagissait selon les règles standards et j'ai lu le code pour vérifier les quelques points pouvant bouger d'une interprétation du jeu à une autre (choix de l'allonge de partie à 700km, présence des coups fourrés, règle du maxi 2 fois la carte 200 bornes, ...).

Une fois le tour du jeu réalisé, je suis reparti de l'IA random de Critor et ai commencé à me faire une bibliothèque de fonctions destinées à me simplifier la vie. Par exemple une fonction test_si_parade_possible(attaque_en_cours) qui regarde si on a la parade à une attaque passée en argument et retourne le coup de parade si on a la parade disponible ou None sinon.

Code: Select all
    def test_si_parade_possible(self, attaque_en_cours):
        for c in self.infos_joueur.main:
            # on check si on peut lancer la parade à l'attaque en cours pour chacune des diverses attaques possibles
            if attaque_en_cours == I_LIMITATION and c == I_FIN_LIMITATION:
                coup = c_coup(self.num_joueur, c, self.num_joueur)
                print_cyan("Parade - limitation terminée")
                return coup
            elif attaque_en_cours == I_FEU_ROUGE and c == I_FEU_VERT:
                coup = c_coup(self.num_joueur, c, self.num_joueur)
                print_cyan("Parade - feu rouge teminé")
                return coup
            elif attaque_en_cours == I_CREVAISON and c == I_ROUE_SECOURS:
                coup = c_coup(self.num_joueur, c, self.num_joueur)
                print_cyan("Parade - crevaison terminée")
                return coup
            elif attaque_en_cours == I_ACCIDENT and c == I_REPARATIONS:
                coup = c_coup(self.num_joueur, c, self.num_joueur)
                print_cyan("Parade - accident terminé")
                return coup
            elif attaque_en_cours == I_PANNE_ESSENCE and c == I_POMPE_ESSENCE:
                coup = c_coup(self.num_joueur, c, self.num_joueur)
                print_cyan("Parade - panne essence terminée")
                return coup
        return None


Mais il pouvait aussi s'agir de fonction permettant de choisir la meilleure carte à jeter si impossibilité de jouer sur ce tour :

Code: Select all
    def decide_meilleure_carte_a_jeter(self):
        coup = None

        # priorité 1 : si on a une carte 200 et on a dejà joué 2 cartes 200
        if I_200_BORNES in self.infos_joueur.main and self.infos_joueur.bornes200 == 2:
            coup = c_coup(self.num_joueur, I_200_BORNES, SABOT)
            return coup

        # priorité 2 : si on a une protection par une botte et une parade associée alors on jette la parade
        if I_FEU_VERT in self.infos_joueur.main and I_VEHICULE_PRIORITAIRE in self.infos_joueur.bottes:
            coup = c_coup(self.num_joueur, I_FEU_VERT, SABOT)
            return coup
        if I_FIN_LIMITATION in self.infos_joueur.main and I_VEHICULE_PRIORITAIRE in self.infos_joueur.bottes:
            coup = c_coup(self.num_joueur, I_FIN_LIMITATION, SABOT)
            return coup
        if I_ROUE_SECOURS in self.infos_joueur.main and I_VEHICULE_INCREVABLE in self.infos_joueur.bottes:
            coup = c_coup(self.num_joueur, I_ROUE_SECOURS, SABOT)
            return coup
        if I_POMPE_ESSENCE in self.infos_joueur.main and I_CAMION_CITERNE in self.infos_joueur.bottes:
            coup = c_coup(self.num_joueur, I_POMPE_ESSENCE, SABOT)
            return coup
        if I_REPARATIONS in self.infos_joueur.main and I_AS_VOLANT in self.infos_joueur.bottes:
            coup = c_coup(self.num_joueur, I_REPARATIONS, SABOT)
            return coup

        # priorité 3 : on check si on a une carte en double dans les trucs pas très utiles
        if self.a_double_en_main(I_FEU_VERT):
            coup = c_coup(self.num_joueur, I_FEU_VERT, SABOT)
            return coup

        if self.a_double_en_main(I_FIN_LIMITATION):
            coup = c_coup(self.num_joueur, I_FIN_LIMITATION, SABOT)
            return coup

        # TODO : à compléter cette liste.

        return coup


Parmi les points qui me sont apparus au cours des diverses parties jouées contre les autres concurrents, les conclusions suivantes ont conduit à mon choix d'algorithme :
- les IA agressives cherchant en priorité à ralentir les adversaires avant de chercher à avancer semblent plus efficaces,
- pour l'allonge, après divers critères testé, il m'est apparu que refuser systématiquement l'allonge m'apportait plus souvent la victoire, je refuse donc tout le temps de continuer
- pour vraiment départager 2 IAs, il faut au moins 20 parties et regarder à la fois sur le nombre de victoires et le delta de points pour vérifier si un changement dans le code est pertinent ou pas
- tout comme Afyu, j'ai aussi utiliser un script qui permettait de faire combattre les IAs jusqu'à 900000 points pour permettre de maximiser le nombre de tours

Parmi les choses que j'aurais aimé faire mais pour lesquelles je n'avais vraiment pas de temps :
- j'aurais voulu implémenter quelque chose de plus complexe qu'un simple arbre de décision basé sur des "if" et des "return" (par exemple réseau de neuronnes)
- un calcul statistique de probabilité d'apparition des cartes
- une forme de mémoire des cartes jouées et de leur ordre par rapport à la partie N-1

Bref, il y avait vraiment moyen de faire quelque chose de vraiment top.

Voici désormais le logigramme de mon IA, nettement plus simple que celui d'Afyu, mais qui finalement ne s'en tire pas trop mal.

Image

D'ailleurs pour l’anecdote, en relisant mon code posément pour faire ce logigramme, j'ai découvert pleins de bugs qui sont restés dans mon code. Par exemple le fait que Afyu ne soit pas attaqué en 3ème position vient d'un oubli de retrait de cette partie :

Code: Select all
        # On attaque l'adversaire si il avance, attitude très aggressive de l'IA
        if self.est_ce_que_adversaire_est_arrete(self.cible1vs1):
            coup = self.attaque_adversaire()
            if coup is not None:
                print_green("Attaque préventive")
                return coup


sachant que self.cible1vs1 vaut 1_self.num_joueur donc ça ne devait pas fonctionner en mode truel :D

J'aurai, je pense, avec un peu plus de temps pu améliorer mon logigramme et surtout faire un truc un peu plus "réfléchi" et de moins incrémental. Néanmoins cela était vraiment très fun de pouvoir concourir et de combattre contre les copains. Tous les ans il y a du nouveau et de l'originalité. Bravo à toute l'équipe. En particulier cette année le concept de combat par internet était vraiment top.