RetroChallenge, mars 2019

Sommaire

Dans l'édition précédente (RC2018/09) je me suis initié à la programmation sur SNES mais je n'ai fait qu'un testeur de manette.

Pour cette édition de mars 2019 du RetroChallenge, afin de ne pas tout oublier ce que j'ai réussi à apprendre sur l'assembleur 65816 et l'architecture du SNES, j'ai l'intention de mettre le tout en pratique en réalisant un jeu simple. D'ailleurs, faire un jeu pour SNES, ça fait très longtemps que j'y pense!

Je ne suis pas certain pour le moment du jeu que je réaliserai, mais j'aimerais bien qu'il exploite les boutons supplémentaires du NTT Data Keypad, puisqu'il n'y a pas assez (voir aucun, selon que JRA PAT peut être considéré un jeu ou pas) de jeux supportant cette manette:

La manette NDK10

La manette NDK10






Section 1: Outils de développement

Pour une fois, prendre le temps

La dernière fois que je me suis entraîné à programmé sur SNES, j'ai pris une approche « apprendre-au-vol, que-le-minimum, aller-très-vite ». Cela a fonctionné, oui, mais pas sans obstacles.

Mais cette fois, je prends la programmation pour SNES très au sérieux. J'ai donc imprimé ce qui semble être jusqu'à présent un excellent ouvrage de référence sur le CPU 65816. J'ai passé quelques heures à lire au sujet de son architecture, à découvrir ces multiples modes d'adressage et à apprendre quels sont les limitations et les pièges à éviter. Cela m'a beaucoup servi ces derniers jours, je ne regrette pas.



Environnement de développement et outils

Je préfère de loin travailler sous Linux, mais cela rends la vie du programmeur pour SNES plus difficile. Plusieurs outils et compilateurs sont distribués au format .EXE (et souvent même pas open source). Je pourrais employer Wine ou une machine virtuelle, mais ce n'est pas idéal. Il en résulte que mon jeu d'outils de travail est probablement un peu atypique.

Langage de programmation

Je sais qu'il y a moyen d'utiliser le langage C pour développer sur SNES et qu'il existe des librairies pour simplifier certaines choses, mais je préfère continuer d'utiliser l'assembleur pour le moment. Comme je suis en apprentissage, je pense qu'il vaut mieux être proche du matériel. Ainsi, comme rien n'est caché, il est plus facile de comprendre, et ultimement, de tout maîtriser.

L'assembleur dont je me sers est WLA-DX. J'ai appris à m'en servir lors de mon projet précédent (test de manette) et plusieurs tutoriels de programmation SNES en ligne emploient WLA-DX.


Graphiques

« Tilesets »

J'utilise GIMP pour dessiner les tilesets. Configurer une grille visible de la bonne taille aide beaucoup. Il faut aussi se servir du mode de couleur indexé et bâtir une palette manuellement.



J'enregistre le tilesets dans une seule image .PNG.


« Tilemaps »

Les tuiles (dans le tileset) sont de petites images de taille fixe auquel il est possible de faire référence par un numéro. Un tilemap est un tableau bidimensionnel qui précise à quel endroit chaque tuile doit apparaître dans l'écran. Dans ce projet, la zone visible de l'écran occupe 32x28 tuiles, ou 256x224 pixels (les tuiles font 8x8).

Pour construire un tilemap, j'utilise un outil nommé Tiled. Il est gratuit, open-source et fonctionne sous Linux. Parfait.

J'importe le fichier PNG du tileset dans Tiled, et je place ensuite les tuiles dans la grille. Ce qui est très pratique, c'est que Tiled détecte quand le fichier PNG du tileset est changé (suite à une retouche dans gimp, par exemple) et le charge à nouveau automatiquement. Le résultat est donc instantané.

J'aime bien travailler avec les deux outils simultanément, chacun dans son propre écran: (gauche: Gimp, Droite: Tiled)




Conversion des données

Naturellement, la console SNES ne traite pas directement les fichiers .PNG ni les fichiers .TMX de Tiled. Mais j'ai des outils pour faire la conversion.

Conversion des PNG

Afin de convertir les tileset au format PNG vers le format binaire utilisé par la console, j'utilise png2snes (Voir aussi mon fork sur GitHub). L'outil fonctionne en ligne de commande, il est donc facile de l'appeler depuis un script. J'y fais appel depuis le Makefile de mon projet, ainsi, quand je modifie une image, la conversion est refaite automatiquement lors de la prochaine compilation.

Voici un exemple:
./png2snes -v --bitplanes 4 --tilesize 8 --binary tilemaps/main.png --output=main
L'extrait ci-dessus lit le fichier main.png et génère deux fichiers: main.cgr (la palette) et main.vra (les donnés d'image). Les deux fichiers sont inclus depuis le code source à l'aide de la directive .incbin de l'assembleur.

Conversion du Tilemap

Tiled peut exporter le tilemap en format .CSV. Comme cela peut se faire en ligne de commande, cela s'automatise facilement:
tiled tilemaps/grid.tmx --export-map grid.csv
C'est un début (CSV est bien plus simple qu'un fichier XML, ce que sont réellement les fichiers .TMX de Tiled) mais ce n'est pas ce que le matériel du SNES exige. Pour passer de ce format CSV à un format binaire compris par la console, j'ai créé un outil en C:
csv2bin/csv2bin title.csv title.map
Cela génère le fichier tilemap.map qui est alors inclus par le code source (avec .incbin). Ici encore, la conversion est gérée par le Makefile du projet. Il suffit d'appuyer sur le bouton enregistrer dans Tiled et tout le nécessaire sera fait automatiquement à la prochaine compilation.



Éditeur pour le code

J'utilise déjà VIM pour presque tout, alors il est naturel de l'utiliser pour le développement SNES aussi.

