PokerChip Race : Revue de code des gagnants

STAY CONNECTED, FOLLOW CODINGAME NOW

Les gagnants de PokerChip Race, Xylo et Gamoul, reviennent sur leur code et nous expliquent leur stratégies.

NDRL: si certains d'entre vous souhaitent partager leurs revues de code avec la communauté CodinGame, n'hésitez pas poster votre débrief sur le forum.


Xylo (1ère place, C, France) :

 

Pour ce challenge, je me suis forcé à garder un code aussi simple que possible. Je n'avais pas beaucoup de temps à y consacrer, et je ne voulais surtout pas répéter l'erreur que j'avais commise lors de ma participation au challenge précédent, ou je m'étais noyé dans la complexité (toute relative) de mon propre code. Heureusement, Poker Chip Race était selon moi beaucoup moins difficile que Game of Drones dans la mesure ou accélérer un jeton avait un coût. La plupart du temps, les jetons n'accélèrent pas. A partir de ce constat, j'ai choisi de prendre trois hypothèses simplificatrices:

1) Au tour courant, je suppose mes jetons accélèrent comme bon leur semble2) Après, je suppose que mes jetons n'accélèrent plus du tout
3) Je suppose les jetons adverses n'accélèrent jamais, pas même au tour courant

Les hypothèses 2 et 3 peuvent paraître abusives, mais en pratique elles marchent plutôt bien! J'avais essayé à un moment donné de mitiger l'hypothèse 3 en tenant compte de la proximité (actuelle ou future) de mes jetons avec de gros jetons adverses, mais le résultat était moins bon: mon IA hésitait souvent à capturer de petits jetons ou gouttelettes, apeurée par la proximité de gros jetons, alors que dans la majorité des cas elle aurait pu se le permettre. Au final, j'ai gardé mes hypothèses 2 et 3 telles quelles, quitte à ce que le comportement de mon IA apparaisse comme téméraire par moments.

Une fois arrivé là, le reste vient naturellement: il suffit de prendre un ensemble de stratégies dictant les actions de mes jetons au tour courant, et de les évaluer les unes après les autres en simulant ce qui va se passer sur plusieurs tours si mes hypothèses restent valides. À chacune de ces simulations j'attribue un score en fonction de la masse gagnée ou perdue par mes jetons. Puis je retiens la stratégie qui a obtenu le meilleur score. Facile, non? Néanmoins, l'implémentation de cette méthode demande un peu de rigueur pour éviter certains pièges...



Simulation

Une première étape dans l'implémentation est de simuler le déroulement de la partie sur plusieurs tours. C'est un peu plus difficile qu'il y paraît au premier abord, étant donné que l'ensemble des règles à implémenter est un peu plus compliqué que dans Game of Drones ou que dans Tron Battle. Fort heureusement, le thread "PokerChipRace physics explanations" du forum contenait quasiment toutes les explications nécessaires, et il suffisait de poster une question pour obtenir une réponse rapide et précise de la part de Loïc. Je pense que je n'aurais pas pu mettre en place ma stratégie sans toutes ces informations. Donc merci beaucoup, Loïc!

Au niveau architecture, j'ai défini une structure appelée state_t qui contient l'état du jeu à un instant donné, et une fonction state_update mettant à jour cet état en simulant le déroulement du jeu sur un tour. Suite à un message de Manwe, Loïc avait clarifié que côté serveur, chaque tour contenait une boucle de simulation de 500 pas, avec des tests de collision à chaque pas. J'ai scrupuleusement réimplémenté le même mécanisme de mon côté. Afin de valider cette partie, j'ai créé deux instances de state_t dans main: une correspondant à l'état que je lisais à chaque tour depuis stdin (via une fonction appelée state_read), et l'autre qui était une copie de l'état initial, mais qui était mise à jour à chaque tour avec state_update au lieu de state_read. Puis j'ai temporairement programmé mon IA pour effectuer des actions très simples et déterministes, tout en faisant en sorte qu'elle suppose que ses adversaires suivent la même stratégie qu'elle. Et j'ai lancé dans l'IDE des matchs l'opposant à elle-même. À chaque tour, je comparais mes deux états au niveau du nombre, de la position, de la vitesse et de la taille des entités, et j'envoyais toutes les différences que je trouvais vers stderr. Une fois tous les problèmes corrigés, je pouvais simuler sur plusieurs centaines de tours, donc plusieurs dizaines de milliers d'itérations, sans que les deux états divergent notablement.



