#!/usr/bin/env python3 """ ╔══════════════════════════════════════════════════════════╗ ║ GÉO-RÉVISION — Tectonique des Plaques ║ ║ 76 notions · Chapitres 3, 4, 5 & Synthèse ║ ║ Méthode : Ebbinghaus + Testing Effect + Interleaving ║ ╚══════════════════════════════════════════════════════════╝ """ import curses import random import json import os import re import textwrap # ══════════════════════════════════════════════════════════ # DONNÉES — 76 cartes # ══════════════════════════════════════════════════════════ CARDS = [ # CH.3 — Limites de plaques {"id": 1, "ch": "3", "section": "CH.3 — Limites de plaques", "q": "Quels sont les 3 grands types de limites de plaques ?", "hint": "Pense : naissance / destruction / collision", "a": "1. DORSALES (divergence/accrétion oceanique)\n2. FOSSES OCEANIQUES (convergence/subduction)\n3. CHAINES DE MONTAGNES (convergence/collision)"}, {"id": 2, "ch": "3", "section": "CH.3 — Dorsales", "q": "Quelles roches sont produites à la dorsale ? Quelle est leur texture ?", "hint": "Deux roches, deux textures", "a": "BASALTE : texture microlithique (refroidissement rapide en surface)\nGABBRO : texture grenue (refroidissement lent en profondeur)"}, {"id": 3, "ch": "3", "section": "CH.3 — Dorsales", "q": "Quel type de failles caractérise les dorsales ? Pourquoi ?", "hint": "Extension ou compression ?", "a": "FAILLES NORMALES car la divergence crée une EXTENSION (étirement) de la croûte."}, {"id": 4, "ch": "3", "section": "CH.3 — Dorsales", "q": "Caractéristiques sismiques et thermiques d'une dorsale ?", "hint": "Profondeur + intensité + flux", "a": "Sismicité SUPERFICIELLE et FAIBLE MAGNITUDE.\nFlux géothermique ÉLEVÉ (magmatisme ascendant)."}, {"id": 5, "ch": "3", "section": "CH.3 — Fosses", "q": "Quelles roches plutoniques et volcaniques sont produites en subduction ?", "hint": "4 roches, 2 textures différentes", "a": "Plutoniques (grenues) : DIORITE, GRANITE\nVolcaniques (microlithiques) : ANDÉSITE, RHYOLITE"}, {"id": 6, "ch": "3", "section": "CH.3 — Fosses", "q": "Comment se répartissent les foyers sismiques en zone de subduction ? Jusqu'où ?", "hint": "Un plan célèbre...", "a": "Plan de WADATI-BENIOFF : plan incliné suivant la plaque plongeante.\nAtteint 700 KM de profondeur."}, {"id": 7, "ch": "3", "section": "CH.3 — Fosses", "q": "Décris le flux géothermique en zone de subduction (deux zones).", "hint": "Froid ici, chaud là-bas", "a": "BAS au niveau de la plaque froide qui plonge.\nFORT au niveau de l'arc volcanique sus-jacent."}, {"id": 8, "ch": "3", "section": "CH.3 — Collision", "q": "Caractéristiques géologiques et sismiques d'une zone de collision ?", "hint": "Type de failles + profondeur séismes", "a": "FAILLES INVERSES, flux géothermique FAIBLE,\nsismicité principalement SUPERFICIELLE."}, {"id": 9, "ch": "3", "section": "CH.3 — Points chauds", "q": "Comment un alignement de volcans inactifs prouve-t-il le mouvement des plaques ?", "hint": "Âge + distance", "a": "Le point chaud est FIXE dans le manteau profond.\nLa plaque se déplace : l'âge des volcans AUGMENTE avec la distance au volcan actif."}, {"id": 10, "ch": "3", "section": "CH.3 — Anomalies magnétiques", "q": "Qu'est-ce qu'une anomalie magnétique positive vs négative ?", "hint": "Intensité mesurée vs moyenne", "a": "POSITIVE : intensité > moyenne → polarité NORMALE\nNÉGATIVE : intensité < moyenne → polarité INVERSE (pôles inversés)"}, {"id": 11, "ch": "3", "section": "CH.3 — Anomalies magnétiques", "q": "Décris le patron des anomalies magnétiques autour d'une dorsale.", "hint": "Symétrie + image", "a": "Bandes PARALLÈLES ET SYMÉTRIQUES par rapport à l'axe de la dorsale.\nComparables à un DOUBLE TAPIS ROULANT.\nBasaltes d'autant plus vieux qu'ils sont éloignés."}, {"id": 12, "ch": "3", "section": "CH.3 — Anomalies magnétiques", "q": "Que révèle l'âge et l'épaisseur des sédiments sur le plancher océanique ?", "hint": "Loi simple distance/âge", "a": "Épaisseur et âge des sédiments AUGMENTENT avec la distance à la dorsale.\nPreuve de l'expansion océanique."}, {"id": 13, "ch": "3", "section": "CH.3 — Géodésie spatiale", "q": "Que permet la géodésie spatiale (GPS) en tectonique ?", "hint": "Précision + deux types de vitesses", "a": "Localisation au MILLIMÈTRE PRÈS.\nCalcul des vitesses ABSOLUES (repères fixes) et RELATIVES (entre deux plaques)."}, {"id": 14, "ch": "3", "section": "CH.3 — Résumé frontières", "q": "Quelle frontière présente une vallée axiale (rift) ? Quel mouvement ?", "hint": "Rift = fossé d'effondrement", "a": "La DORSALE : mouvement de DIVERGENCE.\nLa vallée axiale (rift) témoigne de l'extension de la croûte."}, {"id": 15, "ch": "3", "section": "CH.3 — Structure générale", "q": "Lithosphère terrestre : structure de base et activité aux frontières.", "hint": "Découpe + concentration", "a": "Découpée en PLAQUES LITHOSPHÉRIQUES RIGIDES.\nFrontières concentrent l'essentiel de l'ACTIVITÉ SISMIQUE ET VOLCANIQUE."}, {"id": 16, "ch": "3", "section": "CH.3 — Anomalies magnétiques", "q": "Comment les basaltes enregistrent-ils le champ magnétique ?", "hint": "Moment clé = refroidissement", "a": "En se REFROIDISSANT, les minéraux magnétiques (magnétite) se figent dans la direction du champ magnétique terrestre du moment."}, {"id": 17, "ch": "3", "section": "CH.3 — Dorsales", "q": "Que produit l'accrétion océanique à la dorsale ?", "hint": "Naît quoi ?", "a": "La CROÛTE OCÉANIQUE :\n- Basalte en surface\n- Gabbro en profondeur\n- Péridotite mantellique sous-jacente"}, {"id": 18, "ch": "3", "section": "CH.3 — Fosses", "q": "Qu'est-ce qu'une fosse océanique ? Quel processus y est associé ?", "hint": "Plongement...", "a": "Dépression profonde marquant une ZONE DE SUBDUCTION :\nplongement d'une plaque océanique dense dans le manteau."}, {"id": 19, "ch": "3", "section": "CH.3 — Collision", "q": "Qu'est-ce qu'une chaîne de montagnes en termes tectoniques ?", "hint": "Fermeture d'un océan", "a": "Zone de COLLISION CONTINENTALE : résultat de la convergence de deux plaques continentales après fermeture d'un océan."}, # CH.4 — Dorsales rapides {"id": 20, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "Quelle est la vitesse d'écartement d'une dorsale rapide ?", "hint": "10–16 cm/an — Quel océan ?", "a": "10 à 16 CM/AN.\nExemple : dorsale du Pacifique Est (Pacifique)."}, {"id": 21, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "Morphologie d'une dorsale rapide : rift, bombement ?", "hint": "Opposé de la dorsale lente", "a": "BOMBEMENT LARGE mais VALLÉE AXIALE QUASI ABSENTE (magmatisme abondant comble le rift)."}, {"id": 22, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "Décris le processus de fusion partielle sous une dorsale (étapes).", "hint": "Convection → décompression → solidus", "a": "1. Remontée des péridotites par CONVECTION\n2. DÉCOMPRESSION ADIABATIQUE : le géotherme recoupe le solidus\n3. FUSION PARTIELLE (~15%) → gouttelettes de magma"}, {"id": 23, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "Où se loge la chambre magmatique ? Quelles roches en sortent ?", "hint": "Profondeur + 2 roches", "a": "Entre 2 et 7 KM de profondeur.\nRefroidissement lent → GABBROS (grenu)\nRemontée + refroidissement rapide → BASALTES (microlithique)"}, {"id": 24, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "Quelle épaisseur de croûte produit une dorsale rapide ?", "hint": "Chiffre précis en km", "a": "Croûte océanique régulière de 5 À 7 KM d'épaisseur."}, {"id": 25, "ch": "4", "section": "CH.4 — Dorsales lentes", "q": "Quelle est la vitesse d'écartement d'une dorsale lente ?", "hint": "1–5 cm/an — Quel océan ?", "a": "1 à 5 CM/AN.\nExemple : dorsale médio-atlantique (Atlantique)."}, {"id": 26, "ch": "4", "section": "CH.4 — Dorsales lentes", "q": "Morphologie d'une dorsale lente : rift, magmatisme, expansion.", "hint": "Opposé de rapide", "a": "RIFT TRÈS MARQUÉ + grandes failles de détachement.\nMagmatisme RÉDUIT ET DISCONTINU.\nExpansion parfois assurée par ÉTIREMENT TECTONIQUE."}, {"id": 27, "ch": "4", "section": "CH.4 — Dorsales lentes", "q": "Qu'est-ce que la serpentinisation et quand se produit-elle ?", "hint": "Roche + eau + dorsale lente", "a": "Hydratation des PÉRIDOTITES (manteau) remontées en surface (manque de magmatisme).\nL'eau transforme l'olivine en SERPENTINE."}, {"id": 28, "ch": "4", "section": "CH.4 — Vieillissement", "q": "Qu'est-ce que le métamorphisme hydrothermal ?", "hint": "Eau + chaleur + minéraux", "a": "Transformation À L'ÉTAT SOLIDE des minéraux de la croûte océanique par infiltration d'eau de mer chauffée. Produit des minéraux HYDRATÉS."}, {"id": 29, "ch": "4", "section": "CH.4 — Vieillissement", "q": "Séquence de faciès métamorphiques lors du vieillissement océanique ?", "hint": "Deux températures, deux faciès", "a": "> 700°C : faciès AMPHIBOLITE (hornblende)\n500-700°C : faciès SCHISTE VERT (chlorite + actinote)"}, {"id": 30, "ch": "4", "section": "CH.4 — Vieillissement", "q": "Comment évolue la densité de la lithosphère océanique avec le temps ?", "hint": "Refroidissement → épaississement → densité", "a": "La lithosphère SE REFROIDIT, S'ÉPAISSIT (isotherme 1300°C s'approfondit),\nsa DENSITÉ AUGMENTE jusqu'à dépasser celle de l'asthénosphère."}, {"id": 31, "ch": "4", "section": "CH.4 — Vieillissement", "q": "Où et comment naissent les fumeurs noirs ?", "hint": "Cycle eau de mer", "a": "Eau de mer s'infiltre, se RÉCHAUFFE au contact du magma,\nse charge en minéraux et ressort : SOURCES HYDROTHERMALES CHAUDES."}, {"id": 32, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "Qu'est-ce que la décompression adiabatique ? Pourquoi déclenche-t-elle la fusion ?", "hint": "Pas de perte de chaleur", "a": "Remontée de matériel SANS ÉCHANGE DE CHALEUR.\nPression baisse → point de fusion (solidus) baisse aussi.\nLe géotherme RECOUPE LE SOLIDUS → fusion partielle."}, {"id": 33, "ch": "4", "section": "CH.4 — Vieillissement", "q": "Quelle est la signification de l'isotherme 1300°C pour la lithosphère ?", "hint": "Limite LAB", "a": "LIMITE LITHOSPHÈRE-ASTHÉNOSPHÈRE (LAB).\nElle S'APPROFONDIT avec le refroidissement de la plaque éloignée de la dorsale."}, {"id": 34, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "Basalte en coussin vs basalte en filon : quelle différence ?", "hint": "Eau vs fissure", "a": "COUSSIN : refroidissement très rapide au contact de l'eau (lave sous-marine).\nFILON (dyke) : injection de magma dans des fissures verticales."}, {"id": 35, "ch": "4", "section": "CH.4 — Vieillissement", "q": "Quelle minéralogie correspond au faciès schiste vert ?", "hint": "Température < 500°C", "a": "CHLORITE + ACTINOTE (amphibole calcique) : minéraux hydratés de basse température."}, {"id": 36, "ch": "4", "section": "CH.4 — Dorsales lentes", "q": "Pourquoi une dorsale lente a-t-elle un rift très marqué ?", "hint": "Magma insuffisant pour...", "a": "Magmatisme trop faible pour COMBLER L'ESPACE créé par l'écartement.\nL'extension tectonique forme un FOSSÉ D'EFFONDREMENT PROFOND."}, {"id": 37, "ch": "4", "section": "CH.4 — Vieillissement", "q": "Transformation minérale : pyroxène + plagioclase → ? (hydrothermalisme > 700°C)", "hint": "Faciès amphibolite", "a": "Pyroxène + plagioclase (gabbro) → HORNBLENDE (amphibole)\nSous l'action de l'hydrothermalisme à > 700°C."}, {"id": 38, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "La péridotite fond-elle entièrement sous une dorsale ? Quel pourcentage ?", "hint": "Partielle !", "a": "Non : seulement FUSION PARTIELLE (~15%).\nLe reste reste SOLIDE. C'est le liquide qui monte former la croûte."}, {"id": 39, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "Rôle de la convection mantellique dans le fonctionnement d'une dorsale ?", "hint": "Mouvement ascendant", "a": "La CONVECTION assure la REMONTÉE DES PÉRIDOTITES sous la dorsale,\ndéclenchant la décompression adiabatique et la fusion partielle."}, {"id": 40, "ch": "4", "section": "CH.4 — Dorsales lentes", "q": "Qu'est-ce qu'une faille de détachement aux dorsales lentes ?", "hint": "Grande faille, faible pendage", "a": "Grande FAILLE À FAIBLE PENDAGE (presque horizontale)\npermettant l'exhumation de péridotites directement sur le plancher océanique."}, # CH.5 — Zones de convergence {"id": 41, "ch": "5", "section": "CH.5 — Marqueurs de subduction", "q": "Quels sont les 3 marqueurs principaux d'une zone de subduction ?", "hint": "1 relief / 1 alignement / 1 sismique", "a": "1. FOSSE OCÉANIQUE profonde\n2. ALIGNEMENT VOLCANIQUE (arc insulaire ou cordillère)\n3. PLAN DE WADATI-BENIOFF (sismicité profonde)"}, {"id": 42, "ch": "5", "section": "CH.5 — Marqueurs de subduction", "q": "Pourquoi la sismicité suit-elle un plan incliné en subduction ?", "hint": "Rigidité de la plaque", "a": "La plaque reste RIGIDE en plongeant (elle est froide).\nLes contraintes génèrent des séismes le long du plan de contact : WADATI-BENIOFF."}, {"id": 43, "ch": "5", "section": "CH.5 — Magmatisme", "q": "Pourquoi le volcanisme de subduction est-il explosif ?", "hint": "Composition du magma", "a": "Magma riche en SILICE (très visqueux) et en GAZ DISSOUS.\nViscosité élevée → dégazage brutal → EXPLOSION VIOLENTE."}, {"id": 44, "ch": "5", "section": "CH.5 — Magmatisme", "q": "Quelle est la chaîne de réactions menant au volcanisme de subduction ?", "hint": "Eau → péridotite → magma", "a": "1. Métamorphisme → libération d'EAU\n2. Eau monte dans le COIN MANTELLIQUE\n3. Abaisse le point de fusion des péridotites\n4. FUSION PARTIELLE → magma → volcanisme"}, {"id": 45, "ch": "5", "section": "CH.5 — Magmatisme", "q": "Quelles sont les 4 roches de subduction et leur texture ?", "hint": "2 volcaniques + 2 plutoniques", "a": "Volcaniques (microlithique) : ANDÉSITE, RHYOLITE\nPlutoniques (grenue) : DIORITE, GRANITE"}, {"id": 46, "ch": "5", "section": "CH.5 — Magmatisme", "q": "Pourquoi existe-t-il une telle diversité de roches en subduction ?", "hint": "2 processus", "a": "CRISTALLISATION FRACTIONNÉE (différenciation du magma)\n+ CONTAMINATION par la croûte continentale traversée."}, {"id": 47, "ch": "5", "section": "CH.5 — Magmatisme", "q": "Quel est le signe minéralogique caractéristique des roches de subduction ?", "hint": "OH dans les minéraux", "a": "Présence de MINÉRAUX HYDRATÉS portant des groupements OH\n(ex. : amphibole, biotite, muscovite)."}, {"id": 48, "ch": "5", "section": "CH.5 — Métamorphisme", "q": "Séquence de faciès métamorphiques de la plaque subduite ?", "hint": "3 faciès successifs", "a": "SCHISTE VERT → SCHISTE BLEU (glaucophane, HP-BT) → ÉCLOGITE (grenat + jadéite, > 50 km)"}, {"id": 49, "ch": "5", "section": "CH.5 — Métamorphisme", "q": "Quelle minéralogie caractérise le faciès schiste bleu ?", "hint": "Minéral bleu, HP-BT", "a": "GLAUCOPHANE (amphibole sodique bleue) :\nindicateur de conditions HAUTE PRESSION – BASSE TEMPÉRATURE."}, {"id": 50, "ch": "5", "section": "CH.5 — Métamorphisme", "q": "Minéralogie du faciès éclogite et profondeur de formation ?", "hint": "> 50 km", "a": "GRENAT + JADÉITE (pyroxène sodique)\nFormés à > 50 KM de profondeur, sous très haute pression."}, {"id": 51, "ch": "5", "section": "CH.5 — Métamorphisme", "q": "En quoi le contexte P-T de la subduction est-il unique ?", "hint": "Comparer avec métamorphisme classique", "a": "HAUTE PRESSION / BASSE TEMPÉRATURE :\nLa plaque s'enfonce vite et reste froide.\nContraire du métamorphisme de contact (BT/BP)."}, {"id": 52, "ch": "5", "section": "CH.5 — Moteur", "q": "Quel est le moteur principal du mouvement des plaques ?", "hint": "Force de tirage", "a": "TRACTION DE LA PLAQUE SUBDUITE (slab pull) :\nPlaque froide et dense → gravité → tire la plaque par gravité,\nentraîne les mouvements descendants de la convection mantellique."}, {"id": 53, "ch": "5", "section": "CH.5 — Moteur", "q": "Quel lien y a-t-il entre subduction et convection mantellique ?", "hint": "Cause-conséquence", "a": "La subduction ENTRETIENT les courants descendants froids de la convection.\nLes plaques ne sont pas portées passivement : elles TIRENT ACTIVEMENT."}, {"id": 54, "ch": "5", "section": "CH.5 — Collision", "q": "Comment se produit une collision continentale ?", "hint": "Après quoi ?", "a": "Après la FERMETURE COMPLÈTE D'UN OCÉAN par subduction.\nLes deux marges continentales entrent en contact : croûte trop légère pour plonger."}, {"id": 55, "ch": "5", "section": "CH.5 — Collision", "q": "Quelles structures géologiques témoignent d'une collision en surface ?", "hint": "3 structures", "a": "PLIS + FAILLES INVERSES + NAPPES DE CHARRIAGE\n(grandes masses de roches déplacées horizontalement)"}, {"id": 56, "ch": "5", "section": "CH.5 — Collision", "q": "Qu'est-ce qu'une racine crustale ? À quelle profondeur descend le Moho ?", "hint": "Iceberg crustal", "a": "Épaississement de la croûte vers le bas.\nLe MOHO peut descendre jusqu'à 70 KM (normal : ~35 km)."}, {"id": 57, "ch": "5", "section": "CH.5 — Collision", "q": "Quels types de roches métamorphiques se forment en collision ?", "hint": "Roches foliées", "a": "Roches FOLIÉES :\n- MICASCHISTES (métamorphisme faible à modéré)\n- GNEISS (fort métamorphisme)"}, {"id": 58, "ch": "5", "section": "CH.5 — Marqueurs", "q": "Différence entre arc insulaire et cordillère en subduction ?", "hint": "Océan-Océan vs Océan-Continent", "a": "ARC INSULAIRE : subduction Océan/Océan → volcanisme en MER.\nCORDILLÈRE : subduction Océan/Continent → volcanisme en BORDURE DE CONTINENT."}, {"id": 59, "ch": "5", "section": "CH.5 — Magmatisme", "q": "Qu'est-ce que le coin mantellique et quel est son rôle ?", "hint": "Wedge", "a": "Portion du MANTEAU DE LA PLAQUE CHEVAUCHANTE entre les deux plaques.\nReçoit l'EAU libérée → péridotite fond partiellement → magmatisme."}, {"id": 60, "ch": "5", "section": "CH.5 — Risques", "q": "Risque volcanique caractéristique des zones de subduction ?", "hint": "Nuage mortel", "a": "NUÉES ARDENTES (coulées pyroclastiques) :\nmélanges brûlants de gaz, cendres, débris à très grande vitesse.\nLié à la viscosité élevée du magma."}, # SYNTHÈSE {"id": 61, "ch": "synth", "section": "SYNTHÈSE — Cycle lithosphérique", "q": "Résume en 5 étapes le cycle de la lithosphère océanique.", "hint": "Naissance → mort", "a": "1. Naissance à la DORSALE (HT, BP)\n2. Hydratation + refroidissement → schistes verts\n3. Densification → SUBDUCTION\n4. Métamorphisme HP-BT (schiste bleu → éclogite) + libération d'eau\n5. Déclenchement du MAGMATISME → croûte continentale"}, {"id": 62, "ch": "synth", "section": "SYNTHÈSE — Moteur", "q": "Quel processus assure la continuité de la convection mantellique globale ?", "hint": "Traction + plongement", "a": "TRACTION DE LA PLAQUE PLONGEANTE (slab pull) :\nforce gravitaire exercée par la plaque froide et dense,\nmoteur principal du cycle convectif global."}, {"id": 63, "ch": "synth", "section": "SYNTHÈSE — Eau", "q": "Quel est le rôle de l'eau dans le cycle de la lithosphère ?", "hint": "Partout : dorsale → subduction", "a": "DORSALE : hydratation hydrothermale (schistes verts)\nSUBDUCTION : expulsion par métamorphisme → abaissement du solidus des péridotites → magmatisme"}, {"id": 64, "ch": "synth", "section": "SYNTHÈSE — Cycle", "q": "Où naît et où meurt la lithosphère océanique ?", "hint": "Deux lieux opposés", "a": "NAISSANCE : à la DORSALE (accrétion)\nMORT : à la FOSSE DE SUBDUCTION (réintégration dans le manteau)"}, {"id": 65, "ch": "synth", "section": "SYNTHÈSE — Croûte continentale", "q": "Comment la croûte continentale est-elle créée via la subduction ?", "hint": "Eau → fusion → granite", "a": "Eau libérée → fusion coin mantellique → magma riche en silice → volcanisme + intrusions\n→ GRANITE ET DIORITE = croûte continentale"}, # Notions complémentaires {"id": 66, "ch": "3", "section": "CH.3 — Anomalies magnétiques", "q": "Qu'est-ce qu'une polarité magnétique normale vs inverse ?", "hint": "Pôles Nord et Sud", "a": "NORMALE : orientation actuelle (pôle magnétique Nord ≈ Nord géographique)\nINVERSE : pôles NORD ET SUD MAGNÉTIQUES INVERSÉS"}, {"id": 67, "ch": "4", "section": "CH.4 — Vieillissement", "q": "Pourquoi la lithosphère océanique devient-elle plus dense avec le temps ?", "hint": "Refroidissement + manteau", "a": "En se refroidissant, elle intègre du manteau refroidi par sa base.\nMinéraux se contractent → densité dépasse celle de l'ASTHÉNOSPHÈRE (~3,3 g/cm³)."}, {"id": 68, "ch": "5", "section": "CH.5 — Métamorphisme", "q": "Pourquoi le métamorphisme de subduction est-il HP-BT ?", "hint": "Vitesse d'enfouissement", "a": "La plaque S'ENFONCE RAPIDEMENT : pression augmente vite\nmais plaque froide ne se réchauffe pas assez → HAUTE PRESSION, BASSE TEMPÉRATURE."}, {"id": 69, "ch": "5", "section": "CH.5 — Collision", "q": "Pourquoi la croûte continentale ne peut-elle pas subduire ?", "hint": "Densité vs manteau", "a": "Croûte continentale : ~2,7 g/cm³\nManteau : ~3,3 g/cm³\nElle est MOINS DENSE → FLOTTE → ne peut pas plonger → COLLISION."}, {"id": 70, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "Qu'est-ce que le géotherme et le solidus dans le contexte des dorsales ?", "hint": "Deux courbes sur un graphique P-T", "a": "GÉOTHERME : température en fonction de la profondeur.\nSOLIDUS : courbe limite en dessous de laquelle la roche est solide.\nSi géotherme RECOUPE le solidus → LA ROCHE FOND."}, {"id": 71, "ch": "3", "section": "CH.3 — Preuves expansion", "q": "Quels indices prouvent l'expansion océanique (3 types) ?", "hint": "Magnétique + sédimentaire + géodésique", "a": "1. ANOMALIES MAGNÉTIQUES SYMÉTRIQUES\n2. ÂGE/ÉPAISSEUR croissants des sédiments\n3. MESURES GPS (géodésie spatiale)"}, {"id": 72, "ch": "4", "section": "CH.4 — Dorsales rapides", "q": "Composition minéralogique initiale d'un gabbro (avant métamorphisme) ?", "hint": "2 minéraux principaux", "a": "PYROXÈNE (clinopyroxène) + PLAGIOCLASE (feldspath calcique).\nRemplacés par hornblende puis chlorite + actinote lors du métamorphisme hydrothermal."}, {"id": 73, "ch": "5", "section": "CH.5 — Moteur", "q": "Quelle force initie le mouvement de la plaque vers la fosse ?", "hint": "Densité + gravité", "a": "Plaque océanique vieillie : PLUS DENSE QUE L'ASTHÉNOSPHÈRE.\nGravité → PLONGEMENT SPONTANÉ (slab pull) → tire le reste de la plaque."}, {"id": 74, "ch": "5", "section": "CH.5 — Magmatisme", "q": "Quelle roche est l'équivalent plutonique de l'andésite ?", "hint": "Roche à grain visible", "a": "La DIORITE : même composition que l'andésite\nmais refroidissement lent en profondeur → texture GRENUE."}, {"id": 75, "ch": "4", "section": "CH.4 — Vieillissement", "q": "À quelle température se place la limite faciès amphibolite / faciès schiste vert ?", "hint": "Deux seuils de température", "a": "AMPHIBOLITE : > 700°C\nSCHISTE VERT : entre 300 et 500°C\nTransition : 500–700°C"}, {"id": 76, "ch": "synth", "section": "SYNTHÈSE — Les 3 faciès du voyage", "q": "Cite les 3 faciès jalonnant le parcours d'une plaque océanique (dorsale → arc).", "hint": "Dorsale → plaque âgée → subduction", "a": "1. SCHISTE VERT (hydrothermalisme dorsale + vieillissement)\n2. SCHISTE BLEU (début subduction, HP-BT)\n3. ÉCLOGITE (subduction profonde > 50 km)"}, ] # ══════════════════════════════════════════════════════════ # COULEURS ET CONSTANTES # ══════════════════════════════════════════════════════════ SAVE_FILE = os.path.expanduser("~/.geo_revision_save.json") CH_LABELS = { "3": "CH.3 Mobilité", "4": "CH.4 Divergence", "5": "CH.5 Convergence", "synth": "SYNTHÈSE", } STATUS_LABELS = {None: "Non vu", 0: "À revoir", 1: "Hésitant", 2: "Maîtrisé"} WRONG_POOL = [ "Cristallisation fractionnée", "Plan de Wadati-Benioff", "Décompression adiabatique", "Fusion partielle", "Serpentinisation", "Dorsale médio-atlantique", "Glaucophane", "Faille inverse", "Faille normale", "Basalte en coussin", "Jadéite", "Grenat", "Hornblende", "Chlorite", "Actinote", "Gabbro", "Andésite", "Diorite", "Schiste bleu", "Schiste vert", "Éclogite", "Slab pull", "Coin mantellique", "Chambre magmatique", "Isotherme 1300°C", "Solidus", "Géotherme", "Arc insulaire", "Cordillère", "Nuée ardente", "5 à 7 km", "10 à 16 cm/an", "1 à 5 cm/an", "700 km de profondeur", "50 km de profondeur", "Fumeurs noirs", "Micaschiste", "Gneiss", "Péridotite", "Pyroxène", "Plagioclase", "Serpentine", "Nappe de charriage", "Racine crustale", ] # ══════════════════════════════════════════════════════════ # SAUVEGARDE # ══════════════════════════════════════════════════════════ def load_progress(): if os.path.exists(SAVE_FILE): try: with open(SAVE_FILE) as f: data = json.load(f) return {int(k): v for k, v in data.items()} except Exception: pass return {} def save_progress(status): try: with open(SAVE_FILE, "w") as f: json.dump(status, f) except Exception: pass # ══════════════════════════════════════════════════════════ # UTILITAIRES CURSES # ══════════════════════════════════════════════════════════ def init_colors(): curses.start_color() curses.use_default_colors() # Paires : (numéro, fg, bg) curses.init_pair(1, curses.COLOR_WHITE, -1) # normal curses.init_pair(2, curses.COLOR_CYAN, -1) # titre/accent curses.init_pair(3, curses.COLOR_GREEN, -1) # maîtrisé curses.init_pair(4, curses.COLOR_YELLOW, -1) # hésitant / ch.3 curses.init_pair(5, curses.COLOR_RED, -1) # à revoir curses.init_pair(6, curses.COLOR_MAGENTA, -1) # ch.4 / hint curses.init_pair(7, curses.COLOR_BLUE, -1) # ch.5 curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLUE) # sélectionné curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_GREEN) # correct curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_RED) # faux curses.init_pair(11, curses.COLOR_CYAN, curses.COLOR_BLACK) # header bg curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_CYAN) # bouton actif C_NORMAL = lambda: curses.color_pair(1) C_TITLE = lambda: curses.color_pair(2) | curses.A_BOLD C_GREEN = lambda: curses.color_pair(3) | curses.A_BOLD C_YELLOW = lambda: curses.color_pair(4) | curses.A_BOLD C_RED = lambda: curses.color_pair(5) | curses.A_BOLD C_MAGENTA = lambda: curses.color_pair(6) C_BLUE = lambda: curses.color_pair(7) C_SEL = lambda: curses.color_pair(8) | curses.A_BOLD C_OK = lambda: curses.color_pair(9) | curses.A_BOLD C_FAIL = lambda: curses.color_pair(10) | curses.A_BOLD C_HEADER = lambda: curses.color_pair(11) | curses.A_BOLD C_BTN = lambda: curses.color_pair(12) | curses.A_BOLD def ch_color(ch): return { "3": C_YELLOW(), "4": C_MAGENTA(), "5": C_BLUE(), "synth": C_GREEN(), }.get(ch, C_NORMAL()) def safe_addstr(win, y, x, text, attr=0): h, w = win.getmaxyx() if y < 0 or y >= h or x < 0: return max_len = w - x - 1 if max_len <= 0: return try: win.addstr(y, x, text[:max_len], attr) except curses.error: pass def draw_box(win, y, x, h, w, title="", color=None): attr = color or C_NORMAL() h2, w2 = win.getmaxyx() # top try: safe_addstr(win, y, x, "╔" + "═" * (w-2) + "╗", attr) for i in range(1, h-1): if y+i < h2: safe_addstr(win, y+i, x, "║", attr) safe_addstr(win, y+i, x+w-1, "║", attr) safe_addstr(win, y+h-1, x, "╚" + "═" * (w-2) + "╝", attr) if title: t = f" {title} " tx = x + (w - len(t)) // 2 safe_addstr(win, y, tx, t, attr | curses.A_BOLD) except curses.error: pass def draw_header(win, title): h, w = win.getmaxyx() try: win.attron(C_HEADER()) win.addstr(0, 0, " " * w) safe_addstr(win, 0, 0, f" GÉO-RÉVISION │ {title}", C_HEADER()) win.attroff(C_HEADER()) except curses.error: pass def wrap_text(text, width): lines = [] for paragraph in text.split("\n"): if len(paragraph) <= width: lines.append(paragraph) else: wrapped = textwrap.wrap(paragraph, width) lines.extend(wrapped if wrapped else [""]) return lines def get_stats(status): good = sum(1 for v in status.values() if v == 2) ok = sum(1 for v in status.values() if v == 1) bad = sum(1 for v in status.values() if v == 0) unseen = 76 - good - ok - bad return good, ok, bad, unseen def draw_stats_bar(win, y, status): good, ok, bad, unseen = get_stats(status) h, w = win.getmaxyx() bar = ( f" ✓ Maîtrisés:{good:3} " f"~ Hésitants:{ok:3} " f"✗ À revoir:{bad:3} " f"? Non vus:{unseen:3} " f"Total: 76" ) safe_addstr(win, y, 0, bar.ljust(w), C_NORMAL()) # ══════════════════════════════════════════════════════════ # ÉCRAN MENU PRINCIPAL # ══════════════════════════════════════════════════════════ def menu_screen(stdscr, status): options = [ ("1", "Flashcards", "Révision active par carte — toutes chapitres"), ("2", "Flashcards CH.3", "Chapitre 3 : Mobilité horizontale"), ("3", "Flashcards CH.4", "Chapitre 4 : Zones de divergence"), ("4", "Flashcards CH.5", "Chapitre 5 : Zones de convergence"), ("5", "Flashcards Synthèse", "Cycle lithosphérique complet"), ("6", "Flashcards À revoir", "Seulement les cartes non maîtrisées"), ("7", "Quiz QCM", "Entraînement avec 4 propositions"), ("8", "Récapitulatif", "Toutes les 76 notions en lecture"), ("9", "Carte de progression", "Visualisation de l'avancement"), ("0", "Réinitialiser", "Effacer la progression"), ("Q", "Quitter", ""), ] sel = 0 while True: stdscr.clear() h, w = stdscr.getmaxyx() draw_header(stdscr, "MENU PRINCIPAL") # Logo logo = [ "╔═══════════════════════════════════════════════════╗", "║ GÉO-RÉVISION — Tectonique des Plaques ║", "║ 76 notions · CH.3 · CH.4 · CH.5 · Synthèse ║", "║ Méthode : Ebbinghaus + Testing Effect ║", "╚═══════════════════════════════════════════════════╝", ] for i, line in enumerate(logo): safe_addstr(stdscr, 2+i, max(0,(w-len(line))//2), line, C_TITLE()) # Stats draw_stats_bar(stdscr, 8, status) # Options for i, (key, name, desc) in enumerate(options): y = 10 + i attr = C_SEL() if i == sel else C_NORMAL() prefix = "▶ " if i == sel else " " line = f"{prefix}[{key}] {name:<30} {desc}" safe_addstr(stdscr, y, 2, line[:w-4], attr) safe_addstr(stdscr, h-2, 2, "↑↓ pour naviguer · ENTRÉE pour sélectionner", C_MAGENTA()) k = stdscr.getch() if k == curses.KEY_UP: sel = (sel - 1) % len(options) elif k == curses.KEY_DOWN: sel = (sel + 1) % len(options) elif k in (curses.KEY_ENTER, 10, 13): key = options[sel][0] return key elif k in range(256): c = chr(k).upper() for i, (key, _, _) in enumerate(options): if c == key.upper(): return key # ══════════════════════════════════════════════════════════ # FLASHCARD # ══════════════════════════════════════════════════════════ def flashcard_screen(stdscr, status, cards, title="Flashcards"): if not cards: stdscr.clear() draw_header(stdscr, title) safe_addstr(stdscr, 5, 4, "Aucune carte dans ce filtre.", C_YELLOW()) safe_addstr(stdscr, 7, 4, "Appuie sur une touche pour revenir.", C_NORMAL()) stdscr.getch() return status deck = cards[:] random.shuffle(deck) idx = 0 flipped = False while True: card = deck[idx] stdscr.clear() h, w = stdscr.getmaxyx() draw_header(stdscr, f"{title} [{idx+1}/{len(deck)}]") draw_stats_bar(stdscr, 1, status) # Barre progression pct = (idx + 1) / len(deck) bar_w = min(w - 4, 60) filled = int(pct * bar_w) bar = "█" * filled + "░" * (bar_w - filled) safe_addstr(stdscr, 2, 2, f"[{bar}] {int(pct*100)}%", C_CYAN() if hasattr(curses, 'COLOR_CYAN') else C_NORMAL()) # Encart chapitre safe_addstr(stdscr, 4, 2, f"■ {card['section']}", ch_color(card["ch"])) # Statut de la carte s = status.get(card["id"]) s_colors = {None: C_NORMAL(), 0: C_RED(), 1: C_YELLOW(), 2: C_GREEN()} s_label = STATUS_LABELS.get(s, "Non vu") safe_addstr(stdscr, 4, w-20, f"[#{card['id']:02d}] {s_label}", s_colors.get(s, C_NORMAL())) if not flipped: # Recto : question draw_box(stdscr, 5, 1, min(12, h-14), w-2, "QUESTION", C_TITLE()) q_lines = wrap_text(card["q"], w-6) for i, line in enumerate(q_lines[:6]): safe_addstr(stdscr, 7+i, 4, line, C_NORMAL() | curses.A_BOLD) if card.get("hint"): safe_addstr(stdscr, 13, 4, f"💡 {card['hint']}", C_MAGENTA()) safe_addstr(stdscr, 15, 4, "[ ESPACE ] Révéler la réponse", C_TITLE()) safe_addstr(stdscr, 16, 4, "[ ← / → ] Carte précédente / suivante [ M ] Mélanger [ Q ] Menu", C_MAGENTA()) else: # Verso : réponse draw_box(stdscr, 5, 1, 3, w-2, "QUESTION", C_YELLOW()) q_short = card["q"][:w-6] safe_addstr(stdscr, 6, 4, q_short, C_YELLOW()) draw_box(stdscr, 8, 1, min(11, h-16), w-2, "RÉPONSE", C_GREEN()) a_lines = wrap_text(card["a"], w-6) for i, line in enumerate(a_lines[:7]): safe_addstr(stdscr, 10+i, 4, line, C_NORMAL()) # Boutons notation y_btn = h - 6 safe_addstr(stdscr, y_btn, 2, "[ 1 ] ✗ À REVOIR", C_RED()) safe_addstr(stdscr, y_btn, 25, "[ 2 ] ~ HÉSITANT", C_YELLOW()) safe_addstr(stdscr, y_btn, 50, "[ 3 ] ✓ MAÎTRISÉ", C_GREEN()) safe_addstr(stdscr, y_btn+1, 2, "[ ← / → ] Naviguer [ ESPACE ] Retourner [ Q ] Menu", C_MAGENTA()) k = stdscr.getch() if k == ord(' '): flipped = not flipped elif k == curses.KEY_RIGHT or k == ord('n') or k == ord('N'): idx = (idx + 1) % len(deck) flipped = False elif k == curses.KEY_LEFT or k == ord('p') or k == ord('P'): idx = (idx - 1) % len(deck) flipped = False elif k == ord('m') or k == ord('M'): random.shuffle(deck) idx = 0 flipped = False elif k == ord('1') and flipped: status[card["id"]] = 0 save_progress(status) flipped = False idx = (idx + 1) % len(deck) elif k == ord('2') and flipped: status[card["id"]] = 1 save_progress(status) flipped = False idx = (idx + 1) % len(deck) elif k == ord('3') and flipped: status[card["id"]] = 2 save_progress(status) flipped = False idx = (idx + 1) % len(deck) elif k == ord('q') or k == ord('Q'): break return status def C_CYAN(): return curses.color_pair(2) # ══════════════════════════════════════════════════════════ # QUIZ QCM # ══════════════════════════════════════════════════════════ def build_quiz_question(card): """Crée une question QCM à partir d'une carte.""" first_line = card["a"].split("\n")[0].strip() # Nettoie les annotations type "1. " correct = re.sub(r"^\d+\.\s*", "", first_line).strip() if len(correct) > 70: correct = correct[:70] + "..." # Génère 3 mauvaises réponses différentes pool = [w for w in WRONG_POOL if w.lower() not in correct.lower()] random.shuffle(pool) wrongs = pool[:3] options = wrongs + [correct] random.shuffle(options) return { "question": card["q"], "correct": correct, "options": options, "explanation": card["a"], "section": card["section"], "ch": card["ch"], } def quiz_screen(stdscr, status, cards=None): if cards is None: cards = CARDS[:] random.shuffle(cards) score = 0 total = len(cards) wrong_ids = [] for qi, card in enumerate(cards): qdata = build_quiz_question(card) sel = 0 answered = False chosen = None while True: stdscr.clear() h, w = stdscr.getmaxyx() draw_header(stdscr, f"QUIZ [{qi+1}/{total}] Score: {score}/{qi}") draw_stats_bar(stdscr, 1, status) # Section safe_addstr(stdscr, 3, 2, f"■ {qdata['section']}", ch_color(qdata["ch"])) # Question draw_box(stdscr, 4, 1, 4, w-2, "QUESTION", C_TITLE()) q_lines = wrap_text(qdata["question"], w-6) for i, line in enumerate(q_lines[:2]): safe_addstr(stdscr, 5+i, 4, line, C_NORMAL() | curses.A_BOLD) # Options for i, opt in enumerate(qdata["options"]): y = 9 + i * 2 prefix = " " if not answered: attr = C_SEL() if i == sel else C_NORMAL() prefix = "▶ " if i == sel else " " else: if opt == qdata["correct"]: attr = C_GREEN() prefix = "✓ " elif opt == chosen and chosen != qdata["correct"]: attr = C_RED() prefix = "✗ " else: attr = C_NORMAL() prefix = " " opt_short = opt[:w-10] safe_addstr(stdscr, y, 2, f"{prefix}[{'ABCD'[i]}] {opt_short}", attr) if answered: # Explication exp_y = 18 if chosen == qdata["correct"]: safe_addstr(stdscr, exp_y, 2, "✓ BONNE RÉPONSE !", C_GREEN()) else: safe_addstr(stdscr, exp_y, 2, f"✗ Réponse correcte : {qdata['correct'][:w-30]}", C_RED()) exp_lines = wrap_text(qdata["explanation"], w-6) for i, line in enumerate(exp_lines[:3]): safe_addstr(stdscr, exp_y+1+i, 4, line, C_MAGENTA()) safe_addstr(stdscr, h-2, 2, "[ ENTRÉE ou ESPACE ] Question suivante [ Q ] Arrêter", C_TITLE()) else: safe_addstr(stdscr, h-2, 2, "[ ↑↓ ] Naviguer [ ENTRÉE ] Valider [ Q ] Arrêter", C_TITLE()) k = stdscr.getch() if not answered: if k == curses.KEY_UP: sel = (sel - 1) % 4 elif k == curses.KEY_DOWN: sel = (sel + 1) % 4 elif k in range(ord('a'), ord('e')): sel = k - ord('a') elif k in range(ord('A'), ord('E')): sel = k - ord('A') elif k in (curses.KEY_ENTER, 10, 13, ord(' ')): answered = True chosen = qdata["options"][sel] if chosen == qdata["correct"]: score += 1 status[card["id"]] = max(status.get(card["id"], 0) or 0, 2) else: wrong_ids.append(card["id"]) status[card["id"]] = 0 save_progress(status) elif k == ord('q') or k == ord('Q'): break else: if k in (curses.KEY_ENTER, 10, 13, ord(' ')): break elif k == ord('q') or k == ord('Q'): # Montre résultat intermédiaire qi = total break if k == ord('q') or k == ord('Q'): break # Résultat final stdscr.clear() h, w = stdscr.getmaxyx() draw_header(stdscr, "RÉSULTAT DU QUIZ") pct = int(score / total * 100) if total > 0 else 0 color = C_GREEN() if pct >= 80 else C_YELLOW() if pct >= 50 else C_RED() draw_box(stdscr, 3, w//2-20, 12, 40, "SCORE FINAL", C_TITLE()) score_str = f"{pct}%" safe_addstr(stdscr, 6, w//2 - len(score_str)//2, score_str, color | curses.A_BOLD) detail = f"{score} bonnes réponses sur {total}" safe_addstr(stdscr, 8, w//2 - len(detail)//2, detail, C_NORMAL()) msg = ("🏆 Excellent ! Chapitres maîtrisés !" if pct >= 80 else "👍 Bien ! Quelques révisions restent utiles." if pct >= 60 else "⚠ Des lacunes — retourne aux flashcards !" if pct >= 40 else "📚 Reprends les flashcards et révise tout !") safe_addstr(stdscr, 10, w//2 - len(msg)//2, msg[:w-4], C_TITLE()) if wrong_ids: safe_addstr(stdscr, 16, 2, f"Cartes à revoir : {', '.join(f'#{i}' for i in wrong_ids[:15])}", C_RED()) safe_addstr(stdscr, h-2, 2, "Appuie sur une touche pour revenir au menu.", C_MAGENTA()) stdscr.getch() return status # ══════════════════════════════════════════════════════════ # RÉCAPITULATIF # ══════════════════════════════════════════════════════════ def recap_screen(stdscr, status): lines = [] groups = [ ("3", "═══ CHAPITRE 3 — Mobilité horizontale des plaques ═══"), ("4", "═══ CHAPITRE 4 — Dynamique des zones de divergence ═══"), ("5", "═══ CHAPITRE 5 — Dynamique des zones de convergence ═══"), ("synth", "═══ SYNTHÈSE — Cycle de la lithosphère océanique ═══"), ] for ch, title in groups: lines.append(("title", ch, title)) for c in CARDS: if c["ch"] == ch: lines.append(("section", ch, f" ─ {c['section']}")) lines.append(("question", ch, f" #{c['id']:02d} Q: {c['q']}")) for al in c["a"].split("\n"): lines.append(("answer", ch, f" → {al}")) lines.append(("sep", ch, "")) scroll = 0 while True: stdscr.clear() h, w = stdscr.getmaxyx() draw_header(stdscr, "RÉCAPITULATIF COMPLET — 76 notions") draw_stats_bar(stdscr, 1, status) visible = h - 5 for i, (ltype, ch, text) in enumerate(lines[scroll:scroll+visible]): y = 3 + i text_trunc = text[:w-2] if ltype == "title": safe_addstr(stdscr, y, 0, text_trunc.ljust(w-1), ch_color(ch) | curses.A_BOLD) elif ltype == "section": safe_addstr(stdscr, y, 0, text_trunc, C_CYAN()) elif ltype == "question": safe_addstr(stdscr, y, 0, text_trunc, C_NORMAL() | curses.A_BOLD) elif ltype == "answer": safe_addstr(stdscr, y, 0, text_trunc, C_MAGENTA()) else: pass pct_scroll = int(scroll / max(1, len(lines) - visible) * 100) safe_addstr(stdscr, h-1, 2, f"↑↓/PgUp/PgDn : défiler ({pct_scroll}%) Q : menu", C_TITLE()) k = stdscr.getch() if k == curses.KEY_UP: scroll = max(0, scroll - 1) elif k == curses.KEY_DOWN: scroll = min(len(lines) - visible, scroll + 1) elif k == curses.KEY_PPAGE: scroll = max(0, scroll - visible) elif k == curses.KEY_NPAGE: scroll = min(len(lines) - visible, scroll + visible) elif k == ord('q') or k == ord('Q'): break # ══════════════════════════════════════════════════════════ # CARTE DE PROGRESSION # ══════════════════════════════════════════════════════════ def map_screen(stdscr, status): while True: stdscr.clear() h, w = stdscr.getmaxyx() draw_header(stdscr, "CARTE DE PROGRESSION") draw_stats_bar(stdscr, 1, status) safe_addstr(stdscr, 3, 2, "Légende :", C_NORMAL() | curses.A_BOLD) safe_addstr(stdscr, 3, 14, "[██] Non vu", C_NORMAL()) safe_addstr(stdscr, 3, 28, "[██] À revoir", C_RED()) safe_addstr(stdscr, 3, 44, "[~~] Hésitant", C_YELLOW()) safe_addstr(stdscr, 3, 60, "[OK] Maîtrisé", C_GREEN()) # Grille cols = min(19, (w - 4) // 5) for c in CARDS: idx = c["id"] - 1 row = idx // cols col = idx % cols y = 5 + row * 2 x = 2 + col * 5 if y >= h - 3: break s = status.get(c["id"]) if s == 2: attr = C_GREEN() label = f"OK " elif s == 1: attr = C_YELLOW() label = f"~~ " elif s == 0: attr = C_RED() label = f"✗ " else: attr = C_NORMAL() label = f" " safe_addstr(stdscr, y, x, f"{c['id']:02d}", attr | curses.A_BOLD) safe_addstr(stdscr, y+1, x, label, attr) # Résumé par chapitre y_sum = h - 8 safe_addstr(stdscr, y_sum, 2, "Par chapitre :", C_TITLE()) for ch in ["3", "4", "5", "synth"]: ch_cards = [c for c in CARDS if c["ch"] == ch] ch_good = sum(1 for c in ch_cards if status.get(c["id"]) == 2) ch_ok = sum(1 for c in ch_cards if status.get(c["id"]) == 1) line = f"{CH_LABELS[ch]:20s} ✓{ch_good:3d} ~{ch_ok:3d} /{len(ch_cards)}" safe_addstr(stdscr, y_sum+1 + list(["3","4","5","synth"]).index(ch), 4, line, ch_color(ch)) safe_addstr(stdscr, h-1, 2, "Q : revenir au menu", C_MAGENTA()) k = stdscr.getch() if k == ord('q') or k == ord('Q'): break # ══════════════════════════════════════════════════════════ # RÉINITIALISATION # ══════════════════════════════════════════════════════════ def confirm_reset(stdscr, status): stdscr.clear() h, w = stdscr.getmaxyx() draw_header(stdscr, "RÉINITIALISATION") safe_addstr(stdscr, 5, 4, "⚠ Effacer toute la progression ?", C_RED()) safe_addstr(stdscr, 7, 4, "[ O ] Oui, effacer [ N ] Annuler", C_NORMAL()) k = stdscr.getch() if k in (ord('o'), ord('O')): status.clear() if os.path.exists(SAVE_FILE): os.remove(SAVE_FILE) safe_addstr(stdscr, 9, 4, "Progression effacée.", C_GREEN()) stdscr.refresh() curses.napms(1000) return status # ══════════════════════════════════════════════════════════ # MAIN # ══════════════════════════════════════════════════════════ def main(stdscr): curses.curs_set(0) stdscr.keypad(True) init_colors() status = load_progress() while True: choice = menu_screen(stdscr, status) if choice == "1": status = flashcard_screen(stdscr, status, CARDS[:], "Flashcards — Tout") elif choice == "2": cards = [c for c in CARDS if c["ch"] == "3"] status = flashcard_screen(stdscr, status, cards, "Flashcards — CH.3") elif choice == "3": cards = [c for c in CARDS if c["ch"] == "4"] status = flashcard_screen(stdscr, status, cards, "Flashcards — CH.4") elif choice == "4": cards = [c for c in CARDS if c["ch"] == "5"] status = flashcard_screen(stdscr, status, cards, "Flashcards — CH.5") elif choice == "5": cards = [c for c in CARDS if c["ch"] == "synth"] status = flashcard_screen(stdscr, status, cards, "Flashcards — Synthèse") elif choice == "6": cards = [c for c in CARDS if status.get(c["id"]) in (None, 0)] if not cards: cards = CARDS[:] status = flashcard_screen(stdscr, status, cards, "Flashcards — À revoir") elif choice == "7": status = quiz_screen(stdscr, status) elif choice == "8": recap_screen(stdscr, status) elif choice == "9": map_screen(stdscr, status) elif choice == "0": status = confirm_reset(stdscr, status) elif choice in ("Q", "q"): break save_progress(status) if __name__ == "__main__": curses.wrapper(main)