J'aime avoir de la couleur, mais par défaut il n'y a pas de coloriage syntactique pour l'assembleur 65816. Mais j'ai trouvé un dépôt git contenant les fichiers de règles de syntaxe nécessaires pour le développement snes (65816, SuperFX et SPC700), exactement ce qu'il me fallait: vim syntax highlighting for 65816, SuperFX and spc700 assembly

Bien, bien!:




Faire rouler le code

Émulation

J'utilise presque tout le temps bsnes-plus, une version de bsnes spécialisée pour le débogage. J'ai contribué au projet au cours du Retro-challenge précédent en y ajoutant le support pour la manette NTT Data Keypad. C'est bien, je vais en avoir besoin. Vous aussi d'ailleurs, si vous voulez essayer mon jeu. (Avec le temps il y aura d'autres émulateurs capables d'émuler cette manette. Il me semble avoir lu que byuu ajouterait cela dans la prochaine version d'Higan, mais je n'arrive plus à trouve le tweet où il en était question...)

Tel que décrit, bsnes-plus est bien équipé pour le développement. Il est possible d'inspecter le contenu de la mémoire, d'exécuter le programme pas à pas, de regarder ce que contiennent les registres du PPU, et ainsi de suite.

J'ai une cible spéciale dans mon Makefile nommée run qui démarre automatiquement le ROM dans bsnes-plus, ce qui est pratique. Le menu System -> Reload dans bsnes-plus permet de charger un nouveau ROM sans perdre les fenêtres de débogage déjà ouvertes. Excellent!




Sur le vrai matériel!

Bien sûr, le code doit pouvoir fonctionner sur le vrai matériel. Le Super Everdrive que j'ai acheté de krikzz il y a quelques mois rend cela possible:

Everdrive + SD Card

Everdrive + SD Card



Tester dans bsnes-plus est rapide et facile, mais essayer sur la console de temps en temps est très important. Même si bsnes est un émulateur dont le fonctionnement est très fidèle à la console d'origine, il faut se méfier. Si jamais mon jeu cesse de fonctionner et que je n'ai pas essayé sur la console depuis plusieurs semaines, il pourrait être difficile de trouver quelle modification est en cause. (Cela est vrai pour tout développement fait dans un émulateur, j'en sais quelque chose. J'ai eu ce genre de problème en développant des jeux dans DOSBox...)

Mon premier test

Mon premier test

Plus tard: Test d'affichage de la grille

Plus tard: Test d'affichage de la grille




Section 2: Décision du jeu, énonciation des buts

J'avais quelques idée pour le jeu, mais je n'était pas certain de ce que j'allais faire. Mais j'ai reçu une suggestion par Twitter (de @Shadoff_d) de faire un jeu de Sudoku. J'ai trouvé que c'était une bonne idée:

  • C'est un jeu simple. Pas trop d'objets qui bougent, pas de chronométrage serré, pas de détection de collision... parfait pour débuter.
  • L'utilisation du NTT Data Keypad pour entrer les chiffres est vraiment logique et avantageuse.
Alors c'est décidé, je fais un jeu de Sudoku! Maintenant, énonçons des buts:
  • Le jeu devrait offrir plusieurs grilles différentes
  • Le jeu devrait offrir différents niveaux de difficulté
  • Le jeu doit supporter le NTT Data Keypad
  • Le jeu doit aider le joueur en refusant les entrées invalides, ou en indiquant lesquelles sont en conflit.
  • Le jeu doit détecter lorsque la solution est trouvée.