Calcul du score

 

Le score attribué à une stratégie découle de son évaluation sur 7 tours. C'est l'une des limitations actuelles de mon IA: si l'un de mes jetons est à l'arrêt, il ne va jamais accélérer pour prendre une entité plus petite située à plus de 7×200/14 = 100 unités de lui. C'est parfois problématique.

Pour chacun des 7 tours de l'évaluation, j'attribue un sous-score correspondant à la somme de la masse totale de mes jetons et de deux fois la masse de mon plus gros jeton. Cette heuristique permet de privilégier les scénarios où mes jetons se regroupent entre eux. Le score final est la somme des ces sous-scores, pondérés par 0.7^n -- où n est le numéro du tour considéré, de 0 à 6 -- afin de favoriser les scénarios où je gagne de la masse rapidement. En effet, mes hypothèses 2 et 3 ont leurs limites, et plus le temps passe, plus il y a des chances que l'adversaire bouge ou que j'ai moi-même intérêt à bouger. Via cette pondération, les sous-scores correspondant aux évolutions les plus probables ont le plus d'impact dans le score final.


Stratégies

 

Pour déterminer les accélérations proprement dites de mes jetons, j'ai utilisé une méthode probabiliste. À chaque tour, je tente les stratégies suivantes:
+ ne rien faire
+ faire converger tous mes jetons vers leur barycentre équipondéré (marche rarement)
+ altérer la meilleure stratégie trouvée jusqu'ici en accélérant un jeton au hasard (répété 40 fois, marche bien)
+ altérer la meilleure stratégie trouvée jusqu'ici par recuit simulé (5 itérations)

En pratique, on peut considérer tout ce processus comme un seul grand recuit simulé au cours duquel on reste longtemps à température maximale (c.-à-d. pendant les 40 itérations où les jetons sont accélérés de façon totalement aléatoire). Les actions les plus efficaces utilisées par mon IA («double propulsion» en se servant d'une paroi, absorption d'un adversaire via une gouttelette expulsée, etc.) sont simplement celles retenues au terme de la cinquantaine de stratégies déterminées aléatoirement et testées à chaque tour.

Voila, c'est fini! Plus qu'à lancer le bot dans l'arène...


C'est trop lent

 

...et constater qu'il time out. Ce n'est pas étonnant: on a besoin de 7×500=3500 pas de simulation par stratégie, en vérifiant à chaque pas s'il y a collision entre toutes les paires possibles d'entités. Supposons qu'il y ait 20 entités; pour une cinquantaine de stratégies, ça fait 50×3500×(20x19)/2 = beaucoup trop de tests de collision. Pour information, avec l'IA telle que je l'ai décrite jusqu'ici, il est possible d'évaluer 3 stratégies par tour. Au moment ou j'avais essayé de l'utiliser dans l'arène, je crois que ça avait suffit pour atteindre la vingt-et-quelque-ième place.

D'après ce que j'ai pu lire sur le forum, beaucoup de participants ont utilisé des méthodes analytiques pour calculer la date de la collision la plus proche, et c'est exactement ce qu'il fallait faire. Cela dit, pour un maximum de précision, il fallait arrondir cette date à celle du pas de simulation qui la suit immédiatement, et refaire les tests et calculs de collision à cette date arrondie. Ainsi, il n'y a aucune perte au niveau de la précision de la simulation, et il est possible d'observer un gain en performances de l'ordre de ×20, permettant d'atteindre les quelques 50 stratégies testées par tour que je visais. C'est vraiment l'astuce qu'il fallait voir, je pense, lors de ce challenge. Il ne faut surtout pas sous-estimer l'importance d'avoir des prédictions à la fois rapides et fiables du déroulement de la partie.



Winamax Open Day

 

