Et c'est encore une participation aux « Retro Programmers United for Obscure Systems » cette fois à la découverte du Sinclair QL. Je ne connaissais pas du tout le Sinclair QL. Enfin si, de nom et de photo. Mais je n'en ai jamais vu en vrai et forcément jamais pianoté dessus, ni utilisé. C'était une découverte totale.
Un changement de classe de machine aussi. Jusqu'à maintenant, RPUfOS avait toujours sélectionné des machines 8 bits avec des capacités assez limitées. Ici, un processeur presque 16 bits, de la mémoire à foison et un support magnétique intégré. Le grand luxe !
Le jeu de Könane
Pour cette édition et sur cette machine complètement inconnue pour moi, avec aucune idée des performances atteignables, aucune idée des capacités graphiques, aucune idée du système... j'ai hésité entre une aventure graphique et un jeu de pion. Le temps passant, faire des illustrations à peu près potables (ou trouver un artiste) pour un jeu d'aventure graphique est assez long, c'est le jeu de pions qui l'a emporté.
J'ai rapidement écarté l'idée de reprendre le Reversi du Canon-X07 de la dernière fois. L'idée aurait été de le pousser un peu plus graphiquement (facile) et dans son IA (probablement facile), peut-être même en prenant un logiciel publié plus récemment, pour filer l'idée de l'adaptation de jeu publié sur magazine. Mais... déjà ce n'était pas très fun, et ensuite, il y a déjà un Reversi sur Sinclair QL qui a l'air pas mal du tout. Donc bon, bof.
J'ai donc regardé ce que j'avais dans ma ludothèque ou bibliothèque ludique comme jeu un peu moins connu, en tout cas de moi, que je trouverais intéressant à partager (et à découvrir moi-même). Des jeux de pions, traditionnels ou modernes, il en existe des centaines. Parfois pas très éloignés les uns des autres, ils se classent en grandes familles.
En fouillant un peu, je suis tombé sur le Könane, un jeu dont j'avais déjà lu les règles, mais jamais eu l'occasion de faire une partie. Ni même, en fait, réellement comprendre les règles et ce qu'elles signifient sur le déroulement du jeu.
Je ne vais pas entrer dans les détails du jeu, ceux-ci sont disponibles soit sur Wikipedia soit dans le manuel de mon adaptation publiée sur itch.io. Globalement, il s'agit d'être le dernier joueur à pouvoir jouer un coup valide, c'est à dire capturer une pièce adverse.
Le Sinclair QL
Machine sortie probablement au mauvais moment avec un mauvais positionnement, ce n'est pas une mauvaise machine et elle a toujours une communauté qui semble assez active. Mais tout semble un peu bancal et surtout bridé. On dirait que son architecture a été conçue spécifiquement pour ne pas y permettre le développement de jeux vidéo, à part les jeux « sérieux » type échecs. Ça ne serait pas complètement étonnant, il semblerait que Lord Sinclair était déçu des destinées de « console de jeux » qu'avaient eu les machines précédentes. Lui voulait du sérieux et de l'éducatif.
Qu'est-ce qu'on a dans cette machine : un processeur principal 68008, qui est un processeur 16 bits... avec un bus de données 8 bits. Mais 32 ko réservés à l'affichage, pas mal, sur un total de 128 ko. En sortant de toutes ces machines 8 bits avec des 4ko, 8ko, 16ko de RAM, il y a de quoi donner le tournis. À vrai dire, plusieurs fois pendant le développement j'étais inquiet de voir la RAM s'approcher de certaines limites, comme 32ko, avant de me rappeler que, ah ah, non, pas de soucis d'adressage, de bank ou de limites à 64ko. La LIBERTÉ !
Enfin, avant de m’apercevoir que le système prenait quand même une bonne partie de cette mémoire.
Système qui est d'ailleurs plutôt intéressant. Le QDOS est multi-tâches, certes dans MMU, mais tout de même, l'ordonnanceur fonctionne bien et on peut lancer des tâches concurrentielles. Et même depuis son BASIC, le SuperBASIC, très complet d'après la documentation (je n'ai pas poussé l'exploration du BASIC de la machine).
Quelques choix techniques
J'ai commencé les tests assez tôt pendant la Jam, à un moment où je me suis dit que j'aurais peut-être eu le temps de faire deux jeux, qui sait. Naïf que je suis. Mais je voulais de toute façon garder une partie démos/tests, pour explorer la machine, et une partie jeu, pour le jeu en lui-même.
Je suis donc partie sur une architecture similaire à MSXgl dans lequel le framework place ces projets dans des sous répertoires. C'était aussi l'occasion d'utiliser la fonctionnalité de worktrees de git, que j'avais souvent vu passer sans la mettre en œuvre.
Ainsi, j'avais sur le disque deux regards sur le même repo via des branches différentes : la branche de framework (qui est aussi la branche de démo), et la branche du jeu. Lorsque j'avais besoin d'une nouvelle fonctionnalité, je la programmais et la testais sur la branche de jeu puis, pour le commit, je découpais en deux commits, donc un partait sur la branche framework et l'autre la branche jeu. Plutôt pratique.
Lors des premiers essais graphiques, les routines de blit/fill/copy étaient en C comme le reste du programme, puisqu'avec une machine comme celle-ci, le choix du C est bien plus acceptable qu'avec les machines 8 bits. En tout cas, ça a des chances d'être plus efficace. Mais... le compilateur utilisé (XTC68) reste quand même une adaptation/portage d'un vieux compilateur natif de la machine (si j'ai bien suivi). Il n'est pas si performant, et les routines d'affichage ont dû rapidement passer en assembleur.
Ce sont les seules parties en assembleur, tout le reste est en C.
Autre choix technique : se débarrasser du QDOS. Le système est conçu pour des applications de bureau, et lors de mes premiers tests, j'étais gêné par le système, sans trop comprendre comment vivre à ses côtés, comment gérer le fait d'être une tâche, partager les ressources. A priori, il est possible de le garder, certains y sont arrivés. De mon côté, j'ai vite fait le choix de passer en mode superviseur dès l’entrée dans le programme et de couper le système. J'appelle quelques fonctions système « inertes » depuis le programme (lecture clavier et son), mais je ne le laisse pas tourner.
Autre choix : pas d'allocations dynamique. Avec le système qui ne tourne pas, et sans arriver à être certain d'avoir bien compris sur quoi malloc() était branché, je fais simple : je déclare une grosse partie statique non initialisée et je gère tout via des variables statiques et des buffers simples. La nature du jeu ne nécessite pas d'allocation dynamique.
L'IA du jeu
On parle ici d'IA « à l'ancienne » : exploration d'un arbre de possibilité et sélection du meilleur coup. Un min-max (plus exactement un negamax puisque c'est un jeu à somme nulle) accompagné de son alpha-beta.
Pour descendre dans l'arbre, je maintiens une pile d'application de mouvement et de son undo. Pas d'appels récursif, mais un point de continuation, puisque l'idée dès le début était de pouvoir rendre la main à l'affichage pendant la recherche, afin d'avoir de l'animation à l'écran.
Globalement, en fonction du niveau, tous les n évaluations de nœud de l'arbre, la boucle principale reprend la main,
met à jour son affichage (les yeux du robot bougent !) puis relance la recherche.
Niveaux de difficultés
Il y a cinq niveaux de difficultés dans le jeu. Quel que soit le niveau, le premier coup (retirer un pion du plateau) est du pur hasard pris parmi les coups légaux. Je n'ai pas trouvé dans la maigre littérature sur le jeu d'information qui indiquerait quels coups sont les plus avantageux lors de la phase d'introduction. Donc... au hasard.
Pour la suite du jeu, il y a un peu de recherche ou d'autres implémentations, mais c'est assez rare (et parfois derrière paywall). Ce que j'en retire, et ce que mes premières parties contre une IA jouant au hasard m'ont appris, c'est que ce qui importe dans ce jeu, c'est les options de mouvements. Puisque l'on perd lorsqu'on ne peut plus bouger, la stratégie générale est de maximiser ses possibilités de mouvements et de minimiser celles de l'adversaire.
Le matériel en lui-même (le nombre de pions) n'a pas vraiment d'importance. Par contre, il faut faire attention que certains mouvements sont exclusifs entre eux, ou bien en empêchent d'autres.
Les deux premiers niveaux de difficultés sont ceux que j'ai utilisés pour valider la recherche de l'arbre. Je les ai gardés, mais ils sont en fait assez peu intéressants, à part pour référence des autres niveaux. Vous n'apprendrez pas à jouer au jeu en jouant à ces niveaux-là.
Le premier niveau est donc du pur hasard parmi les coups valides, sans même une détection de victoire ou de perte immédiate. Le deuxième niveau est juste prendre le premier mouvement valable dans l'arbre des possibilités, avec évaluation maximale donnée à une victoire et minimale à une défaite. Ce niveau repère donc les victoires immédiates (et les choisis) et les défaites immédiates (et donc les évite si possible).
Les trois autres niveaux évaluent les nœuds en comptant les mouvements possibles de chaque joueur et en faisant leur somme pondérée par deux constantes. Au niveau trois, seules comptent les possibilités du joueur actif. C'est le premier niveau où l'ordinateur peut vous apprendre un peu le jeu.
Au niveau quatre, l'algorithme cherche aussi à minimiser les libertés de mouvement de l'autre joueur, à parts égales.
Enfin au niveau cinq, l'algorithme cherche absolument à réduire les libertés de l'autre joueur, même au détriment de ses propres positions.
Et c'est tout. Je ne suis pas allé voir du côté des « îlots » qui se forment pendant la partie, et qui ont sûrement un intérêt sur des plateaux plus grands. Sur cette taille de plateau, qui semble être une taille d'initiation d'après ce que j'ai pu voir, des îlots se forment, mais tard dans le jeu. Vu la puissance de la machine, j'ai laissé ça de côté.
En parlant puissance, et vu le budget de temps que je voulais pour une réponse de la machine (aux alentours de 5 secondes pour des coups de milieu de partie au niveau 3, et potentiellement aller jusqu'à 10 secondes pour les niveaux les plus forts sur quelques coups), j'ai dû rester à une profondeur de 2. Ce n'est pas beaucoup. Mais la profondeur 3 amenait des réponses bien trop longues. L'alpha beta que j'ai implémenté n'est malheureusement pas très efficace, car je n'ai pas trouvé de moyen de trier de manière heuristique les nœuds à chercher pour que cet algorithme fasse vraiment son effet.
Ceci dit, pour mon niveau de jeu, les niveaux 4 et 5 suffisent déjà largement : je gagne parfois, mais pas tout le temps. Et comme ce n'est pas un jeu très connu, je me suis dit que c'était suffisant.
Une dernière chose : les niveaux 4 et 5 jouent légèrement différemment, et à la simulation le niveau 5 gagne statistiquement, mais de peu. Ce sont donc des niveaux sensiblement aussi forts l'un que l'autre. Le niveau 3 est bien plus simple. Les niveaux 1 et 2 sont juste là pour découvrir le jeu sans se faire massacrer immédiatement.
Graphismes et textes
J'ai choisi le mode 4 couleurs pour sa définition. Je n'ai pas une grande maîtrise de l'utilisation des couleurs (euphémisme), encore moins avec les palettes exigeantes de ces machines. Avec 4 couleurs, j'ai une couleur par joueur, une couleur neutre de texte et le fond. Parfait.
Tout a été dessiné avec Gimp. Les pions sont des sélections circulaires remplies avec un gradient puis passées au tramage et entourées.
Le robot a été construit péniblement depuis des références Pinterest (« tete de robot humanoide annees 70 low color » est pas mal). Pour les expressions, je me suis contenté de faire bouger les yeux pour éviter la catastrophe d'une tête animée.
Les sprites et les images sont compressés en ZX0 (sauf les yeux du robot, car trop petits pour avoir un gain). Le titre est une image en pleine largeur d'écran, afin de pouvoir décompresser directement en mémoire écran. Ça prend quelques trames, mais ça ne se perçoit pas vraiment. Et puis ça s'affiche de haut en bas, donc ça passe.
Les textes viennent d'un atlas de fonte. Comme je stocke la fonte sous une forme qui peut se décoder à la fois en mode 4 couleurs et 8 couleurs, les glyphes sont assez lents à afficher, ça se sent sur la page d'instructions. Je n'ai pas jugé utile d'optimiser ça.
Un point sur les émulateurs
Lors du développement, j'ai utilisé deux émulateurs différents. MAME, que j'ai profité à améliorer sur le support des microdrive et les timing des accès CPU (c'est loin loin d'être parfait, mais un peu mieux). MAME ne supporte que les MDV et les chargements sont lents, je le lançais donc de temps en temps pour vérifier.
Plus régulièrement, j'utilisais sQLux. sQLux a l'air plutôt correct mais prend certains raccourcis visiblement (sur l'affichage, qui cache des problèmes de vsync, sur le décodage du clavier, sur le son). Par contre, le chargement est quasi instantané si on le demande et cela a permis des tests rapides.
À noter que pour le son, les deux émulateurs ne produisent pas le même résultat pour les mêmes paramètres, en particulier pour la même fréquence. J'ai resserré les mélodies autour des notes jouées de manière assez similaire mais je n'ai pas de certitude sur le rendu final. Surprise lorsque quelqu'un utilisera.
Conclusion
La machine est une découverte. Elle n'a pas été très populaire et on peut comprendre pourquoi : architecture un peu bancale, positionnement résolument professionnel face à une concurrence féroce et plus attrayante... Mais il y a plein de bonnes choses, et une descendance à travers l'évolution de QDOS que j'ai découverte au passage. Minerva déjà, qui tourne aussi sur le QL, puis SMSQ/E, plus moderne et qui tourne sur du matériel plus récent.
J'ai aussi pu découvrir un jeu assez intéressant et qui sort des jeux de pions les plus classiques dans nos régions. Ah, et si vous gagnez en niveau 3 ou plus, vous débloquerez quelque chose ! (il y a aussi un mot secret dans le menu de sélection qui débloque directement... chut).
