From f4fea7324a4e397b2f38169347ef72973b893b48 Mon Sep 17 00:00:00 2001 From: kazerlelutin Date: Tue, 21 Jul 2026 22:29:22 +0200 Subject: [PATCH] Add challenge feedback API --- README.md | 58 +++++++++++ dist/cookiecode.js | 187 +++++++++++++++++++++++++++++++++++- examples/demo.js | 12 ++- features/cookiecode.feature | 12 +++ package.json | 2 +- src/cookiecode.js | 187 +++++++++++++++++++++++++++++++++++- test/cookiecode.test.js | 48 ++++++++- 7 files changed, 500 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c8dbe97..b41f971 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ CookieCode.ingredient("farine", 200, "g"); CookieCode.materiel("saladier"); CookieCode.constante("temperature", 180); CookieCode.preparation("pate"); +CookieCode.defi("Cookie avec boucle", { ingredientsMinimum: 3 }); ``` ### Methodes d'une recette @@ -81,6 +82,7 @@ CookieCode.preparation("pate"); .afficher() .toText() .toHTML() +.analyser() ``` ## Exemple avec variables @@ -117,6 +119,62 @@ CookieCode.recette("Pate a cookies") .servir(); ``` +## Feedback ludique + +CookieCode peut analyser une recette et verifier si elle respecte un defi. + +```js +const recette = CookieCode.recette("Cookie") + .prendre("farine", 200, "g") + .prendre("sucre", 100, "g") + .prendre("chocolat", 150, "g") + .ajouter("farine") + .ajouter("sucre") + .ajouter("chocolat") + .melanger() + .repeter(2, function (recette) { + recette.melanger(); + }) + .cuire(12, "minutes") + .servir(); + +const defi = CookieCode.defi("Cookie avec boucle", { + ingredientsMinimum: 3, + actionsObligatoires: ["melanger", "cuire", "servir"], + boucleObligatoire: true, + terminerParServir: true, + interdireErreurs: true +}); + +const verification = CookieCode.verifier(recette, defi); +console.log(verification.toText()); +``` + +Sortie possible : + +```txt +Defi : Cookie avec boucle +Resultat : reussi +✅ Tu as utilise 3 ingredient(s). +✅ Action trouvee : melanger. +✅ Action trouvee : cuire. +✅ Action trouvee : servir. +✅ Tu as utilise une boucle avec repeter. +✅ Ta recette se termine par servir. +✅ Aucune erreur logique detectee. +``` + +Une recette peut aussi etre analysee directement : + +```js +const rapport = recette.analyser(); + +console.log(rapport.ingredients); +console.log(rapport.actions); +console.log(rapport.utiliseBoucle); +console.log(rapport.erreurs); +``` + ## Cartes pedagogiques conseillees | Type de carte | Mots cles | Notion | diff --git a/dist/cookiecode.js b/dist/cookiecode.js index 1f263a6..7f5a5c4 100644 --- a/dist/cookiecode.js +++ b/dist/cookiecode.js @@ -381,6 +381,47 @@ return lignes; }; + Recette.prototype.analyser = function () { + var actions = this.notions.fonctions.slice(); + var etapesAPlates = aplatirEtapes(this.etapes); + var derniereAction = + etapesAPlates.length > 0 + ? actionDeTexte(etapesAPlates[etapesAPlates.length - 1].texte) + : ""; + var erreurs = []; + + if (this.etapes.length === 0) { + erreurs.push("Ta recette est vide : ajoute au moins une etape."); + } + + if (this.etapes.length > 0 && derniereAction !== "servir") { + erreurs.push("Ta recette ne se termine pas par servir."); + } + + etapesAPlates.forEach((etape, index) => { + if ( + actionDeTexte(etape.texte) === "servir" && + index !== etapesAPlates.length - 1 + ) { + erreurs.push("Tu as servi avant la fin de la recette."); + } + }); + + return { + nom: this.nom, + ingredients: this.notions.variables.slice(), + variables: this.notions.variables.slice(), + materiels: this.notions.constantes.slice(), + constantes: this.notions.constantes.slice(), + actions: actions, + nombreEtapes: etapesAPlates.length, + utiliseBoucle: this.notions.boucles, + utiliseCondition: this.notions.conditions, + termineParServir: derniereAction === "servir", + erreurs: erreurs, + }; + }; + Recette.prototype._lignesNotions = function () { var lignes = [ "", @@ -446,6 +487,148 @@ return html; }; + function aplatirEtapes(etapes) { + var resultat = []; + etapes.forEach((etape) => { + resultat.push(etape); + if (etape.enfants && etape.enfants.length > 0) { + resultat = resultat.concat(aplatirEtapes(etape.enfants)); + } + }); + return resultat; + } + + function actionDeTexte(texteEtape) { + var index; + var valeur = texte(texteEtape).toLowerCase(); + var actions = [ + "prendre", + "utiliser", + "ajouter", + "retirer", + "couper", + "melanger", + "verser", + "chauffer", + "cuire", + "attendre", + "repeter", + "si", + "servir", + ]; + + for (index = 0; index < actions.length; index += 1) { + if (valeur.indexOf(actions[index]) === 0) { + return actions[index]; + } + } + + return ""; + } + + function defi(nom, regles) { + if (!nom && nom !== 0) { + throw new Error("CookieCode : un defi doit avoir un nom."); + } + + return { + type: "defi", + nom: texte(nom), + regles: regles || {}, + }; + } + + function verifier(recetteAVerifier, defiAVerifier) { + var rapport = recetteAVerifier.analyser(); + var regles = defiAVerifier?.regles ? defiAVerifier.regles : {}; + var messages = []; + var reussi = true; + + function valider(condition, succes, echec) { + if (condition) { + messages.push("✅ " + succes); + } else { + reussi = false; + messages.push("❌ " + echec); + } + } + + if (regles.ingredientsMinimum !== undefined) { + valider( + rapport.ingredients.length >= regles.ingredientsMinimum, + "Tu as utilise " + rapport.ingredients.length + " ingredient(s).", + "Il faut au moins " + regles.ingredientsMinimum + " ingredient(s).", + ); + } + + if (regles.materielsMinimum !== undefined) { + valider( + rapport.materiels.length >= regles.materielsMinimum, + "Tu as utilise " + rapport.materiels.length + " materiel(s).", + "Il faut au moins " + regles.materielsMinimum + " materiel(s).", + ); + } + + (regles.actionsObligatoires || []).forEach((action) => { + valider( + rapport.actions.indexOf(action) !== -1, + "Action trouvee : " + action + ".", + "Action manquante : " + action + ".", + ); + }); + + if (regles.boucleObligatoire) { + valider( + rapport.utiliseBoucle, + "Tu as utilise une boucle avec repeter.", + "Il manque une boucle : essaie avec .repeter().", + ); + } + + if (regles.conditionObligatoire) { + valider( + rapport.utiliseCondition, + "Tu as utilise une condition avec si.", + "Il manque une condition : essaie avec .si().", + ); + } + + if (regles.terminerParServir) { + valider( + rapport.termineParServir, + "Ta recette se termine par servir.", + "Ta recette doit se terminer par .servir().", + ); + } + + if (regles.interdireErreurs) { + valider( + rapport.erreurs.length === 0, + "Aucune erreur logique detectee.", + rapport.erreurs.join(" ") || "Une erreur logique a ete detectee.", + ); + } + + if (messages.length === 0) { + messages.push("ℹ️ Aucun critere de defi a verifier."); + } + + return { + reussi: reussi, + nom: defiAVerifier ? defiAVerifier.nom : "Defi", + messages: messages, + rapport: rapport, + toText: function () { + return [ + "Defi : " + this.nom, + this.reussi ? "Resultat : reussi" : "Resultat : a ameliorer", + ] + .concat(this.messages) + .join("\n"); + }, + }; + } + function etapesHTML(etapes) { var html = "
    "; etapes.forEach((etape) => { @@ -470,7 +653,9 @@ materiel: materiel, constante: constante, preparation: preparation, - version: "0.1.0", + defi: defi, + verifier: verifier, + version: "0.2.0", }; return CookieCode; diff --git a/examples/demo.js b/examples/demo.js index 01e5cf0..66e23ba 100644 --- a/examples/demo.js +++ b/examples/demo.js @@ -3,7 +3,7 @@ CookieCode.configurer({ afficherNotions: true, }); -CookieCode.recette("Cookie au chocolat") +const recette = CookieCode.recette("Cookie au chocolat") .prendre("farine", 200, "g") .prendre("sucre", 100, "g") .prendre("chocolat", 150, "g") @@ -17,3 +17,13 @@ CookieCode.recette("Cookie au chocolat") }) .cuire(12, "minutes") .servir(); + +const defi = CookieCode.defi("Cookie avec boucle", { + ingredientsMinimum: 3, + actionsObligatoires: ["melanger", "cuire", "servir"], + boucleObligatoire: true, + terminerParServir: true, + interdireErreurs: true +}); + +console.log(CookieCode.verifier(recette, defi).toText()); diff --git a/features/cookiecode.feature b/features/cookiecode.feature index ba653a1..3a3b83d 100644 --- a/features/cookiecode.feature +++ b/features/cookiecode.feature @@ -24,3 +24,15 @@ Feature: Apprendre l'algorithmique avec CookieCode Then la recette affiche l'etape conditionnelle And les actions du bloc conditionnel sont affichees And la lecture algorithmique indique que si est une condition + + Scenario: Verifier un defi ludique + Given un defi demande au moins trois ingredients, une boucle et une fin avec servir + When l'apprenant verifie une recette qui respecte ces criteres + Then CookieCode indique que le defi est reussi + And CookieCode affiche des messages de validation positifs + + Scenario: Aider a ameliorer une recette incomplete + Given un defi demande au moins trois ingredients, une boucle et une fin avec servir + When l'apprenant verifie une recette incomplete + Then CookieCode indique que le resultat est a ameliorer + And CookieCode explique les criteres manquants diff --git a/package.json b/package.json index 08030d9..a568e59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cookiecode", - "version": "0.1.0", + "version": "0.2.0", "description": "Microlibrairie educative pour apprendre l'algorithmique avec des recettes de cuisine.", "main": "dist/cookiecode.js", "scripts": { diff --git a/src/cookiecode.js b/src/cookiecode.js index 1f263a6..7f5a5c4 100644 --- a/src/cookiecode.js +++ b/src/cookiecode.js @@ -381,6 +381,47 @@ return lignes; }; + Recette.prototype.analyser = function () { + var actions = this.notions.fonctions.slice(); + var etapesAPlates = aplatirEtapes(this.etapes); + var derniereAction = + etapesAPlates.length > 0 + ? actionDeTexte(etapesAPlates[etapesAPlates.length - 1].texte) + : ""; + var erreurs = []; + + if (this.etapes.length === 0) { + erreurs.push("Ta recette est vide : ajoute au moins une etape."); + } + + if (this.etapes.length > 0 && derniereAction !== "servir") { + erreurs.push("Ta recette ne se termine pas par servir."); + } + + etapesAPlates.forEach((etape, index) => { + if ( + actionDeTexte(etape.texte) === "servir" && + index !== etapesAPlates.length - 1 + ) { + erreurs.push("Tu as servi avant la fin de la recette."); + } + }); + + return { + nom: this.nom, + ingredients: this.notions.variables.slice(), + variables: this.notions.variables.slice(), + materiels: this.notions.constantes.slice(), + constantes: this.notions.constantes.slice(), + actions: actions, + nombreEtapes: etapesAPlates.length, + utiliseBoucle: this.notions.boucles, + utiliseCondition: this.notions.conditions, + termineParServir: derniereAction === "servir", + erreurs: erreurs, + }; + }; + Recette.prototype._lignesNotions = function () { var lignes = [ "", @@ -446,6 +487,148 @@ return html; }; + function aplatirEtapes(etapes) { + var resultat = []; + etapes.forEach((etape) => { + resultat.push(etape); + if (etape.enfants && etape.enfants.length > 0) { + resultat = resultat.concat(aplatirEtapes(etape.enfants)); + } + }); + return resultat; + } + + function actionDeTexte(texteEtape) { + var index; + var valeur = texte(texteEtape).toLowerCase(); + var actions = [ + "prendre", + "utiliser", + "ajouter", + "retirer", + "couper", + "melanger", + "verser", + "chauffer", + "cuire", + "attendre", + "repeter", + "si", + "servir", + ]; + + for (index = 0; index < actions.length; index += 1) { + if (valeur.indexOf(actions[index]) === 0) { + return actions[index]; + } + } + + return ""; + } + + function defi(nom, regles) { + if (!nom && nom !== 0) { + throw new Error("CookieCode : un defi doit avoir un nom."); + } + + return { + type: "defi", + nom: texte(nom), + regles: regles || {}, + }; + } + + function verifier(recetteAVerifier, defiAVerifier) { + var rapport = recetteAVerifier.analyser(); + var regles = defiAVerifier?.regles ? defiAVerifier.regles : {}; + var messages = []; + var reussi = true; + + function valider(condition, succes, echec) { + if (condition) { + messages.push("✅ " + succes); + } else { + reussi = false; + messages.push("❌ " + echec); + } + } + + if (regles.ingredientsMinimum !== undefined) { + valider( + rapport.ingredients.length >= regles.ingredientsMinimum, + "Tu as utilise " + rapport.ingredients.length + " ingredient(s).", + "Il faut au moins " + regles.ingredientsMinimum + " ingredient(s).", + ); + } + + if (regles.materielsMinimum !== undefined) { + valider( + rapport.materiels.length >= regles.materielsMinimum, + "Tu as utilise " + rapport.materiels.length + " materiel(s).", + "Il faut au moins " + regles.materielsMinimum + " materiel(s).", + ); + } + + (regles.actionsObligatoires || []).forEach((action) => { + valider( + rapport.actions.indexOf(action) !== -1, + "Action trouvee : " + action + ".", + "Action manquante : " + action + ".", + ); + }); + + if (regles.boucleObligatoire) { + valider( + rapport.utiliseBoucle, + "Tu as utilise une boucle avec repeter.", + "Il manque une boucle : essaie avec .repeter().", + ); + } + + if (regles.conditionObligatoire) { + valider( + rapport.utiliseCondition, + "Tu as utilise une condition avec si.", + "Il manque une condition : essaie avec .si().", + ); + } + + if (regles.terminerParServir) { + valider( + rapport.termineParServir, + "Ta recette se termine par servir.", + "Ta recette doit se terminer par .servir().", + ); + } + + if (regles.interdireErreurs) { + valider( + rapport.erreurs.length === 0, + "Aucune erreur logique detectee.", + rapport.erreurs.join(" ") || "Une erreur logique a ete detectee.", + ); + } + + if (messages.length === 0) { + messages.push("ℹ️ Aucun critere de defi a verifier."); + } + + return { + reussi: reussi, + nom: defiAVerifier ? defiAVerifier.nom : "Defi", + messages: messages, + rapport: rapport, + toText: function () { + return [ + "Defi : " + this.nom, + this.reussi ? "Resultat : reussi" : "Resultat : a ameliorer", + ] + .concat(this.messages) + .join("\n"); + }, + }; + } + function etapesHTML(etapes) { var html = "
      "; etapes.forEach((etape) => { @@ -470,7 +653,9 @@ materiel: materiel, constante: constante, preparation: preparation, - version: "0.1.0", + defi: defi, + verifier: verifier, + version: "0.2.0", }; return CookieCode; diff --git a/test/cookiecode.test.js b/test/cookiecode.test.js index 33a9888..7ddabda 100644 --- a/test/cookiecode.test.js +++ b/test/cookiecode.test.js @@ -1,7 +1,7 @@ const assert = require("assert"); const CookieCode = require("../dist/cookiecode.js"); -CookieCode.configurer({ sortie: null, afficherNotions: true }); +CookieCode.configurer({ sortie: "test", afficherNotions: true }); const texteRecette = CookieCode.recette("Cookie au chocolat") .prendre("farine", 200, "g") @@ -48,6 +48,50 @@ assert.ok(!html.includes(" { + recette.melanger(); + }) + .cuire(12, "minutes") + .servir(); + +const rapport = recetteDefi.analyser(); +assert.deepStrictEqual(rapport.ingredients, ["farine", "sucre", "chocolat"]); +assert.ok(rapport.actions.includes("repeter")); +assert.strictEqual(rapport.utiliseBoucle, true); +assert.strictEqual(rapport.termineParServir, true); +assert.deepStrictEqual(rapport.erreurs, []); + +const defiCookie = CookieCode.defi("Cookie avec boucle", { + ingredientsMinimum: 3, + actionsObligatoires: ["melanger", "cuire", "servir"], + boucleObligatoire: true, + terminerParServir: true, + interdireErreurs: true, +}); + +const verification = CookieCode.verifier(recetteDefi, defiCookie); +assert.strictEqual(verification.reussi, true); +assert.match(verification.toText(), /Resultat : reussi/); +assert.match(verification.toText(), /✅ Tu as utilise 3 ingredient\(s\)\./); + +const recetteIncomplete = CookieCode.recette("Cookie incomplet") + .prendre("farine", 200, "g") + .melanger(); + +const verificationIncomplete = CookieCode.verifier(recetteIncomplete, defiCookie); +assert.strictEqual(verificationIncomplete.reussi, false); +assert.match(verificationIncomplete.toText(), /❌ Il faut au moins 3 ingredient\(s\)\./); +assert.match(verificationIncomplete.toText(), /❌ Il manque une boucle : essaie avec \.repeter\(\)\./); +assert.match(verificationIncomplete.toText(), /❌ Ta recette doit se terminer par \.servir\(\)\./); + +assert.strictEqual(CookieCode.version, "0.2.0"); console.log("CookieCode tests: OK");