Phase 2 : hasard + opérations mathématiques
Build and deploy / test-python (push) Successful in 1m26s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

screens/labels/data_operations.py : 6 nouvelles opérations pour
"Modifier une donnée"/"Modifier une variable" — multiplier, diviser,
modulo (garde-fou division par zéro : valeur inchangée plutôt qu'une
ZeroDivisionError qui interromprait le graphe), minimum/maximum (borne
la valeur ACTUELLE — utile pour une variable globale, qui n'a pas de
min_value/max_value comme un champ d'objet), et alea (tire un nombre
aléatoire entre deux bornes).

screens/data_actions/compute_operation.py (déjà factorisé en Phase 0,
donc une seule implémentation pour apply_data_action.py/
apply_variable_action.py) : implémente les 6. "alea" est la seule à
deux opérandes — réutilise data_value au format "min,max" plutôt qu'une
nouvelle colonne de nœud (bornes remises dans l'ordre si inversées).
random.uniform pour un résultat décimal, random.randint pour un entier.

static/js/screen_edit/flow-editor.js + templates/screen_edit.html :
petit indice visuel — le champ "Valeur / montant" du formulaire de nœud
affiche "min,max (ex. 1,6)" quand "alea" est choisi, pour ne pas laisser
deviner ce format à deux nombres, différent de toutes les autres
opérations. Sinon aucun nouveau champ/changement de schéma nécessaire,
le <select> était déjà généré depuis DATA_OPERATIONS.

Vérifié : 250 tests passent (19 dans test_compute_operation.py, dont un
qui a dû être corrigé — il utilisait "multiplier" comme exemple
d'opération INCONNUE, devenu un mauvais exemple maintenant qu'elle
existe), 13 tests node:test toujours au vert, syntaxe JS validée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-30 16:47:59 +02:00
co-authored by Claude Sonnet 5
parent e9d12945a9
commit 7476ed229e
5 changed files with 135 additions and 6 deletions
+46
View File
@@ -1,3 +1,6 @@
import random
def compute_new_value(operation, current, raw_value, is_decimal): def compute_new_value(operation, current, raw_value, is_decimal):
"""Calcule la nouvelle valeur d'un champ d'objet ou d'une variable """Calcule la nouvelle valeur d'un champ d'objet ou d'une variable
globale pour une opération de DATA_OPERATIONS (voir globale pour une opération de DATA_OPERATIONS (voir
@@ -32,4 +35,47 @@ def compute_new_value(operation, current, raw_value, is_decimal):
amount = -amount amount = -amount
result = (current or 0) + amount result = (current or 0) + amount
return result if is_decimal else int(result) return result if is_decimal else int(result)
# Phase 2 — hasard + opérations mathématiques. "multiplier"/"diviser"/
# "modulo" reprennent le même patron qu'incrementer/decrementer (un
# seul opérande numérique dans raw_value) ; "minimum"/"maximum"
# bornent la valeur ACTUELLE (utile pour une variable globale, qui n'a
# pas de min_value/max_value comme un champ d'objet, voir
# _clamp_to_field_bounds dans apply_data_action.py) ; "alea" est la
# seule opération à deux opérandes ("min,max" dans raw_value plutôt
# qu'une nouvelle colonne de nœud — voir updateDataValuePlaceholder()
# dans static/js/screen_edit/flow-editor.js).
if operation == "multiplier":
try:
factor = float(raw_value) if raw_value not in (None, "") else 1
except (TypeError, ValueError):
factor = 1
result = (current or 0) * factor
return result if is_decimal else int(result)
if operation in ("diviser", "modulo"):
try:
divisor = float(raw_value) if raw_value not in (None, "") else 0
except (TypeError, ValueError):
divisor = 0
if divisor == 0:
# Garde-fou division par zéro : valeur inchangée plutôt qu'une
# ZeroDivisionError qui interromprait tout le graphe de logique.
return current
result = (current or 0) / divisor if operation == "diviser" else (current or 0) % divisor
return result if is_decimal else int(result)
if operation in ("minimum", "maximum"):
try:
bound = float(raw_value)
except (TypeError, ValueError):
return current
result = max(current or 0, bound) if operation == "minimum" else min(current or 0, bound)
return result if is_decimal else int(result)
if operation == "alea":
parts = (raw_value or "").split(",")
try:
lo, hi = float(parts[0]), float(parts[1])
except (IndexError, TypeError, ValueError):
lo, hi = 0, 0
if lo > hi:
lo, hi = hi, lo
return random.uniform(lo, hi) if is_decimal else random.randint(int(lo), int(hi))
raise ValueError(f"opération inconnue : {operation!r}") raise ValueError(f"opération inconnue : {operation!r}")
+8
View File
@@ -8,5 +8,13 @@ DATA_OPERATIONS = [
("basculer_bool", "Basculer Vrai / Faux"), ("basculer_bool", "Basculer Vrai / Faux"),
("incrementer", "Augmenter de..."), ("incrementer", "Augmenter de..."),
("decrementer", "Diminuer de..."), ("decrementer", "Diminuer de..."),
# Phase 2 (hasard + opérations mathématiques) — voir
# screens/data_actions/compute_operation.py pour le calcul de chacune.
("multiplier", "Multiplier par..."),
("diviser", "Diviser par..."),
("modulo", "Reste de la division par... (modulo)"),
("minimum", "Ne jamais descendre sous..."),
("maximum", "Ne jamais dépasser..."),
("alea", "Tirer un nombre aléatoire entre... (min,max)"),
] ]
DATA_OPERATION_LABELS = dict(DATA_OPERATIONS) DATA_OPERATION_LABELS = dict(DATA_OPERATIONS)
+13
View File
@@ -457,6 +457,19 @@ function toggleFlowActionFields() {
document.getElementById('nf-action-element-property-wrap').style.display = isOnglet ? 'none' : 'block'; document.getElementById('nf-action-element-property-wrap').style.display = isOnglet ? 'none' : 'block';
if (typeSel.value === 'modifier_element') toggleFlowElementPropertyValue(); if (typeSel.value === 'modifier_element') toggleFlowElementPropertyValue();
if (typeSel.value === 'modifier_donnee') onFlowDefinitionChange('action'); if (typeSel.value === 'modifier_donnee') onFlowDefinitionChange('action');
updateDataValuePlaceholder('nf-action-data-operation', 'nf-action-data-value');
updateDataValuePlaceholder('nf-action-variable-operation', 'nf-action-variable-value');
}
// "alea" (Phase 2 — hasard) attend "min,max" dans data_value (voir
// screens/data_actions/compute_operation.py) plutôt qu'un seul nombre
// comme les autres opérations — indice visuel dans le champ plutôt que
// d'ajouter un champ dédié (aucun changement de schéma nécessaire).
function updateDataValuePlaceholder(operationSelectId, valueInputId) {
var opSel = document.getElementById(operationSelectId);
var valueInp = document.getElementById(valueInputId);
if (!opSel || !valueInp) return;
valueInp.placeholder = opSel.value === 'alea' ? 'min,max (ex. 1,6)' : 'Valeur / montant';
} }
function toggleFlowElementPropertyValue() { function toggleFlowElementPropertyValue() {
+2 -2
View File
@@ -419,7 +419,7 @@
</select> </select>
<select id="nf-action-data-row" style="margin-top:6px;"></select> <select id="nf-action-data-row" style="margin-top:6px;"></select>
<select id="nf-action-data-field" style="margin-top:6px;"></select> <select id="nf-action-data-field" style="margin-top:6px;"></select>
<select id="nf-action-data-operation" style="margin-top:6px;"> <select id="nf-action-data-operation" style="margin-top:6px;" onchange="updateDataValuePlaceholder('nf-action-data-operation', 'nf-action-data-value')">
{% for key, label in data_operations %} {% for key, label in data_operations %}
<option value="{{ key }}">{{ label }}</option> <option value="{{ key }}">{{ label }}</option>
{% endfor %} {% endfor %}
@@ -437,7 +437,7 @@
<option value="{{ v.name }}">{{ v.name }}</option> <option value="{{ v.name }}">{{ v.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<select id="nf-action-variable-operation" style="margin-top:6px;"> <select id="nf-action-variable-operation" style="margin-top:6px;" onchange="updateDataValuePlaceholder('nf-action-variable-operation', 'nf-action-variable-value')">
{% for key, label in data_operations %} {% for key, label in data_operations %}
<option value="{{ key }}">{{ label }}</option> <option value="{{ key }}">{{ label }}</option>
{% endfor %} {% endfor %}
+66 -4
View File
@@ -1,9 +1,9 @@
"""Tests unitaires de compute_new_value (screens/data_actions/compute_operation.py) """Tests unitaires de compute_new_value (screens/data_actions/compute_operation.py)
— logique partagée par apply_data_action.py (champ d'objet) et — logique partagée par apply_data_action.py (champ d'objet) et
apply_variable_action.py (variable globale), factorisée pour ne plus être apply_variable_action.py (variable globale), factorisée pour ne plus être
dupliquée dans les deux fichiers avant que la Phase 2 (hasard/opérations dupliquée dans les deux fichiers. Verrouille le comportement des 7
mathématiques) n'y ajoute 6 nouvelles opérations. Verrouille le opérations historiques ET des 6 opérations ajoutées en Phase 2 (hasard +
comportement des 7 opérations existantes.""" opérations mathématiques)."""
import pytest import pytest
from screens.data_actions.compute_operation import compute_new_value from screens.data_actions.compute_operation import compute_new_value
@@ -51,4 +51,66 @@ def test_incrementer_valeur_invalide_ne_change_rien():
def test_operation_inconnue_leve_value_error(): def test_operation_inconnue_leve_value_error():
with pytest.raises(ValueError): with pytest.raises(ValueError):
compute_new_value("multiplier", 10, "2", False) compute_new_value("teleporter", 10, "2", False)
# ---------- Phase 2 — hasard + opérations mathématiques ----------
def test_multiplier():
assert compute_new_value("multiplier", 10, "3", False) == 30
assert compute_new_value("multiplier", 4.0, "2.5", True) == 10.0
def test_diviser():
assert compute_new_value("diviser", 10, "2", False) == 5
assert compute_new_value("diviser", 5.0, "2", True) == 2.5
def test_diviser_par_zero_ne_change_rien():
assert compute_new_value("diviser", 10, "0", False) == 10
def test_modulo():
assert compute_new_value("modulo", 10, "3", False) == 1
assert compute_new_value("modulo", 10, "0", False) == 10 # garde-fou division par zéro
def test_minimum_releve_la_valeur_si_en_dessous():
assert compute_new_value("minimum", 3, "10", False) == 10
assert compute_new_value("minimum", 15, "10", False) == 15 # déjà au-dessus : inchangé
def test_maximum_plafonne_la_valeur_si_au_dessus():
assert compute_new_value("maximum", 15, "10", False) == 10
assert compute_new_value("maximum", 3, "10", False) == 3 # déjà en dessous : inchangé
def test_minimum_maximum_valeur_invalide_ne_change_rien():
assert compute_new_value("minimum", 5, "pas un nombre", False) == 5
assert compute_new_value("maximum", 5, "", False) == 5
def test_alea_entier_dans_les_bornes():
for _ in range(50):
result = compute_new_value("alea", 0, "1,6", False)
assert isinstance(result, int)
assert 1 <= result <= 6
def test_alea_decimal_dans_les_bornes():
for _ in range(50):
result = compute_new_value("alea", 0, "1.5,2.5", True)
assert isinstance(result, float)
assert 1.5 <= result <= 2.5
def test_alea_bornes_inversees_sont_remises_dans_lordre():
for _ in range(50):
result = compute_new_value("alea", 0, "6,1", False)
assert 1 <= result <= 6
def test_alea_valeur_invalide_retombe_sur_zero():
assert compute_new_value("alea", 0, "pas un nombre", False) == 0
assert compute_new_value("alea", 0, "", False) == 0
assert compute_new_value("alea", 0, "5", False) == 0 # une seule borne : incomplet