Ma stratégie a dans une certaine mesure a montré ses limites lors du Winamax Open Day. Lors de cet event, les règles étaient les mêmes que lors du challenge de deux semaines, sauf que les jetons atteignant le bord de la table étaient perdus au lieu de rebondir. Trois soucis apparaissaient alors:
1) Il me fallait changer la fonction de calcul du score pour faire en sorte de favoriser les situations où mes jetons vont lentement (et donc mettent beaucoup de temps à atteindre les bords de la table). Cela n'était pas trivial: lors de mes premiers essais, pour abaisser la vitesse moyenne de mes jetons, mon IA choisissait souvent d'expulser volontairement de la table les plus rapides d'entre eux, ce que j'aurais préféré qu'elle s'abstienne de faire... Néanmoins, il fallait toujours essayer d'absorber les gouttelettes neutres rapidement en début de partie, donc ne pas privilégier la lenteur de jetons à ce moment là. Outre ces deux problèmes, le fait de ralentir les jetons passé un certain stade diminuait l'efficacité globale de mon IA à cause de sa myopie vis-à-vis de ceux qui sont quasiment à l'arrêt, comme déjà évoqué plus haut. Pour remédier tant bien que mal à cette limitation, j'ai passé le nombre de tours de simulation pour le calcul du score de 7 à 15, quitte à réduire de nombre de stratégies évaluées.
2) Les hypothèses que prenaient un certain nombre de mes adversaires et dont la validité était limitée à cause de la présence de bords devenaient soudain beaucoup plus valables. C'est notamment le cas de Manwe, qui m'a avoué que la double accélération via rebond contre un bord, souvent utilisée par mon IA, posait des soucis pour la sienne. Mais impossible d'accélérer de cette façon lorsqu'il n'y a plus de bords!
3) Les IA avancées, qui anticipaient les actions adverses, devenaient plus fortes, dans la mesure où leurs prédictions s'avéraient justes beaucoup plus souvent. En effet, les décisions adverses difficiles à prévoir, notamment celles impliquant des rebonds complexes contre les bords, devenaient beaucoup plus rares.


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

trnsnt (2e place, Python, France)




Les débuts

 

Ma première idée était de simuler différents déplacements pour chacun de mes pions afin de déterminer lequel me menait à la meilleure solution au bout de X tours. J'ai cependant vite renoncé à cette idée car la limite des 100ms par tour ne me permettait pas de simuler assez de déplacements et j'avais donc des résultats plus que moyens. J'ai ensuite tenté de modéliser au mieux le système de jeu avant de me rendre compte que la modélisation importait moins que la stratégie appliquée. Il était donc temps de chercher une stratégie !


Une stratégie principalement défensive...

 

Je suis donc reparti de zéro, j'ai regardé et joué beaucoup de matches afin d'essayer de définir une nouvelle stratégie. Alors que beaucoup de monde semblait privilégier l'attaque, je me suis dit que j'allais principalement me concentrer sur une stratégie défensive pour essayer de garder mes jetons en vie le plus longtemps possible. Pour ce faire, pour chacun de mes pions je détermine, à chaque tour, quels sont les pions adversaires qui sont susceptibles de l'absorber s’ils le prennent pour cible (pour les pions contrôlés par des joueurs) ou non (pour les pions neutres) au bout de 3 tours. J’en retire donc une liste de pions potentiellement dangereux, et si cette liste est non vide, je détermine la "meilleure" direction pour m'éloigner de tous ces pions.


... Mais pas seulement

 

Bien que cette technique soit assez efficace, pour rester en vie, elle ne permet pas de faire grossir les pions, et ne permet donc pas de monter très haut dans le classement. Il me restait donc à implémenter une stratégie d'attaque, qui serait utilisée seulement dans le cas où il n'y a aucun pion dangereux détecté. Ma première idée a été d'attaquer le pion le plus proche, en termes de distance, et même si cette idée peut sembler bonne, elle n’est pas vraiment efficace. En effet le pion le plus proche n'est pas nécessairement le pion le plus simple à capturer. Par exemple 2 pions peuvent être proches mais avoir des vitesses opposées. Il fallait donc être capable de déterminer au bout de combien de tours deux pions peuvent se rencontrer. C'est, d'ailleurs le seul moment où j'ai eu besoin de faire un petit peu de mathématiques. Sachant que la trajectoire d'un pion durant le tour à venir est : (x + vx*t, y + vy*t), pour détecter la collision entre deux jetons, il suffit de chercher la valeur minimum (et positive) de t vérifiant (x + vx*t - x1 - vx1*t)² + (y + vy*t - y1 - vy1*t)² - r1 -r2 = 0.
J'ai ensuite distingué 3 cas en fonction de la valeur de t :
  • t ∈ [0,1] Dans ce cas, la collision va avoir lieu au prochain tour, il ne me reste plus qu'à déterminer s'il est préférable d'attendre ou d'attaquer. Pour ce faire, je simule une fuite de la part du pion adversaire, et je détermine si je peux tout de même l'atteindre sans accélération ou pas.
  • t ∈ ]1,∞] Dans ce cas, la collision va avoir lieu dans plus d’un tour. Afin d'affiner cette valeur, je réévalue ce temps en simulant le déplacement de mon pion vers le pion ciblé, sur 3 tours. Cela peut servir par exemple si deux pions avancent lentement l'un vers l'autre. Le fait de faire accélérer mon pion vers l'autre va réduire le temps de collision entre les deux pions.
  • t < 0 (t n’existe pas) Dans ce cas, comme précédemment, Je réévalue ce temps en simulant le déplacement de mon pion vers le pion ciblé sur 3 tours.

