feat(sandbox): add carre program

This commit is contained in:
Guillaume ARM 2026-06-09 09:15:07 +02:00
parent ccd3dfef7b
commit dea9bd52ee
6 changed files with 544 additions and 0 deletions

177
PLAN.md Normal file
View File

@ -0,0 +1,177 @@
# Programme `carre` Plan
## Objectif
Ajouter un nouveau programme ComputerCraft nommé `carre` qui dessine des carres dans le terminal.
Le programme doit etre utile pour un test simple, mais assez amusant pour explorer plusieurs rendus: carre fixe, carre plein, carre aleatoire, et repetition de carres.
Le code doit aussi pouvoir servir d'exemple pedagogique pour debutants Lua. L'implementation pourra donc contenir quelques commentaires en francais, courts et utiles, pour expliquer les etapes importantes sans noyer le programme.
## Emplacement
- Programme: `programs/carre.lua`
- Tests: `tests/programs/carre_test.lua` ou `tests/carre_test.lua`, selon les conventions existantes au moment de l'implementation
- Package: nouveau package separe `trapos-sandbox`, afin de tester `ccpm` sans inclure `carre` directement dans `trapos`
## Interface Proposee
Commande de base:
```text
carre [options]
```
Options:
- `-size <n>`: taille du carre, par defaut `8`
- `-x <n>`: colonne de depart, par defaut centree si possible
- `-y <n>`: ligne de depart, par defaut centree si possible
- `-char <c>`: caractere utilise pour dessiner, par defaut `#`
- `-fill`: dessine un carre plein au lieu d'un contour
- `-random`: choisit une taille, une position et un caractere aleatoires simples compatibles avec le terminal
- `-count <n>`: dessine plusieurs carres, utile avec `-random`, par defaut `1`
- `-delay <s>`: pause entre deux carres quand `-count` est superieur a `1`, par defaut `0`
- `-clear`: efface l'ecran avant de dessiner
- `-help` / `--help`: affiche l'aide
- `-version` / `--version`: affiche la version du package via `/apis/libversion`
Exemples:
```text
carre
carre -size 12 -char * -clear
carre -size 6 -fill -x 4 -y 3
carre -random
carre -random -count 5 -delay 0.2 -clear
```
## Comportement Attendu
- Le programme dessine dans le terminal courant avec `term.setCursorPos()` et `write()`.
- Si `-clear` est donne, il appelle `term.clear()` avant le dessin.
- Sans `-x` ou `-y`, le carre est centre dans la zone disponible quand la taille du terminal le permet.
- Les coordonnees et tailles sont bornees pour eviter les erreurs ou les dessins hors ecran.
- `-size 1` dessine un seul caractere.
- `-size 2` dessine un carre minimal valide.
- Si `-fill` est absent, seul le contour est dessine.
- Si `-fill` est present, toute la surface est remplie.
- Si `-random` est present, les options explicites doivent rester prioritaires quand c'est raisonnable: par exemple `carre -random -char @` garde `@` mais randomise taille/position.
- Si `-count` est superieur a `1`, chaque carre est dessine successivement sur le meme ecran par defaut. Avec `-clear`, l'ecran est efface avant chaque carre pour produire une petite animation.
- Les couleurs ne sont pas prioritaires pour la premiere version: l'objectif est de garder un programme lisible pour debutants.
## Regles De Validation Des Arguments
- `-size` doit etre un entier positif.
- `-x` et `-y` doivent etre des entiers positifs.
- `-count` doit etre un entier positif.
- `-delay` doit etre un nombre positif ou nul.
- `-char` prend le premier caractere non vide fourni.
- Option inconnue: afficher une erreur courte puis suggerer `carre -help`.
- Argument invalide: afficher une erreur courte puis quitter sans stack trace.
## Idee D'Implementation
Structure minimale dans `programs/carre.lua`:
1. Charger `/apis/libversion` pour `--version`.
2. Definir `printUsage()`.
3. Parser `...` avec une boucle simple sur `args`.
4. Recuperer `term.getSize()`.
5. Calculer une configuration finale de dessin.
6. Dessiner avec une fonction locale `drawSquare(x, y, size, char, fill)`.
7. Repeter selon `count`, avec `sleep(delay)` si necessaire.
Commentaires a privilegier dans le code:
- Expliquer pourquoi on lit `...` pour recuperer les arguments du programme.
- Expliquer le calcul de centrage.
- Expliquer la difference entre contour et remplissage.
- Expliquer que le mode aleatoire borne les valeurs pour rester visible dans le terminal.
Commentaires a eviter:
- Commentaires qui repetent exactement le code, par exemple `-- ajoute 1 a x`.
- Gros blocs de theorie Lua qui rendraient le programme moins lisible.
Pseudo-rendu contour:
```text
########
# #
# #
# #
# #
# #
# #
########
```
Pseudo-rendu plein:
```text
########
########
########
########
```
## Tests Proposes
Ajouter des tests CraftOS-PC avec `/apis/libtest.lua` pour les parties deterministes:
- Parse des options de base.
- Rejet des valeurs invalides.
- Calcul de centrage avec une taille de terminal simulee.
- Calcul de bornage quand le carre depasse la zone disponible.
- Rendu contour dans une surface terminal simulee.
- Rendu plein dans une surface terminal simulee.
- Mode random teste avec une source aleatoire injectable ou avec des invariants seulement: taille positive, position visible, pas d'erreur.
Pour rendre le programme testable sans gros refactor, l'implementation peut garder les fonctions pures dans le fichier et n'executer `main(...)` qu'a la fin. Si les conventions du depot preferent tester seulement les programmes via execution CraftOS-PC, adapter les tests en consequence.
## Verification
Apres implementation:
```text
just check
just trapos --headless --exec 'shell.run("/programs/carre", "-size", "5", "-clear"); os.shutdown()'
just trapos --headless --exec 'shell.run("/programs/carre", "-random", "-count", "3", "-delay", "0"); os.shutdown()'
```
Si des tests sont ajoutes:
```text
just trapos --headless --exec 'shell.run("/programs/runtest", "/tests/carre_test.lua"); os.shutdown()'
```
## Packaging
- Creer le nouveau package `packages/trapos-sandbox/ccpm.json`.
- Ne pas ajouter `carre` au package principal `trapos`.
- Ajouter `programs/carre.lua` dans les fichiers du package `trapos-sandbox`.
- Bumper la version du package `trapos-sandbox` quand le comportement change.
- Reporter la meme version dans `packages/index.json`.
- Verifier que `carre --version` retourne bien la version attendue.
## Decisions Validees
- `carre` doit etre dans un nouveau package separe, pas dans `trapos`, pour faciliter les tests de `ccpm`.
- Le package s'appelle `trapos-sandbox`.
- Le programme s'appelle `carre.lua`, sans accent.
- Les commentaires en francais sont souhaites pour rendre le code utile a des debutants.
- Les couleurs sont repoussees: le programme doit rester simple et pedagogique pour la premiere version.
## Questions Restantes
- `carre -random -count N` doit-il garder tous les carres visibles par defaut, ou faut-il finalement effacer entre chaque dessin meme sans `-clear`?
## Hors Scope Pour La Premiere Version
- Dessin de rectangles non carres.
- Animation avancee avec rebonds.
- Interface interactive au clavier.
- Sauvegarde du dessin dans un fichier.
- Support multi-caracteres pour les bordures.
- Couleurs de texte ou de fond.

