diff --git a/skill-mh/TP.md b/skill-mh/TP.md index f936d0e..d27f8ec 100644 --- a/skill-mh/TP.md +++ b/skill-mh/TP.md @@ -38,6 +38,8 @@ embarque son propre pip, voir `requirements.txt`.) texte de son bouton (ex: `"Traverser le Portail"`). Refait un login + une lecture fraîche de la page juste avant de soumettre, pour toujours utiliser un token anti-CSRF (`cp`) à jour. +- `mh_vue.py` / `mh_move_toward.py` — voir section 3 ci-dessous (déplacement + vers une cible visible dans la Vue). --- @@ -105,6 +107,65 @@ jeu, jamais de mémoire/supposition. --- +## 3. Skill "Se déplacer vers une cible" (Déplacement Éclair) + +Fichiers additionnels : +- `mh_vue.py` — liste les monstres (ou trolls/trésors/champignons/lieux) + visibles dans la Vue du troll, triés par distance, filtrables par nom. + Usage : `.venv/bin/python3 mh_vue.py monstres "Djinn"` +- `mh_move_toward.py` — calcule le trajet en compétence **Déplacement Éclair** + (`ai_ToDo=112`) pour s'approcher d'une cible trouvée par `mh_vue.py`, et + ne l'exécute QUE si demandé explicitement (`--execute`). + +Le Déplacement Éclair déplace le troll d'au plus 1 case sur X, Y **et** N en +une seule utilisation (1 PA, +1 PA si le N/noeud change ce pas-ci). Il faut +donc plusieurs utilisations successives pour s'approcher d'une cible loin. + +### Cycle pour "va vers X" (ex: "le Djinn le plus proche") + +**Étape A — Calculer le plan (sans rien soumettre)** +```bash +.venv/bin/python3 mh_move_toward.py "Djinn" --dry-run +``` +Renvoie un JSON : position actuelle, cible trouvée (la plus proche dont le nom +contient le filtre), `nb_pas` nécessaires pour arriver adjacent à la cible, et +`pa_estime` (coût total en Points d'Action). + +**Étape B — Rapporter à Perco et demander confirmation** +> "Le [nom de la cible] le plus proche est à [dist] cases (position X,Y,N). +> Il faudrait [nb_pas] utilisations de Déplacement Éclair, pour un coût +> d'environ [pa_estime] PA. Veux-tu que je m'en approche ?" + +**Étape C — Attendre le "oui" explicite**, comme pour toute autre action. + +**Étape D — Si confirmé, exécuter réellement** +```bash +.venv/bin/python3 mh_move_toward.py "Djinn" --execute +``` +Le script relit un token `cp` frais à chaque pas (obligatoire, le token +change à chaque chargement), s'arrête dès que la cible devient adjacente ou +si un pas échoue (PA insuffisants, etc.), et renvoie le résultat de chaque +pas. Résume ce résultat à Perco. + +**Piège rencontré en le testant** : la page "Lieu" (`Play_a_Action.php? +type=L`) ne renvoie AUCUNE position quand le troll n'est pas sur un lieu +spécial (elle affiche juste "Vous n'êtes pas sur un lieu"). La position +fiable dans tous les cas vient de `Play_vue2.php`, attribut +`data-position="{...}"` sur `
  • ` — c'est ce que `mh_move_toward.py` +utilise (`get_position()`). + +**Autre piège** : l'API `json_vue.php` qui liste monstres/trolls/trésors +renvoie la chaîne littérale `"KO"` (pas du JSON) si les en-têtes +`X-Requested-With: XMLHttpRequest` et `Referer` (vers `Play_vue2.php`) ne +sont pas envoyés. + +**Limite connue** : `dist` (distance de Chebyshev sur X/Y, renvoyée par +`mh_vue.py`) ne tient pas compte du N (noeud/dimension) — le coût réel en PA +peut donc être supérieur au nombre de pas si N diffère beaucoup, chaque pas +changeant N coûtant 1 PA de plus. + +--- + ## Référence complémentaire Le repo `mh` (projet compagnon MountyHall de Perco, plus large) contient diff --git a/skill-mh/mh_move_toward.py b/skill-mh/mh_move_toward.py new file mode 100644 index 0000000..cacbd38 --- /dev/null +++ b/skill-mh/mh_move_toward.py @@ -0,0 +1,167 @@ +"""Calcule (et, seulement sur confirmation, execute) le trajet en 'Deplacement +Eclair' (competence, ai_ToDo=112) pour s'approcher d'une cible (monstre/troll/ +tresor) trouvee par mh_vue.py. + +Le Deplacement Eclair deplace le troll d'AU PLUS 1 case sur CHAQUE axe (X, Y, +N) a la fois, par utilisation (radio boutons -1/0/+1 sur Play_action.php? +as_Action=ACTION!&ai_ToDo=112). Une utilisation coute 1 PA ; +1 PA +supplementaire si le N (noeud/dimension) change lors de ce pas. + +Usage (mode CALCUL SEUL, ne soumet rien) : + python3 mh_move_toward.py "Djinn" --dry-run + +Usage (mode EXECUTION, ne JAMAIS lancer sans confirmation explicite de Perco +pour CETTE cible precise) : + python3 mh_move_toward.py "Djinn" --execute + +Le script : +1. Recupere la position actuelle du troll (page Lieu). +2. Cherche la cible la plus proche dont le nom contient (categorie + 'monstres' par defaut) via mh_vue.py. +3. Calcule le nombre de pas necessaires pour arriver ADJACENT a la cible + (distance de Chebyshev <= 1 en X/Y), et le cout total en PA (en tenant + compte du nombre de pas ou N change). +4. En --dry-run (ou par defaut) : affiche seulement le plan (JSON), ne + soumet AUCUNE requete de deplacement. +5. En --execute : soumet les pas un par un (relit un token cp frais a + chaque pas), s'arrete des que la cible est adjacente (dist<=1) ou si le + nombre de pas prevus est atteint, et affiche le resultat de chaque pas. +""" +import json +import re +import sys + +from mh_common import BASE, get_session +from mh_vue import fetch_category, get_troll_id, parse_entries + +DE_TODO = "112" +DE_TYPE = "C" +DE_ID = "12" + + +def get_position(session): + """Lit la position actuelle depuis Play_vue2.php (data-position sur
  • ). + Contrairement a la page Lieu (Play_a_Action.php?type=L), cette page fonctionne + meme quand le troll n'est PAS sur un lieu special ('Vous n'etes pas sur un lieu').""" + r = session.get(f"{BASE}/Play_vue2.php") + m = re.search(r'data-position="([^"]+)"', r.text) + if not m: + raise RuntimeError("Impossible de lire la position actuelle sur Play_vue2.php") + import html as html_module + pos = json.loads(html_module.unescape(m.group(1))) + return pos["x"], pos["y"], pos["n"] + + +def sign(v): + return (v > 0) - (v < 0) + + +def plan_route(x0, y0, n0, xt, yt, nt): + """Renvoie la liste des pas (dx,dy,dn) a effectuer, un par appel de DE, + jusqu'a etre adjacent (chebyshev X/Y <= 1). Chaque pas rapproche d'au + plus 1 sur chaque axe simultanement.""" + steps = [] + x, y, n = x0, y0, n0 + while max(abs(xt - x), abs(yt - y)) > 1: + dx = sign(xt - x) + dy = sign(yt - y) + dn = sign(nt - n) + steps.append((dx, dy, dn)) + x, y, n = x + dx, y + dy, n + dn + if len(steps) > 200: # garde-fou anti boucle infinie + break + return steps + + +def estimate_pa(steps): + total = 0 + for dx, dy, dn in steps: + total += 1 + if dn != 0: + total += 1 + return total + + +def fetch_de_form(session): + """Recupere la page de la competence (token cp frais + valeurs radio actuelles).""" + r = session.get( + f"{BASE}/Play_action.php", + params={"as_Action": "ACTION!", "ai_ToDo": DE_TODO}, + ) + m_cp = re.search(r'name="cp" value="([^"]+)"', r.text) + if not m_cp: + raise RuntimeError("Token cp introuvable sur la page Deplacement Eclair") + return m_cp.group(1) + + +def submit_step(session, dx, dy, dn): + cp = fetch_de_form(session) + payload = { + "as_Action": "ACTION !!", + "type": DE_TYPE, + "id": DE_ID, + "cp": cp, + "depl_x": str(dx), + "depl_y": str(dy), + "depl_n": str(dn), + } + r = session.post(f"{BASE}/Play_a_ActionResult.php", data=payload) + return r.text + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python3 mh_move_toward.py \"filtre nom cible\" [--dry-run|--execute] [categorie]", file=sys.stderr) + sys.exit(1) + + filtre = sys.argv[1] + mode = "--dry-run" + categorie = "monstres" + for arg in sys.argv[2:]: + if arg in ("--dry-run", "--execute"): + mode = arg + else: + categorie = arg + + session = get_session() + x0, y0, n0 = get_position(session) + + idtroll = get_troll_id(session) + raw = fetch_category(session, idtroll, categorie) + entries = parse_entries(raw) + needle = filtre.lower() + candidats = [e for e in entries if needle in e["nom"].lower()] + + if not candidats: + print(json.dumps({"erreur": f"Aucune cible contenant '{filtre}' trouvee dans la Vue"}, ensure_ascii=False)) + sys.exit(1) + + cible = candidats[0] # deja trie par dist croissante + steps = plan_route(x0, y0, n0, cible["x"], cible["y"], cible["n"]) + pa_estime = estimate_pa(steps) + + plan = { + "position_actuelle": {"x": x0, "y": y0, "n": n0}, + "cible": cible, + "nb_pas": len(steps), + "pa_estime": pa_estime, + "pas_detailles": [{"depl_x": s[0], "depl_y": s[1], "depl_n": s[2]} for s in steps], + } + + if mode == "--dry-run": + print(json.dumps(plan, ensure_ascii=False, indent=2)) + sys.exit(0) + + # --execute : soumet reellement les pas, un par un + resultats = [] + for i, (dx, dy, dn) in enumerate(steps, 1): + html_resultat = submit_step(session, dx, dy, dn) + # texte simplifie du resultat (premieres lignes utiles) + texte = re.sub(r"<[^>]+>", " ", html_resultat) + texte = re.sub(r"\s+", " ", texte).strip() + resultats.append({"pas": i, "depl": [dx, dy, dn], "resultat_resume": texte[:500]}) + # arret anticipe si le message indique un probleme (PA insuffisants, etc.) + if "insuffisant" in texte.lower() or "erreur" in texte.lower(): + break + + print(json.dumps({"plan": plan, "execution": resultats}, ensure_ascii=False, indent=2)) diff --git a/skill-mh/mh_vue.py b/skill-mh/mh_vue.py new file mode 100644 index 0000000..ca3ad3c --- /dev/null +++ b/skill-mh/mh_vue.py @@ -0,0 +1,81 @@ +"""Liste les monstres (ou trolls/tresors/champignons/lieux) visibles dans la Vue du troll. + +Usage: + python3 mh_vue.py [categorie] [filtre] + +categorie : monstres (defaut) | trolls | tresors | champignons | lieux | cenotaphes +filtre : sous-chaine (insensible a la casse) a chercher dans le nom, ex "Djinn" + +Sortie JSON: liste triee par distance croissante, chaque entree : + {"id":..., "nom":..., "x":..., "y":..., "n":..., "dist":...} + +`dist` est la distance de Chebyshev (max(|dx|,|dy|)) calculee par le jeu et +NE tient PAS compte de la difference de N (noeud/dimension) - deux cases avec +le meme X/Y mais un N different peuvent avoir dist=0 alors qu'elles ne sont +pas sur le meme "plan". Voir mh_move_toward.py pour le cout reel en PA qui +lui prend N en compte. + +PIEGE : l'endpoint json_vue.php renvoie la chaine "KO" (pas du JSON) si les +en-tetes HTTP suivants ne sont pas envoyes : X-Requested-With: XMLHttpRequest +et un Referer pointant vers Play_vue2.php. +""" +import json +import re +import sys + +from mh_common import get_session + +VUE_BASE = "https://mh2.mh.raistlin.fr/mountyhall/MH_PageUtils/Services/json_vue.php" + + +def get_troll_id(session): + r = session.get("https://mh2.mh.raistlin.fr/mountyhall/MH_Play/Play_vue2.php") + m = re.search(r'data-idtroll="(\d+)"', r.text) + if not m: + raise RuntimeError("Impossible de trouver l'idtroll sur Play_vue2.php") + return m.group(1) + + +def fetch_category(session, idtroll, categorie="monstres"): + headers = { + "X-Requested-With": "XMLHttpRequest", + "Referer": "https://mh2.mh.raistlin.fr/mountyhall/MH_Play/Play_vue2.php", + "Accept": "application/json, text/javascript, */*; q=0.01", + } + r = session.get(VUE_BASE, params={"w": categorie, "t": idtroll}, headers=headers) + if r.text.strip() == "KO": + raise RuntimeError("Le serveur a renvoye KO - session invalide ou en-tetes manquants") + return r.json() + + +def parse_entries(raw): + out = [] + for entry in raw: + v = entry["value"] + label = re.sub(r"<[^>]+>", "", v.get("nom", {}).get("value", "")).strip() + out.append({ + "id": v.get("id"), + "nom": label, + "x": v.get("x"), + "y": v.get("y"), + "n": v.get("n"), + "dist": v.get("dist"), + }) + out.sort(key=lambda e: e["dist"] if e["dist"] is not None else 999999) + return out + + +if __name__ == "__main__": + categorie = sys.argv[1] if len(sys.argv) > 1 else "monstres" + filtre = sys.argv[2] if len(sys.argv) > 2 else None + + session = get_session() + idtroll = get_troll_id(session) + raw = fetch_category(session, idtroll, categorie) + entries = parse_entries(raw) + + if filtre: + needle = filtre.lower() + entries = [e for e in entries if needle in e["nom"].lower()] + + print(json.dumps(entries, ensure_ascii=False, indent=2))