/* * CookieCode - apprendre l'algorithmique avec des recettes de cuisine. * Microlibrairie sans dépendance, compatible navigateur, CDN et Node/CommonJS. */ ((root, factory) => { if (typeof module === "object" && module.exports) { module.exports = factory(); } else { root.CookieCode = factory(); } })(typeof globalThis !== "undefined" ? globalThis : this, () => { var configuration = { sortie: "console", afficherNotions: true, }; function configurer(options) { options = options || {}; if (Object.hasOwn(options, "sortie")) { configuration.sortie = options.sortie; } if (Object.hasOwn(options, "afficherNotions")) { configuration.afficherNotions = Boolean(options.afficherNotions); } return CookieCode; } function texte(valeur, defaut) { if (valeur === undefined || valeur === null || valeur === "") { return defaut || ""; } return String(valeur); } function echapperHTML(valeur) { return texte(valeur) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function creerElement(type, nom, quantite, unite, valeur) { if (!nom && nom !== 0) { throw new Error( "CookieCode : un nom est obligatoire pour creer un " + type + ".", ); } return { type: type, nom: texte(nom), quantite: quantite, unite: unite, valeur: valeur, }; } function ingredient(nom, quantite, unite) { return creerElement("ingredient", nom, quantite, unite); } function materiel(nom) { return creerElement("materiel", nom); } function constante(nom, valeur) { return creerElement("constante", nom, undefined, undefined, valeur); } function preparation(nom) { return { type: "preparation", nom: texte(nom, "preparation"), etat: {}, definir: function (cle, valeur) { this.etat[cle] = valeur; return this; }, est: function (valeur) { return ( this.etat.texture === valeur || this.etat.etat === valeur || this.etat[valeur] === true ); }, }; } function nomDe(element) { if (element && typeof element === "object" && element.nom !== undefined) { return texte(element.nom); } return texte(element); } function quantiteDe(element, quantite, unite) { var q = quantite !== undefined ? quantite : element?.quantite; var u = unite !== undefined ? unite : element?.unite; if (q === undefined || q === null || q === "") { return ""; } return texte(q) + (u ? " " + texte(u) : ""); } function formatElement(element, quantite, unite) { var quantiteTexte = quantiteDe(element, quantite, unite); var nom = nomDe(element); return quantiteTexte ? quantiteTexte + " de " + nom : nom; } function conditionLisible(condition) { if (typeof condition === "string") { return condition; } if (typeof condition === "boolean") { return condition ? "condition vraie" : "condition fausse"; } if (typeof condition === "function") { return "condition fournie"; } return "condition"; } function evaluerCondition(condition) { if (typeof condition === "function") { return Boolean(condition()); } if (typeof condition === "boolean") { return condition; } return true; } function creerEtape(texteEtape, notion, enfants) { return { texte: texteEtape, notion: notion, enfants: enfants || [], }; } function Recette(nom) { if (!nom && nom !== 0) { throw new Error("CookieCode : une recette doit avoir un nom."); } this.nom = texte(nom); this.etapes = []; this.notions = { programme: true, variables: [], constantes: [], fonctions: [], boucles: false, conditions: false, sequence: true, }; } Recette.prototype._noterFonction = function (nom) { if (this.notions.fonctions.indexOf(nom) === -1) { this.notions.fonctions.push(nom); } }; Recette.prototype._noterVariable = function (nom) { if (nom && this.notions.variables.indexOf(nom) === -1) { this.notions.variables.push(nom); } }; Recette.prototype._noterConstante = function (nom) { if (nom && this.notions.constantes.indexOf(nom) === -1) { this.notions.constantes.push(nom); } }; Recette.prototype._ajouterEtape = function (texteEtape, notion, enfants) { this.etapes.push(creerEtape(texteEtape, notion, enfants)); return this; }; Recette.prototype.prendre = function (nom, quantite, unite) { this._noterVariable(nomDe(nom)); this._noterFonction("prendre"); return this._ajouterEtape( "Prendre " + formatElement(nom, quantite, unite) + ".", "variable", ); }; Recette.prototype.utiliser = function (outil) { this._noterConstante(nomDe(outil)); this._noterFonction("utiliser"); return this._ajouterEtape("Utiliser " + nomDe(outil) + ".", "constante"); }; Recette.prototype.ajouter = function (element, quantite, unite) { this._noterFonction("ajouter"); if (element && element.type === "ingredient") { this._noterVariable(element.nom); } return this._ajouterEtape( "Ajouter " + formatElement(element, quantite, unite) + ".", "fonction", ); }; Recette.prototype.retirer = function (element) { this._noterFonction("retirer"); return this._ajouterEtape("Retirer " + nomDe(element) + ".", "fonction"); }; Recette.prototype.couper = function (element, forme) { this._noterFonction("couper"); return this._ajouterEtape( "Couper " + nomDe(element) + (forme ? " en " + texte(forme) : "") + ".", "fonction", ); }; Recette.prototype.melanger = function () { this._noterFonction("melanger"); return this._ajouterEtape("Melanger.", "fonction"); }; Recette.prototype.verser = function (contenu, contenant) { this._noterFonction("verser"); return this._ajouterEtape( "Verser " + nomDe(contenu) + (contenant ? " dans " + nomDe(contenant) : "") + ".", "fonction", ); }; Recette.prototype.chauffer = function (element, temperature) { this._noterFonction("chauffer"); return this._ajouterEtape( "Chauffer " + nomDe(element) + (temperature ? " a " + nomDe(temperature) : "") + ".", "fonction", ); }; Recette.prototype.cuire = function (duree, unite) { this._noterFonction("cuire"); return this._ajouterEtape( "Cuire" + (duree ? " pendant " + texte(duree) + (unite ? " " + texte(unite) : "") : "") + ".", "fonction", ); }; Recette.prototype.attendre = function (duree, unite) { this._noterFonction("attendre"); return this._ajouterEtape( "Attendre " + texte(duree) + (unite ? " " + texte(unite) : "") + ".", "fonction", ); }; Recette.prototype.repeter = function (nombre, action) { this.notions.boucles = true; this._noterFonction("repeter"); var bloc = new Recette("bloc repeter"); if (typeof action === "function") { action(bloc); } this._fusionnerNotions(bloc); return this._ajouterEtape( "Repeter " + texte(nombre) + " fois :", "boucle", bloc.etapes, ); }; Recette.prototype.si = function (condition, action, sinonAction) { this.notions.conditions = true; this._noterFonction("si"); var bloc = new Recette("bloc si"); var blocSinon = new Recette("bloc sinon"); var conditionVraie = evaluerCondition(condition); if (conditionVraie && typeof action === "function") { action(bloc); } if (!conditionVraie && typeof sinonAction === "function") { sinonAction(blocSinon); } this._fusionnerNotions(bloc); this._fusionnerNotions(blocSinon); var enfants = bloc.etapes; if (blocSinon.etapes.length > 0) { enfants = enfants.concat([ creerEtape("Sinon :", "condition", blocSinon.etapes), ]); } return this._ajouterEtape( "Si " + conditionLisible(condition) + " :", "condition", enfants, ); }; Recette.prototype.servir = function (element) { this._noterFonction("servir"); this._ajouterEtape( "Servir" + (element ? " " + nomDe(element) : "") + ".", "fonction", ); return this.afficher(); }; Recette.prototype.afficher = function () { var cible; if (configuration.sortie === "console" || !configuration.sortie) { if (typeof console !== "undefined" && console.log) { console.log(this.toText()); } return this; } if ( typeof configuration.sortie === "string" && typeof document !== "undefined" ) { cible = document.querySelector(configuration.sortie); if (cible) { cible.innerHTML = this.toHTML(); } } return this; }; Recette.prototype._fusionnerNotions = function (autreRecette) { autreRecette.notions.variables.forEach((nom) => { this._noterVariable(nom); }); autreRecette.notions.constantes.forEach((nom) => { this._noterConstante(nom); }); autreRecette.notions.fonctions.forEach((nom) => { this._noterFonction(nom); }); this.notions.boucles = this.notions.boucles || autreRecette.notions.boucles; this.notions.conditions = this.notions.conditions || autreRecette.notions.conditions; }; Recette.prototype._lignesEtapes = function (etapes, prefixe, indentation) { var lignes = []; etapes.forEach(function (etape, index) { var numero = prefixe ? prefixe + "." + (index + 1) : String(index + 1); lignes.push(indentation + numero + ". " + etape.texte); if (etape.enfants && etape.enfants.length > 0) { lignes = lignes.concat( this._lignesEtapes(etape.enfants, numero, indentation + " "), ); } }, this); 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 = [ "", "Notions utilisees :", "- Recette = programme", "- Sequence = les etapes sont executees dans l'ordre", ]; if (this.notions.variables.length > 0) { lignes.push( "- Ingredients = variables : " + this.notions.variables.join(", "), ); } if (this.notions.constantes.length > 0) { lignes.push( "- Materiel / valeurs fixes = constantes : " + this.notions.constantes.join(", "), ); } if (this.notions.fonctions.length > 0) { lignes.push( "- Actions = fonctions : " + this.notions.fonctions.join(", "), ); } if (this.notions.boucles) { lignes.push("- Repeter = boucle"); } if (this.notions.conditions) { lignes.push("- Si = condition"); } return lignes; }; Recette.prototype.toText = function () { var lignes = ["Recette : " + this.nom, "", "Etapes :"]; lignes = lignes.concat(this._lignesEtapes(this.etapes, "", "")); if (configuration.afficherNotions) { lignes = lignes.concat(this._lignesNotions()); } return lignes.join("\n"); }; Recette.prototype.toHTML = function () { var html = '"; 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 = "