171
apis/libcarre.lua Normal file
View File

@ -0,0 +1,171 @@
local DEFAULT_SIZE = 8;
local DEFAULT_CHAR = '#';
local RANDOM_CHARS = { '#', '*', '+', 'x', '@', '=' };
local function firstChar(value)
value = tostring(value or '');
if value == '' then return nil; end
return string.sub(value, 1, 1);
end
local function parsePositiveInteger(value, name)
local number = tonumber(value);
if not number or number < 1 or number ~= math.floor(number) then
return nil, name .. ' doit etre un entier positif';
end
return number;
end
local function parseNonNegativeNumber(value, name)
local number = tonumber(value);
if not number or number < 0 then
return nil, name .. ' doit etre un nombre positif ou nul';
end
return number;
end
local function clamp(value, minValue, maxValue)
if value < minValue then return minValue; end
if value > maxValue then return maxValue; end
return value;
end
local function createCarre(opts)
opts = opts or {};
local random = opts.random or math.random;
local api = {};
function api.parseArgs(args)
args = args or {};
local config = {
size = DEFAULT_SIZE,
char = DEFAULT_CHAR,
fill = false,
random = false,
count = 1,
delay = 0,
clear = false,
explicit = {},
};
local i = 1;
while i <= (args.n or #args) do
local arg = args[i];
if arg == '-size' then
local value, err = parsePositiveInteger(args[i + 1], '-size');
if not value then return nil, err; end
config.size = value;
config.explicit.size = true;
i = i + 1;
elseif arg == '-x' then
local value, err = parsePositiveInteger(args[i + 1], '-x');
if not value then return nil, err; end
config.x = value;
config.explicit.x = true;
i = i + 1;
elseif arg == '-y' then
local value, err = parsePositiveInteger(args[i + 1], '-y');
if not value then return nil, err; end
config.y = value;
config.explicit.y = true;
i = i + 1;
elseif arg == '-char' then
local value = firstChar(args[i + 1]);
if not value then return nil, '-char doit recevoir un caractere'; end
config.char = value;
config.explicit.char = true;
i = i + 1;
elseif arg == '-fill' then
config.fill = true;
elseif arg == '-random' then
config.random = true;
elseif arg == '-count' then
local value, err = parsePositiveInteger(args[i + 1], '-count');
if not value then return nil, err; end
config.count = value;
i = i + 1;
elseif arg == '-delay' then
local value, err = parseNonNegativeNumber(args[i + 1], '-delay');
if not value then return nil, err; end
config.delay = value;
i = i + 1;
elseif arg == '-clear' then
config.clear = true;
else
return nil, 'option inconnue: ' .. tostring(arg);
end
i = i + 1;
end
return config;
end
function api.computeSquare(config, width, height)
width = math.max(1, tonumber(width) or 1);
height = math.max(1, tonumber(height) or 1);
config = config or {};
local explicit = config.explicit or {};
local maxSize = math.max(1, math.min(width, height));
local size = config.size or DEFAULT_SIZE;
local char = config.char or DEFAULT_CHAR;
if config.random then
if not explicit.size then
size = random(1, maxSize);
end
if not explicit.char then
char = RANDOM_CHARS[random(1, #RANDOM_CHARS)];
end
end
size = clamp(size, 1, maxSize);
local maxX = math.max(1, width - size + 1);
local maxY = math.max(1, height - size + 1);
local x = config.x;
local y = config.y;
if config.random and not explicit.x then
x = random(1, maxX);
elseif not x then
x = math.floor((width - size) / 2) + 1;
end
if config.random and not explicit.y then
y = random(1, maxY);
elseif not y then
y = math.floor((height - size) / 2) + 1;
end
return {
x = clamp(x, 1, maxX),
y = clamp(y, 1, maxY),
size = size,
char = firstChar(char) or DEFAULT_CHAR,
fill = config.fill == true,
};
end
function api.drawSquare(termLib, square)
for row = 1, square.size do
termLib.setCursorPos(square.x, square.y + row - 1);
for column = 1, square.size do
local isBorder = row == 1 or row == square.size or column == 1 or column == square.size;
if square.fill or isBorder then
termLib.write(square.char);
else
termLib.write(' ');
end
end
end
end
return api;
end
return createCarre;

View File

@ -6,6 +6,7 @@
"trapos-net": "0.2.1", "trapos-net": "0.2.1",
"trapos-ui": "0.2.2", "trapos-ui": "0.2.2",
"trapos-ai": "0.5.1", "trapos-ai": "0.5.1",
"trapos-sandbox": "0.1.0",
"trapos": "0.6.2" "trapos": "0.6.2"
} }
} }