Après avoir déterminé ce temps pour chaque pion, je choisi le déplacement qui m'amène vers le pion le plus sûr (i.e. avec la partie entière de t la plus faible et qui me fera grossir le plus).



Petites optimisations


J'ai finalement été obligé de prendre en compte certaines subtilités afin d'éviter quelques soucis qui arrivent assez couramment.

Pour chaque pion que je vise, je vérifie s'il n'est pas déjà ciblé par un autre pion qui pourrait l’atteindre avant moi et qui formerait après collision un pion plus gros que le mien. Je vérifie aussi que mon pion ne va pas croiser sur sa route un autre pion plus gros. De plus, si je détecte qu'il me faut X tours pour atteindre un pion, je contrôle toujours que ma taille après X - 1 accélérations sera toujours supérieure à la taille du pion ciblé.



Conclusion


Je finirais par quelques petits conseils qui m’ont été fort utiles durant le challenge. Le premier, aussi évident que cela puisse paraître est qu’il est important de lire correctement l'énoncé! En effet j'ai passé une grande partie du challenge (et je ne suis pas le seul) en considérant qu'une accélération faisait perdre 1/15 du rayon et non de la masse. Corriger ce problème m'a permis de gagner facilement une bonne dizaine de places. J’ai aussi mis en place un système me permettant de rejouer sur mon PC chaque tour de jeu, ce qui ma permis de résoudre pas mal de petits problèmes.

Un dernier petit mot pour féliciter tous les participants, et pour remercier CodinGame, car c'était vraiment génial!


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

Gamoul (3e place, PHP, France)


Ma première réaction en voyant l'énoncé du challenge a été de me dire que ça allait être vraiment compliqué. Calcul des trajectoires, gestion des collisions et des conséquences, rebonds sur les bords... Rien que la modélisation du système de jeu allait être un vrai casse tête. Au final, je suis loin d'avoir pris en considération tous les paramètres, je n'ai pas tenu compte du changement de trajectoire lors des collisions par exemple. Je n'ai pas géré les rebonds sur les bords jusqu'à quelques heures avant la fin, et encore, j'ai implémenté ça d'une manière très approximative. Et je n'ai pas géré non plus la trajectoire des droplets éjectés lors des accélérations.


Collisions 

 

Dès le début, j'ai pensé que le point principal était de pouvoir anticiper les collisions. En particulier savoir à quel temps un de mes jetons allait entrer en collision avec un autre. J'ai d'abord pensé simuler l'environnement en déplaçant les jetons pas à pas, mais le problème c'est qu'avoir un pas trop grand me ferait rater des collisions, et un pas trop petit consommerait trop de temps ou ne me permettrait pas de voir assez loin.

Ensuite je me suis dit qu'en traçant les 2 droites de trajectoire des 2 jetons, et en regardant l'intersection des droites et l'endroit sur chaque droite ou se faisait le croisement proportionnellement à leur taille je pourrai peut-être arriver à quelque chose. Mais ça me semblait trop compliqué...

Puis j'ai décidé qu'il fallait faire un peu de maths.

En écrivant les équations temporelles en x et y de chaque jetons j'obtenais des fonctions du genre X(t) = x + vx*t et Y(t) = y + vy*t.

Une collision entre 2 jetons arrive quand la distance entre les 2 centres est inférieur ou égale à la somme des rayons. La distance se calcule facilement à partir des coordonnées X et Y des 2 jetons, et au final, on se retrouve avec une équation du second degré selon t. On a donc soit aucune solution (pas de collision) soit une solution (les jetons se frôlent) soit 2 solutions (début et fin de collision). Des solutions négatives indiquent des trajectoires divergentes.