C'est assez je crois. Plusieurs de ces tâches d'apparence facile ne le seront pas pour moi en raison de mon inexpérience en assembleur 65816. (Ah si c'était du 8088 ce serait une autre histoire...)

J'ai commencé à mettre en place mon environnement de développement et à travailler sur le jeu samedi après midi, et dimanche j'avais un pilote fonctionnel pour le keypad (ok, je me suis basé sur mon projet précédent pour cette partie) et un début d'écran titre. J'avais aussi le code en place pour dessiner la grille à l'écran à partir des données contenues dans un tableau de 81 éléments (9x9) représentant l'état du jeu.

L'écran titre fonctionne. Lorsqu'on appuie sur START, l'écran fait une transition vers le noir, puis l'écran avec la grille apparaît progressivement. Normalement j'attendrais la fin pour ce genre de chose, mais quand j'ai vu à quel point c'était facile, je n'ai pas su résister. Il suffit d'écrire une valeur entre 0 et 15 à l'adresse $2100 (INIDISP) et l'effet est immédiat. Très pratique!

Voici quelques images du jeu dans son état actuel:



La grille n'est pas centrée, car je pense utiliser l'espace à droite. Je mettrai peut-être un chronomètre, et peut-être des infos sur ce que font les boutons (par exemple: Y pour effacer, B pour placer un numéro sûr, A pour placer un essai?). Ce sont des choses à expérimenter.

La prochaine chose que je souhaite faire est rendre la grille interactive. Mais j'ai besoin d'un curseur, et donc d'un sprite. Alors maintenant je dois donc apprendre comment les sprites fonctionnent sur SNES :-)



Section 3: Utilisation d'un sprite pour le pointeur

Le document Qwertie's SNES Documentation est une bonne référence sur le fonctionnement des graphiques sur SNES. Une lecture attentive m'a fourni ce qu'il fallait savoir pour utiliser les sprites.

Il faut:
  • Disposer les images de sprites en VRAM dans le bon format.
  • Indiquer au PPU la taille des sprites et à quelle adresse les images de sprites sont placés en VRAM (registre OBSEL / $2101)
  • Mettre en place au moins une palette de 16 couleurs à l'adresse CGRAM $80.
  • Écrire en OAM (Mémoire d'attribut d'objets) une structure indiquant les attributs, la position et le numéro de la première tuile 8x8 du sprite.

Disposer les images de sprites en VRAM

Le document de Qwertie mentionné ci-haut explique que les sprites sont un assemblage de petites tuiles 8x8 et qu'il doit y avoir un espace de 16 tuiles du début d'une ligne à l'autre. Cela veut dire que je peux utiliser png2snes comme je le fais déjà pour les arrière-plans, en mode 8x8, couleurs 4 bit (16 couleurs), à condition que l'image .png source fasse exactement 128 pixels de large.



Taille des sprites et origine en mémoire

Les trois bits les moins significatifs du registre 8-bit $2101 (OBSEL) permettent d'indiquer à quel endroit les tuiles sont placées en mémoire vidéo (VRAM). 1 bit vaut 16 kilo-octet, les emplacements possibles sont donc: $0000, $4000, $8000, $C000. J'ai déjà des tuiles et des tilemap au début de la mémoire vidéo, alors j'ai écrit une valeur de 2, ce qui correspond à $8000 (32K) pour sauter par dessus tout ça.

Les bits 7 à 5 permettent de choisir deux tailles pour les sprites. Chaque sprite peut être de deux grandeurs: Petite ou large (contrôlé par une table en mémoire, démontré plus bas). Ici, ce à quoi correspond les tailles dites 'petites' et 'larges' et défini:

  • 0: Petite: 8x8, Large: 16x16
  • 1: Petite: 8x8, Large: 32x32
  • 2: Petite: 8x8, Large: 64x64
  • 3: Petite: 16x16, Large: 32x32
  • 4: Petite: 16x16, Large: 64x64
  • 5: Petite: 32x32, Large: 64x64
J'ai utilisé une valeur de 5, afin d'avoir des sprites 32x32.

Palette

Un total de 8 palettes différentes (16 couleurs chaque) peut être placé en mémoire CGRAM à partir de l'adresse $80. En modifiant les attributs d'objets, il est possible d'avoir plusieurs sprites à l'écran partageant les mêmes données graphiques source (même dessin) mais s'affichant de couleurs différente. Pour mon jeu, je n'utilise qu'un sprite et une seule palette est suffisante.

Bsnes-plus contient un visualiseur de palette. Voici la palette actuelle du jeu. La bande centrale est la palette pour les sprites.



OAM (Object attribute memory)

La console SNES contient plusieurs espaces mémoire:
  • WRAM ("Work RAM", la mémoire vive "normale" pour le CPU)
  • VRAM (mémoire vidéo)
  • CGRAM (mémoire pour la palette)
  • OAM ("Object attribute memory")
Cette dernière zone, nommée OAM, contient une table de 128 entrées détaillant quelles sprites doivent apparaître dans l'écran, et à quel endroit. D'autres attributs sont également présent: La taille (petite ou grande), le numéro de palette, la priorité, le numéro de la tuile d'origine, inversion horizontale/verticale.

Bref, pour mon jeu très simple, je n'ai qu'à m'occuper de la première sprite et mettre à jour son emplacement (X/Y) en l'écrivant dans la OAM.

Le sprite viewer de bsnes-plus est très pratique pour voir ce que contient la table, particulièrement lorsque ça ne fonctionne pas comme prévu.



Après quelques problèmes, j'avais un pointeur et il était possible d'insérer des chiffres. J'ai donc pu m'amuser à terminer un puzzle avec le jeu pour la première fois!




Section 4: Problèmes sur le vrai matériel

J'ai continué à améliorer le jeu, à modifier les graphiques et à tester avec bsnes-plus. Puis j'ai voulu essayer le jeu qui était à présent très jouable sur la console. Mais surprise! Le sprite ne fonctionnait pas...

À ce moment dans le développement du jeu, le pointeur utilisé dans la grille apparaissait aussi dans le coin de l'écran titre. Mais pour une raison alors inconnue, il n'apparaissait pas sur la vrai console. Je me suis demandé quel changement avait causé cela. J'avais fait beaucoup de modifications... (je n'ai pas écouté mon propre conseil: Tester sur le vrai matériel, et souvent!).

Après plusieurs essais, j'ai compris que les sprites ne fonctionnait pas depuis le début. J'avais 127 sprites placées dans le coin de l'écran à la position 0,0. Il y a des limites au nombre de sprite total ou nombre de sprite par ligne que le SNES peut afficher, mais ce n'est pas très bien documenté (ou je comprends mal, ce qui est fort possible). Le comportement du système lorsque ces limites sont dépassées n'est sans doute pas parfaitement reproduit par l'émulateur.

Lorsque j'ai modifié le code pour que les sprites/objets inutilisés soient placés hors de l'écran, le curseur est devenu visible.

Sprite manquant

Sprite manquant

Plus tard: Sprite présent!

Plus tard: Sprite présent!



Ayant réussi à faire fonctionner les sprites même sur la console, j'ai fêté cette petite victoire en terminant un puzzle:

Première partie sur console

Première partie sur console




Section 5: Validation des coups

J'ai d'abord fait en sorte de ne pas permettre au joueur d'écraser les chiffres faisant partie du puzzle initial.

Ensuite je voulais que le jeu empêche le joueur de faire un coup qui n'est pas dans les règles. L'algorithme pour vérifier s'il est possible d'écrire un chiffre dans une case est le suivant:
  • Vérifier si le chiffre est déjà présent dans ce groupe de cases 3x3
  • Vérifier si le chiffre est déjà présent dans cette colonne
  • Vérifier si le chiffre est déjà présent dans cette rangée
Facile! Mais étant donnée une case X,Y et un chiffre J à insérer, il faut faire des boucles et accéder aux "voisins" de colonne, rangée et de groupe. Et pour chaque cellule à examiner, il faut en calculer l'adresse mémoire. (L'état du jeu est stocké dans un tableau 81 mots de 16 bits).