View File

@ -0,0 +1,11 @@
{
"name": "trapos-sandbox",
"version": "0.1.0",
"description": "TrapOS sandbox programs for ccpm experiments and Lua learning",
"dependencies": ["trapos-core"],
"files": [
"apis/libcarre.lua",
"programs/carre.lua"
],
"autostart": []
}

62
programs/carre.lua Normal file
View File

@ -0,0 +1,62 @@
local createCarre = require('/apis/libcarre');
local createVersion = require('/apis/libversion');
-- Les arguments du programme sont disponibles dans `...`.
-- `table.pack` permet de les garder dans une table facile a parcourir.
local args = table.pack(...);
local function printUsage()
print('carre usage:');
print();
print(' carre');
print(' carre -size <n> [-x <n>] [-y <n>] [-char <c>] [-fill] [-clear]');
print(' carre -random [-count <n>] [-delay <s>] [-clear]');
print(' carre --version');
print(' carre --help');
print();
print('exemples:');
print(' carre -size 10 -char * -clear');
print(' carre -size 6 -fill -x 4 -y 3');
print(' carre -random -count 5 -delay 0.2');
end
local command = args[1];
if command == '-help' or command == '--help' or command == 'help' then
printUsage();
return;
end
if command == '-version' or command == '--version' or command == 'version' then
print('v' .. createVersion().forSelf());
return;
end
local carre = createCarre();
local config, err = carre.parseArgs(args);
if not config then
print(err);
print('utilise: carre -help');
return;
end
local width, height = term.getSize();
for index = 1, config.count do
if config.clear then
term.clear();
end
-- Le calcul choisit une position visible. Si rien n'est donne,
-- le carre est centre dans le terminal.
local square = carre.computeSquare(config, width, height);
-- Le mode contour ecrit seulement les bords. Le mode plein ecrit partout.
carre.drawSquare(term.current(), square);
if config.delay > 0 and index < config.count then
sleep(config.delay);
end
end
term.setCursorPos(1, height);

