feat(skill-mh): skill "sort/attaque à cible" (Vampirisme testé sur un Sphinx)

mh_action_target.py gère le flux différent des formulaires classiques
(Play_action.php avec ai_ToDo -> redirection vers une page <select
id_target> -> POST Play_a_ActionResult.php avec le token cp frais).
Dry-run par défaut, --execute seulement sur confirmation explicite.

Testé en conditions réelles : Vampirisme (ai_ToDo=203) sur "un Sphinx
[Adulte]" - sortilège réussi, 108 dégâts infligés, 2 PX gagnés, 4 PA.
This commit is contained in:
perco
2026-07-06 09:10:56 +02:00
parent 6f8f89e08e
commit 8da0953241
2 changed files with 181 additions and 0 deletions
+59
View File
@@ -166,6 +166,65 @@ changeant N coûtant 1 PA de plus.
--- ---
## 4. Skill "Lancer un sort/attaque spéciale sur une cible" (ex: Vampirisme)
Fichier additionnel : `mh_action_target.py`.
Certaines actions (sorts, attaques spéciales) ne sont pas de simples boutons
sur la page "Lieu" : elles redirigent vers une page intermédiaire qui affiche
un `<select name="id_target">` listant les cibles disponibles sur la case
(monstres et/ou trolls). Le flux est différent de `mh_submit.py` (qui rejoue
un `<form>` classique) — voir `../mh/LLM_TUTORIAL.md` section 6 pour le détail
HTTP complet.
Table des `ai_ToDo` connus (identifiant numérique de l'action) :
| ai_ToDo | Action | PA |
|---|---|---|
| `3` | Attaque Normale | 4 |
| `107` | Frénésie | 6 |
| `149` | Lancer de Créatures | 4 |
| `203` | Vampirisme | 4 |
| `228` | Griffe du Sorcier | 4 |
### Cycle pour "lance [sort] sur [cible]"
**Étape A — Vérifier la cible (sans rien soumettre)**
```bash
.venv/bin/python3 mh_action_target.py <ai_ToDo> "sous-texte de la cible"
```
Exemple : `.venv/bin/python3 mh_action_target.py 203 "sphinx"`
Renvoie un JSON `dry_run: true` avec la cible trouvée (recherche insensible à
la casse et aux accents approximatifs — si aucun résultat, le script renvoie
la liste de toutes les cibles présentes sur la case pour comparaison, utile
si Perco s'est trompé d'orthographe, ex. "sphynx" au lieu de "Sphinx").
**Étape B — Rapporter à Perco et demander confirmation**
> "Je vois **[cible_choisie.label]** sur la case. Veux-tu que je lance
> [nom du sort] dessus (coût [PA] PA) ?"
**Étape C — Attendre le "oui" explicite**, comme pour toute autre action.
**Étape D — Si confirmé, exécuter réellement**
```bash
.venv/bin/python3 mh_action_target.py <ai_ToDo> "sous-texte de la cible" --execute
```
Le script relit une page fraîche (token `cp` à jour) avant de soumettre.
Renvoie le texte complet du résultat du jeu (réussite/échec, dégâts, PX
gagnés, PA consommés). Résume ce résultat à Perco.
**Piège évité** : ne jamais chercher la cible "à l'œil" dans un texte déjà lu
précédemment — toujours relire une page fraîche juste avant `--execute`, le
token `cp` change à chaque chargement et une cible peut disparaître/changer
entre deux tours si un monstre a bougé ou est mort entre-temps.
**Testé et validé le 2026-07-06** : Vampirisme (`ai_ToDo=203`) sur "un Sphinx
[Adulte]" — sortilège réussi (jet 60/85%), cible touchée (130 vs esquive 72),
108 dégâts infligés (100 PV réels après armure), 2 PX gagnés, 4 PA dépensés.
---
## Référence complémentaire ## Référence complémentaire
Le repo `mh` (projet compagnon MountyHall de Perco, plus large) contient Le repo `mh` (projet compagnon MountyHall de Perco, plus large) contient
+122
View File
@@ -0,0 +1,122 @@
"""Lance une action a cible (sort/attaque speciale) sur un monstre/troll de la
case courante : Vampirisme, Griffe du Sorcier, etc. (cf. table ai_ToDo dans
../mh/LLM_TUTORIAL.md).
Flux (different de mh_submit.py, qui lui rejoue un <form> vu sur une page
"Lieu" classique) :
1. GET Play_action.php?as_Action=ACTION!&ai_ToDo=<id> -> redirige (302) vers
Play_a_Action.php?type=S&id=<id_sort>, qui affiche un <select name="id_target">
des cibles sur la case.
2. Filtre les cibles par un sous-texte de leur libellé (insensible a la casse).
3. Lit le token anti-CSRF `cp` (change a chaque chargement, jamais reutiliser
un ancien token).
4. Si --execute : POST Play_a_ActionResult.php avec id_target + cp (consomme
des PA reellement). Sinon (dry-run par defaut) : affiche juste ce qui
serait fait, ne soumet rien.
Usage:
python3 mh_action_target.py <ai_ToDo> "<sous-texte de la cible>" [--execute]
Exemple (Vampirisme = ai_ToDo 203, sur un monstre "sphynx") :
python3 mh_action_target.py 203 "sphynx" --dry-run
python3 mh_action_target.py 203 "sphynx" --execute
REGLE ABSOLUE : ne jamais appeler avec --execute sans un "oui" explicite et
recent de Perco pour CETTE cible precise. --execute consomme reellement des
PA en jeu (irreversible).
"""
import json
import sys
from bs4 import BeautifulSoup
from mh_common import BASE, get_session
def load_target_page(session, ai_todo):
r = session.get(
f"{BASE}/Play_action.php",
params={"as_Action": "ACTION!", "ai_ToDo": str(ai_todo)},
allow_redirects=True,
)
soup = BeautifulSoup(r.text, "html.parser")
cp_input = soup.find("input", {"name": "cp"})
type_input = soup.find("input", {"name": "type"})
id_input = soup.find("input", {"name": "id"})
select = soup.find("select", {"name": "id_target"})
if not select:
return {"erreur": "Aucun <select name='id_target'> trouve sur la page de cible", "html_extrait": r.text[:1500]}, None
cibles = [
(o["value"], o.get_text(strip=True))
for o in select.find_all("option")
if o.get("value")
]
info = {
"cp": cp_input["value"] if cp_input else None,
"type": type_input["value"] if type_input else None,
"id": id_input["value"] if id_input else None,
"cibles": [{"value": v, "label": l} for v, l in cibles],
}
return info, r.text
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python3 mh_action_target.py <ai_ToDo> \"sous-texte cible\" [--execute]", file=sys.stderr)
sys.exit(1)
ai_todo = sys.argv[1]
needle = sys.argv[2].lower()
execute = "--execute" in sys.argv
session = get_session()
info, raw_html = load_target_page(session, ai_todo)
if "erreur" in info:
print(json.dumps(info, ensure_ascii=False, indent=2))
sys.exit(1)
matches = [c for c in info["cibles"] if needle in c["label"].lower()]
if not matches:
print(json.dumps({
"erreur": f"Aucune cible ne contient '{sys.argv[2]}'",
"cibles_disponibles": info["cibles"],
}, ensure_ascii=False, indent=2))
sys.exit(1)
target = matches[0]
if not execute:
print(json.dumps({
"dry_run": True,
"action_type": info["type"],
"action_id": info["id"],
"cible_choisie": target,
"cp": info["cp"],
"message": "Rien soumis (pas de --execute). Confirme avec Perco avant de relancer avec --execute.",
}, ensure_ascii=False, indent=2))
sys.exit(0)
payload = {
"as_Action": "ACTION !!",
"type": info["type"],
"id": info["id"],
"id_target": target["value"],
"cp": info["cp"],
}
r = session.post(f"{BASE}/Play_a_ActionResult.php", data=payload)
soup = BeautifulSoup(r.text, "html.parser")
for tag in soup.find_all(["nav", "script", "style"]):
tag.decompose()
main = soup.find("main") or soup
print(json.dumps({
"cible_visee": target,
"resultat": main.get_text("\n", strip=True),
}, ensure_ascii=False, indent=2))