J'ai jugé qu'il serait plus facile d'écrire un programme en C générant pour chaque case du jeu la liste des adresses des voisins à considérer.
void computeNbrs(int x, int y)
{
	int X, Y, i, j;

	// Same row
	for (X=0; X<x; X++)
		outputNeighbor(X,y);
	for (X=x+1; X<GRID_SIZE; X++)
		outputNeighbor(X,y);

	// Same column
	for (Y=0; Y<y; Y++)
		outputNeighbor(x,Y);
	for (Y=y+1; Y<GRID_SIZE; Y++)
		outputNeighbor(x,Y);

	// Same cell AND not already output above
	for (Y = (y/3)*3, j = 0; j < 3; j++,Y++) {
		for (X = (x/3)*3, i = 0; i < 3; i++,X++) {
			if (Y != y && X != x) {
				outputNeighbor(X,Y);
			}
		}
	}
}
Le programme en C génère un fichier .ASM de ce genre:

_nbors_0_0: .dw $02, $04, $06, $08, $0a, $0c, $0e, $10, $12, $24, $36, $48, $5a, $6c, $7e, $90, $14, $16, $26, $28,
_nbors_1_0: .dw $00, $04, $06, $08, $0a, $0c, $0e, $10, $14, $26, $38, $4a, $5c, $6e, $80, $92, $12, $16, $24, $28,
...
_nbors_8_8: .dw $90, $92, $94, $96, $98, $9a, $9c, $9e, $10, $22, $34, $46, $58, $6a, $7c, $8e, $78, $7a, $8a, $8c,

neighbor_list:
	.dw _nbors_0_0, _nbors_1_0, _nbors_2_0, _nbors_3_0, _nbors_4_0, _nbors_5_0, _nbors_6_0, _nbors_7_0, _nbors_8_0,
	...
	.dw _nbors_0_8, _nbors_1_8, _nbors_2_8, _nbors_3_8, _nbors_4_8, _nbors_5_8, _nbors_6_8, _nbors_7_8, _nbors_8_8,
Pour obtenir le pointeur vers la liste de voisins d'une case en particulier, le code tournant sur la console n'a qu'à lire le mot de 16 bit correspondant de neighbor_list et faire une série d'accès indirects, en comptant jusqu'à 20.
	; Get the pointer to the list of neighbors for the designated cell
	lda neighbor_list, Y
	sta dp_indirect_tmp1

	ldy #0
@checknext:
	lda (dp_indirect_tmp1),Y    ; Load offset for neighbor
	tax                         ; Move the offset to X to use it
	lda griddata, X             ; Load the value at this position
	and #$FF
	cmp gridarg_value           ; Check if it is the value we are checking
	beq @foundit                ; Yes? Then it is not unique. Can't put this value in this cell.
	iny                         ; Advance to next neighbor in list
	iny
	cpy #20*2
	bne @checknext

	; All neighbors checked, no match found
@finished:
	; return with carry clear
	clc
	...
	rts

@foundit:
	; TODO ? Perhaps remember *where* we found it to show the user why he can't place it here?

	; return with carry set
	sec
	...
	rts
Je crois que c'est beaucoup plus simple ainsi, et probablement assez rapide en exécution!



Section 6: Indices et suggestions

Vous avez remarqué le bouton bleu (X) nommé Hint dans les screenshots? Ce bouton permet d'obtenir un suggestion. Appuyer une première fois déplace le curseur jusqu'à une case ne pouvant accepter uniquement un chiffre.

Une fois le curseur au dessus de la case suggérée, appuyez une nouvelle fois insère automatiquement le bon chiffre. Les puzzles les plus simples peuvent être complètement résolus en appuyant à répétition sur X:



Pour le moment, ce qui est fait est très simple. Le code regarde chaque case libre et compte combien de coups légaux sont possibles à cet endroit. Si une case avec un seul coup possible est trouvée, c'est elle que le jeu suggère.

Je compte améliorer cette fonction un peu en détectant aussi les cas où un chiffre ne peut qu'apparaître à un seul endroit:
  • À l'intérieur d'une ligne
  • À l'intérieur d'une colonne
  • À l'intérieur d'une groupe (3x3)



Section 7: Les puzzles

Les puzzles sudoku ne sont pas générés par la console (Désolé). J'utilise plutôt un générateur de sudoku nommé qqwing fonctionnant en ligne de commande. Quatre niveaux de difficulté sont disponibles. J'ai fait un petit script qui en génère 100 de chaque sorte:
#!/bin/bash
N_PUZZLES=100

function generatePuzzles
{
			echo "Level: $1..."
					qqwing --generate $N_PUZZLES --one-line --difficulty $1 > $1.txt
}

echo "Generating puzzles..."