122
tests/carre.lua Normal file
View File

@ -0,0 +1,122 @@
local createLibTest = require('/apis/libtest');
local createCarre = require('/apis/libcarre');
local testlib = createLibTest({ ... });
local function packed(...)
return table.pack(...);
end
local function fakeTerm(width, height)
local buffer = {};
local cursorX = 1;
local cursorY = 1;
for y = 1, height do
buffer[y] = {};
for x = 1, width do
buffer[y][x] = '.';
end
end
return {
setCursorPos = function(x, y)
cursorX = x;
cursorY = y;
end,
write = function(text)
text = tostring(text);
for i = 1, #text do
if buffer[cursorY] and buffer[cursorY][cursorX] then
buffer[cursorY][cursorX] = string.sub(text, i, i);
end
cursorX = cursorX + 1;
end
end,
line = function(y)
return table.concat(buffer[y], '');
end,
};
end
testlib.test('parseArgs accepts basic drawing options', function()
local carre = createCarre();
local config = carre.parseArgs(packed('-size', '5', '-x', '2', '-y', '3', '-char', '@', '-fill', '-clear'));
testlib.assertEquals(config.size, 5);
testlib.assertEquals(config.x, 2);
testlib.assertEquals(config.y, 3);
testlib.assertEquals(config.char, '@');
testlib.assertEquals(config.fill, true);
testlib.assertEquals(config.clear, true);
end);
testlib.test('parseArgs rejects invalid sizes', function()
local carre = createCarre();
local config, err = carre.parseArgs(packed('-size', '0'));
testlib.assertEquals(config, nil);
testlib.assertTrue(string.find(err, 'entier positif', 1, true));
end);
testlib.test('computeSquare centers and clamps the square', function()
local carre = createCarre();
local square = carre.computeSquare({ size = 4, char = '#', fill = false }, 10, 6);
testlib.assertEquals(square.x, 4);
testlib.assertEquals(square.y, 2);
testlib.assertEquals(square.size, 4);
square = carre.computeSquare({ size = 20, x = 99, y = 99, char = '#' }, 7, 5);
testlib.assertEquals(square.x, 3);
testlib.assertEquals(square.y, 1);
testlib.assertEquals(square.size, 5);
end);
testlib.test('computeSquare random keeps explicit character', function()
local values = { 3, 2, 4 };
local index = 0;
local carre = createCarre({
random = function(minValue, maxValue)
index = index + 1;
return math.max(minValue, math.min(maxValue, values[index]));
end,
});
local square = carre.computeSquare({
random = true,
char = '@',
explicit = { char = true },
}, 10, 8);
testlib.assertEquals(square.size, 3);
testlib.assertEquals(square.x, 2);
testlib.assertEquals(square.y, 4);
testlib.assertEquals(square.char, '@');
end);
testlib.test('drawSquare renders an outline', function()
local carre = createCarre();
local termLib = fakeTerm(8, 6);
carre.drawSquare(termLib, { x = 2, y = 2, size = 4, char = '#', fill = false });
testlib.assertEquals(termLib.line(1), '........');
testlib.assertEquals(termLib.line(2), '.####...');
testlib.assertEquals(termLib.line(3), '.# #...');
testlib.assertEquals(termLib.line(4), '.# #...');
testlib.assertEquals(termLib.line(5), '.####...');
end);
testlib.test('drawSquare renders a filled square', function()
local carre = createCarre();
local termLib = fakeTerm(6, 5);
carre.drawSquare(termLib, { x = 2, y = 2, size = 3, char = '*', fill = true });
testlib.assertEquals(termLib.line(2), '.***..');
testlib.assertEquals(termLib.line(3), '.***..');
testlib.assertEquals(termLib.line(4), '.***..');
end);
testlib.run();