Un logiciel de calcul formel m'a permis d'obtenir l'expression de ces solutions (t) en fonction des positions, vitesses, et rayons des deux jetons.

Je me suis dont fait une fonction qui, à partir de 2 jetons, indiquait si ou ou non il y avait collision, et au bout de combien de temps.



Comment jouer ? 

 

Maintenait qu'on peut simuler grossièrement l'environnement, se pose la question de savoir quoi en faire. Mes jetons devaient-ils se fixer une cible, puis voir si elle était atteignable ? Chercher le plus gros, celui qu'on allait absorber en moins de temps ?

Finalement j'ai fait au plus simple : tester.

Pour chaque jetons, j'ai regardé ce qu'il se passait soit en en faisant rien, soit en accélérant dans toutes les directions autour de moi.

La encore se posait un problème de performance... il fallait que je fixe un pas d'angle de tir, pour rester précis mais en évitant les timeouts. J'ai finalement fait un pas variable, en fonction du nombre de mes jetons et du nombre total de jetons sur la table. Au début le pas variait entre 0.05 et 0.2 radians, mais au fil de mes améliorations, ça s'est gâté. A la fin, j'avais un pas de 0.5 rad au pire, et au mieux 0.1.

Donc au final, à chaque tour, chacun de mes jetons simule un tir tout autour de lui, et un wait, et regarde dans chacun des cas si il y a une collision, et au bout de combien de temps.

Il me fallait une méthode d'évaluation pour faire mon choix. Plus la cible était grosse, meilleur le choix était (sans pour autant être plus gros que moi), mais le temps pour atteindre la cible lui devait être le plus petit possible.

J'ai donc choisi de donner un score à chaque angle testé, à l'aide d'une méthode totalement empirique que voici :

- si la prochaine collision se fait avec un jeton plus gros (sauf les miens) , le score est de -1 / (1+t)
- si c'est avec un des miens, score = 0.5 * r / (1+t)
- si c'est plus petit neutre, score = r / (1+t)
- si c'est un plus petit ennemi, score = 2 * r / (1+t)

Ainsi, un score négatif signifie que je vais me faire manger, et donc le temps le plus long est mieux. Mais si la cible est plus petite, je favorise le plus gros rayon, le temps le plus court, et aussi l'absorption d'ennemis, puis de neutre, et des miens.

Quelques ajustements sont nécessaires, parce que par exemple si mon jeton file déjà vers une cible, ça n'est pas forcément judicieux d'accélérer pour l'atteindre (sauf si c'est un ennemi). Il faut aussi gérer la perte de rayon à chaque accélération, et ne pas dépenser plus en accélération que ce qu'on gagnerait en absorbant une cible.

J'ai aussi ajusté les prédictions en assumant qu'un ennemi plus gros allait me foncer dessus, et j'ai donc augmenté sa vitesse comme si il visait juste devant moi.

Puis, comme jusqu'à présent je ne simulait qu'une seule accélération, j'ai fait comme si on pouvait accélérer 2 ou 3x plus (un peu moins en fait, pour compenser l'inexactitude de la trajectoire simulée) pour essayer de prévoir 3 coups d'attaque à l'avance. Si aucune collision n'est trouvée à 1 coup, on regarde à 2, puis à 3.



Anticipation des collisions de mes cibles 

 

Le gros problème de ma stratégie, bien qu'elle m'ait amené autour de la 20ème place après environ une semaine de jeu, est que je calcul les temps des collisions vers mes cibles en faisant abstraction d'autres collisions intermédiaires. J'ai implémenté cette solution :

Pour chacune de mes cibles je teste les collisions avec tous les autres jetons de la table afin de voir si ma cible va avoir une collision avant celle avec mon jeton. Je calcule les conséquences de cette collision et ça devient ma nouvelle cible. Mais je ne mets pas à jour les trajectoires.