generatePuzzles simple
generatePuzzles easy
generatePuzzles intermediate
generatePuzzles expert
Voici un court extrait d'un des fichiers générés (simple.txt). Chaque ligne correspond à un puzzle et les cases vides sont représentés par des points:
27....95.3..76.....8629..37...6...............4.....285.........684...9.7..9...62
....8....6.....5...71...6...9...7.......35894.53....71.....4.63.39......4.....1.8
47..5....9.6...............2...3...6.6.8.4.798..2..35.....6......8..7.4.3495.8.6.
........8...........5...21..28.9.5....36..1..9.15.3.6.5.9481.3.....3.4....6..5...
9.1....72.......65...17.....15.....9...325.1....8....6...913........7.8..2...6..7
J'ai créé un outil en C qui passe de ce format texte à un format binaire simple, où chaque case correspond à un octet de valeur 0 à 9 (0 pour les cases vides). Il serait possible de mettre deux chiffres par octet, et même de compresser les donnés, mais je n'ai pas encore de raison pour le faire. (je ne manque pas d'espace).
./puzzletxt2bin simple.txt simple.bin
Chaque collection de 100 puzzles est ensuite incluse depuis l'assembleur avec la directive .incbin.

Avec cette petite collection de 400 puzzles en ROM, il était temps de permettre à l'usager d'y accéder.

Cela a été un peu long, car j'ai créé un système pour afficher du texte, généralisé le concept de pointeur qui se déplace, de sorte que le pointeur en jeu et le pointeur dans les menu est contrôlé par le même code.

Voici le résultat:



Section 8: Première version du ROM

Voici une première version que vous pouvez essayer! Il vous faudra cependant un émulateur supportant la manette NTT Data Keypad, ou sinon une vraie manette physique et un moyen de faire tourner le ROM sur votre SNES...

Caractéristiques de cette version:
  • Total de 400 puzzles inclus
  • 4 niveaux de difficulté (simple, easy, intermediate, expert)
  • Fonction « indice » de base
  • Uniquement jouable avec la manette « NTT Data Keypad »
Téléchargement: super_sudoku_v0.1.sfc (256K)




Section 9: Nouveaux buts

Les choses se sont plutôt bien passées, mais surtout, j'ai eu beaucoup de temps pour travailer sur le jeu. La deuxième fin de semaine de RC2019/03 se termine et j'ai atteint tout mes objectifs. J'aurais peut-être dû être un peu plus ambitieux:

  • Le jeu devrait offrir plusieurs grilles différentes: Oui, 400!
  • Le jeu devrait offrir différents niveaux de difficulté: Oui, 4 niveaux
  • Le jeu doit supporter le NTT Data Keypad: Tout à fait
  • Le jeu doit aider le joueur en refusant les entrées invalides, ou en indiquant lesquelles sont en conflit: Oui!
  • Le jeu doit détecter lorsque la solution est trouvée: Et oui!
Il n'y a qu'un solution à ce petit problème: Se donner de nouveaux buts! Alors les voici:

  • Ajouter un chronomètre (j'ai déjà prévu l'espace dans l'écran de jeu)
  • Ajouter des messages (la bande au bas de l'écran est prévue pour ça)
  • Implémenter un résolveur par recherche exhaustive. Trouvez la solution à n'importe quel puzzle grâce à votre SNES!
  • Améliorer la fonction suggestion. Inclure les cas supplémentaires déjà discutés.
  • Permettre de jouer avec une manette normale. (peut-être avec L/R pour faire tourner les chiffres possibles dans la case sous le curseur?)
  • Ajouter des effets sonore de base (son d'erreur, petit clicks lors du placement d'un chiffre...)
  • Fabriquer une version cartouche du jeu (Avec un ROM programmé. Pas d'Everdrive ni de carte SD)
  • Ajouter une musique d'ambiance
Voilà, alors la semaine prochaine, j'aurai probablement réalisé au moins un de ces nouveaux objectifs.



Section 10: Conception d'un circuit-imprimé pour la cartouche

Un de mes nouveaux buts est de fabriquer une cartouche. J'ai cherché en ligne pour des options mais je n'ai rien trouvé qui corresponde exactement à mes besoins:

  • Circuit simple capable d'accepter un EPROM de 32 broches. (Quand je dis « simple », je veux dire un ROM et la puce CIC. Pas de mémoire de sauvegarde).
  • Contacts en OR (ou plaqué OR).
  • En stock, et expédié assez rapidement pour arriver avant la fin du retrochallenge!
J'ai donc décidé de concevoir mon propre circuit. Bon, j'avoue que j'ai peut-être abandonné mes recherches un peu rapidement car j'avais vraiment envie d'essayer de faire mon propre circuit. Mais j'ai quand même commandé des cartes non idéales (fini HASL plutôt qu'en or et sans chanfrein, pour ne citer que deux problèmes) au cas où je ne réussirais pas à faire fonctionner mon design à temps.


Premier volet: Dimensions de la carte

J'ai ouvert une cartouche (que ces vis soient damnés, je ne trouve plus mes tournevis spéciaux) et effectué une série de mesures avec un pied à coulisse numérique. Je crois bien que le concepteur travaillait en millimètres car les mesures arrivent généralement très prés de valeurs (ou demie valeurs) exactes en unités métrique. J'ai donc arrondi certaines mesures en me fiant à mon jugement et intuition.




Voici le dessin que j'ai réalisé:


J'espère que tout est exact. Mais lorsque j'ai imprimé mon dessin à l'échelle et placé le circuit de référence par dessus, tout s'alignait bien. J'ai donc confiance que mon circuit pourra être installé dans un boîtier de jeu standard sans problème.

Circuit placé par dessus le dessin

Circuit placé par dessus le dessin




Deuxième volet: Schéma et circuit imprimé

Je fabrique une cartouche de type « LoRom ». Avant d'avoir programmé pour le SNES, je n'avais aucune idée de quoi il s'agissait. Mais à présent que je sais que les banques de 00 à 3F sont en deux parties (0000-7FFF: Zone système [WRAM, I/O], 8000-FFFF: ROM), je comprends pourquoi la ligne d'adresse A15 n'est pas câblée au ROM.

Alors que je pensais commencer à comprendre, j'ai eu la chance de tomber sur cette page qui a fait disparaître mes doutes sur le fonctionnement des cartouches LoRom:
LoRom Model (https://www.cs.umb.edu/~bazz/snes/cartridges/lorom.html)

En plus de la mémoire ROM, il y a un autre élément important (et aussi emmerdant que les vis de la cartouche) dont il faut tenir compte: La puce de « lock out », aussi appelée puce CIC. Cette puce fonctionne en tant que clef pour sa collègue (la serrure) à l'intérieur de la console. La serrure échange des informations avec la clef, et si la réponse n'est pas satisfaisante, la console redémarre (et le jeu ne fonctionne donc pas).

Peut-on se procurer des puces CIC neuves? C'est peu probable, mais peu importe! Des équivalents ont été développés à l'aide de micro-contrôleurs PIC 12F629, et le firmware est disponible sur github. J'ai donc simplement installé un PIC 12F629 sur mon circuit.

Alors voici le schéma complet. J'espère que je ne recevrai pas de messages à propos d'erreurs flagrantes. Mais s'il y en a, veuillez m'en faire part au plus vite!



Voici à quoi ressemble le circuit imprimé que j'ai fait à partir du schéma. Je devrais en recevoir quelques uns d'ici 1 à 2 semaines. J'ai très hâte de l'essayer!




Pour la suite, voir la Section 14 où je met ce circuit à l'essais!



Troisième volet: Boîtier

Plusieurs vendeurs eBay offrent des « boitiers de remplacement » pour jeux SNES. J'en ai commandé quelques uns, mais je ne suis pas certain de les recevoir à temps. Au pire je réaliserai un modèle 3D que j'imprimerai.





Section 11: Support des manettes standard

Maintenant que les PCBs sont en commande, j'ai continué à travailler sur le jeu. J'ai d'abord rendu possible l'utilisation d'une manette standard, car la NTT Data Keypad ne court pas les rues.

Il suffit simplement d'utiliser les boutons L et R pour faire apparaître à tour de rôle des chiffres dans la case sous le curseur. Seuls les chiffres qui sont des coups légaux sont proposés.




Section 12: Résolveur de sudoku

J'ai d'abord amélioré la fonction indice (Hint) tel que déjà discuté pour détecter les cas où un chiffre ne peut apparaître que dans une seule case à l'intérieur d'une ligne ou d'une colonne. En appuyant sur Y, davantage de coups sont à présent proposés.

Pour le résolveur automatique, je fais d'abord appel au code de détection d'indice de manière répétitive jusqu'à ce que la grille soit pleine ou qu'il n'y ait plus de nouveaux coups. Pour les puzzles de niveau simple et easy, c'est généralement suffisant pour compléter la grille.

Mais pour les puzzles plus avancés, cela ne suffit pas. Après quelques itérations, il n'y a plus de nouveaux coups. Le résolveur entre alors en phase 2: La recherche exhaustive.

J'effectue la recherche à l'aide d'une fonction récursive (une fonction qui s'apelle elle-même). Voici l'équivalent en pseudo-code:
function bruteforcer()
{
	Cell cell = getEmptyCell();

	if (cell == null) {
		return true; // no more empty cells! Puzzle solved!
	}

	for (value = 1; value <= 9; value++) {
		if (cell.isLegalMove(value)) {
			cell.insertValue(value);
			if (bruteforcer()) {
				return true;
			}
		}
	}
	cell.clear();
	return false;
}
C'est surprenament simple n'est-ce pas! Le code qui s'occupe de vérifier la légalité des coups n'est pas exposé ci-dessus, mais il fonctionne exactement comme celui qui valide les coups du joueur (voir section 5, Validation des coups), seulement je l'ai modifié un peu pour être efficace dans le contexte du résolveur automatique.

Voici un court vidéo du résultat. On peut facilement repérer les deux phases (phase 1: Logique, phase 2: Recherche exhaustive):




Section 13: Deuxième version du ROM

Vous aimeriez essayer le jeu? Voici la version 0.2:

Téléchargement: super_sudoku_v0.2.sfc (256K)

Changements depuis la version précédente:
  • Peut désormais être joué avec une manette standard. Les boutons L/R permettent de choisir le chiffre.
  • La fonction d'indice suggère davantage de coups.
  • Ajout d'un résolveur automatique à deux stades. (logique et recherche)
  • Affichage plus rapide du texte et des menus.
  • Ajout d'une horloge en jeu.
Il me reste encore à ajouter des messges dans la boîte au bas de l'écran, à rendre possible l'interruption du résolveur et à ajouter des effets sonores simples, voir de la musique si j'ai le temps.




Section 14: Test du circuit imprimé

Dès que j'ai reçu le circuit imprimé, j'ai vérifié s'il s'installait correctement dans une cartouche. Aucun problème de ce côté! Il y a cependant quelques détails cosmétiques que j'aimerais corriger: Quelques vias qui tombent à l'extérieur du cuivre sur la face inférieure, présence involontaire de solder resist sur les côtés des « doigts », et passage des traces pas assez uniforme à mon goût à certains endroits. Mais dans l'ensemble, je trouve que j'ai fait du bon travail.

Les circuits

Les circuits

Installation OK

Installation OK



J'ai ensuite assemblé un exemplaire avec des supports pour les puces pour me permettre de tester facilement.



Ensuite il fallait programmer le PIC (la puce à 8 broches). C'est la première fois que j'en programmais un avec mon vieux programmeur universel et je n'étais pas certain de réussir. Mais tout s'est bien déroulé.

Le micro-contrôleur PIC se faisant programmer

Le micro-contrôleur PIC se faisant programmer

Programmation en cours... Sous Windows 98!

Programmation en cours... Sous Windows 98!



Deuxième étape, programmer l'EPROM. Du terrain connu cette fois.

Programmation du EPROM en cours...

Programmation du EPROM en cours...



J'ai ensuite installé les puces programmées dans les supports. Comme je m'y attendais, avec supports la hauteur est trop élevée et le montage ne peut pas être placé dans une cartouche. Lorsque le jeu sera terminé, je souderai les puces directement au circuit, voilà tout.

Hauteur excessive à cause des supports

Hauteur excessive à cause des supports



Et enfin, le moment de vérité. J'ai inséré le circuit en faisant très attention à son orientation, j'ai pris une grande respiration et mis la console sous tension. L'écran titre a fait son apparition une fraction de seconde plus tard!

Les circuit installé

Les circuit installé

Mise sous tension et... succès!!

Mise sous tension et... succès!!



Voici un court vidéo du tout:



Section 15: Effets sonores

Le son sur SNES est géré par un CPU indépendant, le SPC-700. Il s'agit d'un micro-processeur 8 bit ayant accès à 64kB de mémoire et à un DSP capable de gérer 8 voix. Pour plus de détails, voir le document fullsnes - SNES Audio Processing Unit (APU).

Après un reset, l'APU exécute un petit programme (loader) très simple qui attends des instructions. Le programme principal tournant sur SNES doit communiquer avec ce loader par le bias de 4 registres 8-bit afin de charger un programme de musique et/ou effets sonores plus complexe. Cela veut dire un nouveau projet en assembleur pour un autre type de CPU :)

Bon, je sais qu'il existe des solutions comme un convertisseur de musique .IT vers SPC700, ou encore des solutions de son complètes pour SNES comme le SNES Game Sound System de Shiru, qui semble excellent. Mais malgré cela, j'ai décidé d'essayer de faire quelque chose à partir de zéro puisque c'est le meilleur moyen d'apprendre. D'ailleurs, je ne fais qu'ajouter des effets sonores (pas de musique) alors ce n'est pas très compliqué.


Charger un programme dans l'APU

J'ai commencé par écrire le code qui doit télécharger le programme de son vers l'APU. Je me suis simplement basé sur le pseudo-code disponible dans la documentation fullsnes. Comme je n'avais pas encore de programme à charger, j'ai envoyé les lettres "Hello, World!" et vérifié ce que contenait la mémoire de l'APU à l'aide de bsnes-plus pour vérifier si mon code fonctionnait.



Programmer le SPC-700

L'assembleur que j'utilise pour le jeu lui-même (tournant sur le CPU 65816 du SNES) est WLA-DX et il se trouve qu'il supporte aussi le SPC-700. C'est donc ce que j'utilise.

Le premier bloc de 256 octets du SPC700 est la Page zéro (pratique pour des variables). Et le deuxième bloc de 256 octets est utilisé pour la pile. Le code peut donc débuter à l'octet 512 ($200).

Le programme peut faire presque 64 kilo-octet, mais pour simplifier les choses j'ai décidé de rester sous la barre des 32k, ce qui permet au programme de tenir à l'intérieur d'une banque de 32K côté SNES. Voici comment j'ai configuré WLA-DX:
.memorymap
	slotsize $7000
	defaultslot 0
	slot 0 $200
.endme

.rombankmap
	bankstotal 1
	banksize $7000
	banks 1
.endro
J'ai décidé que le point d'entrée pour le programme serait toujours $200, et cela est hardcodé dans le code de téléchargement. La première section de mon programme pour SPC-700 est donc déclarée ainsi, avec le mot clef FORCE pour être certain que ce code soit toujours placé à $200:
.bank 0

.section "Code" FORCE
	entry: jmp !main
...

Le SNES utilise des échantillons sonores (par opposition à d'autres systèmes qui font de la synthèse, FM par exemple) alors ces échantillons doivent être stockés quelque part en mémoire vive. Ils peuvent être placés à n'importe quelle adresse 16-bit, toutefois une table de pointeurs nommée source directory doit être mise en place. Chaque entrée dans cette table occupe 4 octets:

  • Octets 0-1: Adresse de départ
  • Octets 2-3: Adresse de redémarrage (pour les sons en boucle)
La table doit débuter sur un multiple de 256 puisque son emplacement est codé sur 8-bit seulement (voir $5D - DIR - Sample table address). Afin de garantir cet alignement, j'utilise une section free déclarée avec un alignement de $100:
.section "source_directory" align $100 FREE
source_directory:
	.dw sample1 ; Adresse de départ de l'échantillon
	.dw sample1 ; Adresse de boucle (inutilisée)
	....
.ends
Et les échantillons peuvent simplement être inclus à l'aide de la directive .incbin à l'intérieur d'une section ordinaire:
.section "samples"
sample1: .incbin "sample1.brr"
...
Mon jeu de sudoku n'a que 6 effets sonores, alors chaque effet peut être joué par un canal DSP dédié. Il n'est donc pas nécessaire de chercher pour un canal libre ou de pointer vers le bon échantillon au moment d'émettre le son. À titre d'exemple, le canal 0 est toujours utilisé pour le son d'erreur produit lorsqu'on tente de faire un coup interdit (comme effacer un chiffre du puzzle).

J'ai créé des macros pour écrire vers les registres du DSP facilement:
.macro writeDspReg
	mov DSPADDR, #\1
	mov DSPDATA, #\2
.endm
.macro writeDspReg16
	mov DSPADDR, #\1
	mov DSPDATA, #<\2
	mov DSPADDR, #\1+1
	mov DSPDATA, #>\2
.endm
et je m'en sers pour configures les canaux à l'avance. Par exemple, pour le son d'erreur:
	; Sound 0 (error)
	writeDspReg SCRN 0
	writeDspReg16 P_L $0400 ; original $1000
	writeDspReg VOL_L 128
	writeDspReg VOL_R 128
	writeDspReg16 ADSR1 $0000
	writeDspReg GAIN 64
Le registre SCRN ci-dessus est mis à 0, ce qui fait référence à la première entrée dans la table source directory qui pointe vers l'échantillon de son d'erreur.

Pour émettre le son, il suffit d'écrire dans le registre KON (Key On):
	writeDspReg KON 1
Chaque bit correspond à un des 8 canaux. Autrement dit: KON = 1<< canal.

Convertir les échantillons

Le SPC700 ne traite pas des échantillons PCM bruts. Les échantillons doivent être au format BRR (Bit Rate Reduction), un format de compression simple. J'ai localisé BRRtools, une collection d'outils permettant entre autre de convertir des fichiers .WAV vers le format BRR. Voici comment je fais la conversion du son d'erreur:
brr_encoder -sc8000 error.wav error.brr
brr_encoder peut également faire du ré-échantillonnage, ce qui permet d'économiser un peu d'espace. Dans l'exemple ci-dessus, bien que le fichier .WAV source soit à 44 ouo 48 kHz, la fréquence de sortie n'est que 8kHz. Pour obtenir malgré cela la même note lorsque l'APU joue ce son, il suffit de régler le registre P_L (voir extraits ci-dessus) correctement.

Faire jouer les sons

Après avoir configuré les registres du DSP pour chaque canal ainsi que certains registres à effet global (volume global, adresse du source directory, flags etc) mon programme pour SPC700 entre dans une boucle où il attends que la valeur du port 0 (un des 4 ports 8-bit permettant au CPU principal de communiquer avec le SPC-700) change.

À chaque changement, le programme fait écho à la valeur du port 0 (confirmation de réception) et joue le son correspondant à la valeur du port 1.

Autrement dit, du côté SNES:
sound_sendCommand:
	...
	sta APU_COMMAND	; Écrit la commande / numéro d'effet dans le port 1
	inc kick
	lda kick
	sta APU_HANDSHAKE	; Écrit une nouvelle valeur dans le port 0
@waitack:
	cmp APU_HANDSHAKE
	bne @waitack	; Attends la confirmation
	....
Et du côté du SPC-700:
@mainloop:
	mov A, CPUIO0   ; surveille le port 0 pour un changement
@waitChange:
	cmp A, CPUIO0
	beq @waitChange

	mov A, CPUIO1   ; récupère la commande et/ou le no. de son à jouer

	; confirmation
	push A
	mov A, CPUIO0
	mov CPUIO0, A
	pop A

	; ... code pour joueur le son demandé (pas montré) ...

	bra @mainloop

Essais sur le vrai matériel

Tout fonctionnait bien dans bsnes-plus, mais lorsque j'ai essayé le jeu sur ma SNES, j'ai eu une surprise. Il y avait un son continu et désagréable.


Attention!!: Bruit désagréable. Retirez vos écouteurs ou baissez le volume.

J'ai pensé qu'il devait s'agir de registres non initialisés. En effet, peut-être que dans l'émulateur certains registres (par exemple, le volume des voix) sont à zéro au démarrage, mais que sur le vrai matériel ce sont des valeurs plus ou moins aléatoires. Dans le cas d'un canal auquel je ne touche jamais (canal 7, par exemple) cela pourrait vouloir dire qu'il joue des échantillons depuis une adresse aléatoire en mémoire.

J'ai fait plusieurs essais, mais rien ne fonctionnait. J'ai même essayé de mettre le volume de toutes les voix à zéro, mais rien ne changeait.

Un peu découragé, j'ai relu la documentation. J'ai remarqué qu'il y avait des registres de volume pour l'effet d'écho auxquels mon programme ne touchait pas. Quand j'ai mis ceux-ci à 0, le bruit a cessé!

Ouf! Alors c'était beaucoup de travail pour de simples effets sonores.


Section 16: Troisième version du ROM

Vous aimeriez essayer le jeu avec son? Voici la version 0.3:

Téléchargement: super_sudoku_v0.3.sfc (256K)

Changements depuis la version précédente:
  • Ajout d'effets sonores
  • Le résolveur peut désormais être interrompu (bouton A ou START)



Section 17: Assemblage de la cartouche

J'ai finalement implémenté les messages au bas de l'écran, et lors de la sélection du numéro de puzzle, le pointeur est désormais placé à un endroit aléatoire. Je considère le jeu comme terminé et j'ai reçu les boîtiers, alors le temps est venu de fabriquer une cartouche.

L'étiquette n'est en fait qu'un screenshot de l'écran titre. J'ai fait quelques essais, et une fois que j'ai obtenu les bonnes dimensions, j'ai imprimé le tout au laser, sur une feuille autocollante avec un fini lustré. Le résultat n'est pas mauvais je trouve!

Essais et mesures...

Essais et mesures...

Et voilà!

Et voilà!



La programmation du EPROM s'est bien déroulée, j'ai soudé les puces sur le circuit imprimé et sans support, le circuit s'est installé sans difficultés dans le boîtier.





Et enfin, le tout en action!




Section 18: Téléchargez le ROM

Le ROM de la version 1.1 est disponible ici: super_sudoku_v1.1.sfc

Changements depuis la version précédente:
  • L'arrière plan de la grille est désormais opaque
  • La couleur de la grille est plus sombre
  • L'animation du fond est ralenti pour être moins distrayante.
Si vous aimez, n'hésitez-pas à me faire un petit don via ma page ko-fi:
https://ko-fi.com/raphnet

Ou encore, en achetant le jeu sur itch.io:



Le ROM de la version 1.0 est disponible ici: super_sudoku_v1.0.sfc

Changements depuis la version précédente:
  • Il y a maintenant des messages dans la boîte au bas de l'écran
  • Le pointeur est placé à un endroit aléatoire lors de la sélection du puzzle



Section 19: Code source

J'ai publié le code source en février 2022 sur github:

https://github.com/raphnet/super_sudoku


Conclusion

Ceci termine mon projet pour le RetroChallenge 2019/03. Comme par le passé, le fait de réaliser un projet dans le câdre d'un événement limité dans le temps a été une grande source de motivation. J'ai atteint tous les buts que je m'étais initialement fixés de même que mes buts additionels à l'exception d'avoir une musique d'ambiance que je n'ai malheureusement pas fait. Mais dans l'ensemble, ce projet a été amusant, instructif et très satisfaisant.

Ce mois de programmation pour SNES m'a beaucoup appris sur le fonctionnement de cette console, mais j'ai encore tant à essayer et à apprendre! Je crois que ceci ne devrait pas être mon dernier projet pour SNES.

Sur ce, au revoir et à la prochaine!