Cette méthode très approximative (je ne teste qu'une collision) a tout de même été efficace. Au final je me suis rendu compte que le jeu va tellement vite que les changements de trajectoire des cibles ne sont pas si importants.

Par contre, niveau temps d'exécution, cette amélioration m'a obligé à revoir mon code en profondeur afin de l'optimiser. Factorisation dans les calculs, réduction des appels aux tableaux, suppression des sorties d'erreur... et au final pas de petits messages de la part de mes jetons, bien que certains des adversaires m'aient bien fait marrer :D

Après avoir mis tout ça en place, j'ai été très surpris de me retrouver dans le top 10, alors que je ne me préoccupais toujours pas des rebonds sur les bords...



Rebonds 

 

Mardi soir, dernière soirée dédiée au challenge, et à l'implémentation de ces foutus rebonds.

Ils me causent du tracas depuis le début, car le fait que les rayons varient pour chaque jetons complique la tache.

En effet, si on avait uniquement des points, le plus simple aurait été d'appliquer des symétries sur les bords du plateau et d'ajouter des jetons virtuels qui arriveraient de l'extérieur. On aurait 8 plateaux virtuels autour du plateau réel, et encore ça ne traiterait qu'un rebond vertical et un horizontal à la fois.

En tenant compte des rayons, la solution la plus élégante aurait été de calculer les équations de temps des coordonnées de chaque jetons en tenant compte des inversions de vx ou vy lors des rebonds, puis de limiter le domaine de définition de chaque équation pour avoir non pas une droite mais une suite de segments... Ce qui aurait multiplié de manière catastrophique les calculs de collision.

Finalement j'ai traité le problème à minima :

- je ne traite les rebonds que sur 1 axe, en appliquant une symétrie
- je ne considère que le rebond de mon jeton (donc en tenant compte de mon rayon)
- je ne calcul un rebond que si mon jeton va vers le bord, et est à moins d'une certaine distance du bord

Du coup, dans ma fonction de collision, je teste 3 cas : cas normal (pas de rebond) cas ou mon jeton rebondit sur l'axe vertical vers lequel il se dirige, et de même pour l'axe horizontal.

Ça multiplie quand même par 3 le temps de calcul des collisions dans le cas ou tous les jetons seraient proches du bord, et se dirigeraient vers celui ci...



Sprint final 

 

Mercredi, je n'étais pas satisfait de ma place. Malgré l'implémentation des rebonds, je n'arrivais plus à accrocher le top 10. C'est un peu en panique mercredi soir que j'ai tenté d'améliorer les choses en anticipant un peu mieux les absorptions de mes cibles par des jetons intermédiaires. Comme expliqué plus haut, pour chaque cible atteignable en un temps t je regarde si elle ne se fait pas manger par un autre jeton à un temps < t. J'ai donc un niveau de prédiction, et j'ai tenté de passer à 2. Ce fut catastrophique. A moins de 2H de la fin du challenge, je me suis retrouvé dans les 50ème avec plein de timeouts et à ne plus savoir quelle version parmi mes sauvegardes était la meilleure. Devais-je remettre celle sans la gestion des rebonds ?

Finalement après un gros coup de stress j'ai trouvé 2 énormes bugs dans ma version de mardi soir, je les ai corrigés, j'ai fait quelques ajustements sur des constantes et j'ai envoyé ma dernière IA moins d'une heure avant la fin. Je n'ai même pas pris le temps de la sauvegarder...

Quelques minutes avant la fin, j'étais remonté 6ème, au coude à coude avec mes adversaires. Et puis il y a eu LE bug. Je n'ai pas trop à m'en plaindre, le renvoi de mon IA dans l'arène après la fin du tournoi, dans un contexte plus calme, m'a probablement permis d'avoir moins de timeouts et de finir sur le podium.



En conclusion, bravo à tous les participants, et un grand merci à CodinGame d'organiser ce genre d'évènements :)

Et à très bientôt sur le Poker Chip Race Training !

2 commentaires :

  1. Thx pour le partage les gars !

    C'est étonnant de voir ces stratégies vraiment différentes et d'obtenir de bon résultats comme cela ! :)

    RépondreSupprimer
  2. Bravo et merci pour le partage.

    Petite dédicace spéciale à Xylo : Lors de ce challenge auquel je n'ai pas participé mais que j'ai suivi de très près grâce à mon conjoint qui lui a concouru, une chose m'a profondément dérangée : le Tchat.
    Nous avons subi pendant des jours la suprême supériorité du charmant Attractive, qui a disparu dès ton apparition.
    Merci Xylo !
    Mon cher et tendre se sachant absent le week-end et les jours précédents la fin du challenge ne s'est pas investi et n'a donc pas été capable de parer cet orgueil survitaminé.
    Merci donc encore.

    Bravo à tous.

    Malyaa

    RépondreSupprimer