Enoder commited on
Commit
9504baf
·
verified ·
1 Parent(s): 8ba0889

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +530 -794
app.py CHANGED
@@ -1,348 +1,324 @@
1
  #!/usr/bin/env python3
2
  """
3
- ╔══════════════════════════════════════════════════════════╗
4
- ║ GÉO-RÉVISION Tectonique des Plaques ║
5
- 76 notions · Chapitres 3, 4, 5 & Synthèse
6
- ║ Méthode : Ebbinghaus + Testing Effect + Interleaving ║
7
- ╚══════════════════════════════════════════════════════════╝
8
  """
9
 
10
- import curses
11
  import random
12
- import json
13
- import os
14
  import re
15
- import textwrap
16
 
17
  # ══════════════════════════════════════════════════════════
18
  # DONNÉES — 76 cartes
19
  # ══════════════════════════════════════════════════════════
20
  CARDS = [
21
- # CH.3 — Limites de plaques
22
  {"id": 1, "ch": "3", "section": "CH.3 — Limites de plaques",
23
  "q": "Quels sont les 3 grands types de limites de plaques ?",
24
- "hint": "Pense : naissance / destruction / collision",
25
- "a": "1. DORSALES (divergence/accrétion oceanique)\n2. FOSSES OCEANIQUES (convergence/subduction)\n3. CHAINES DE MONTAGNES (convergence/collision)"},
26
  {"id": 2, "ch": "3", "section": "CH.3 — Dorsales",
27
  "q": "Quelles roches sont produites à la dorsale ? Quelle est leur texture ?",
28
- "hint": "Deux roches, deux textures",
29
- "a": "BASALTE : texture microlithique (refroidissement rapide en surface)\nGABBRO : texture grenue (refroidissement lent en profondeur)"},
30
  {"id": 3, "ch": "3", "section": "CH.3 — Dorsales",
31
  "q": "Quel type de failles caractérise les dorsales ? Pourquoi ?",
32
- "hint": "Extension ou compression ?",
33
- "a": "FAILLES NORMALES car la divergence crée une EXTENSION (étirement) de la croûte."},
34
  {"id": 4, "ch": "3", "section": "CH.3 — Dorsales",
35
  "q": "Caractéristiques sismiques et thermiques d'une dorsale ?",
36
- "hint": "Profondeur + intensité + flux",
37
- "a": "Sismicité SUPERFICIELLE et FAIBLE MAGNITUDE.\nFlux géothermique ÉLEVÉ (magmatisme ascendant)."},
38
  {"id": 5, "ch": "3", "section": "CH.3 — Fosses",
39
  "q": "Quelles roches plutoniques et volcaniques sont produites en subduction ?",
40
- "hint": "4 roches, 2 textures différentes",
41
- "a": "Plutoniques (grenues) : DIORITE, GRANITE\nVolcaniques (microlithiques) : ANDÉSITE, RHYOLITE"},
42
  {"id": 6, "ch": "3", "section": "CH.3 — Fosses",
43
  "q": "Comment se répartissent les foyers sismiques en zone de subduction ? Jusqu'où ?",
44
- "hint": "Un plan célèbre...",
45
- "a": "Plan de WADATI-BENIOFF : plan incliné suivant la plaque plongeante.\nAtteint 700 KM de profondeur."},
46
  {"id": 7, "ch": "3", "section": "CH.3 — Fosses",
47
  "q": "Décris le flux géothermique en zone de subduction (deux zones).",
48
- "hint": "Froid ici, chaud là-bas",
49
- "a": "BAS au niveau de la plaque froide qui plonge.\nFORT au niveau de l'arc volcanique sus-jacent."},
50
  {"id": 8, "ch": "3", "section": "CH.3 — Collision",
51
  "q": "Caractéristiques géologiques et sismiques d'une zone de collision ?",
52
- "hint": "Type de failles + profondeur séismes",
53
- "a": "FAILLES INVERSES, flux géothermique FAIBLE,\nsismicité principalement SUPERFICIELLE."},
54
  {"id": 9, "ch": "3", "section": "CH.3 — Points chauds",
55
  "q": "Comment un alignement de volcans inactifs prouve-t-il le mouvement des plaques ?",
56
- "hint": "Âge + distance",
57
- "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."},
58
  {"id": 10, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
59
  "q": "Qu'est-ce qu'une anomalie magnétique positive vs négative ?",
60
- "hint": "Intensité mesurée vs moyenne",
61
- "a": "POSITIVE : intensité > moyenne → polarité NORMALE\nNÉGATIVE : intensité < moyenne → polarité INVERSE (pôles inversés)"},
62
  {"id": 11, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
63
  "q": "Décris le patron des anomalies magnétiques autour d'une dorsale.",
64
- "hint": "Symétrie + image",
65
- "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."},
66
  {"id": 12, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
67
  "q": "Que révèle l'âge et l'épaisseur des sédiments sur le plancher océanique ?",
68
- "hint": "Loi simple distance/âge",
69
- "a": "Épaisseur et âge des sédiments AUGMENTENT avec la distance à la dorsale.\nPreuve de l'expansion océanique."},
70
  {"id": 13, "ch": "3", "section": "CH.3 — Géodésie spatiale",
71
  "q": "Que permet la géodésie spatiale (GPS) en tectonique ?",
72
- "hint": "Précision + deux types de vitesses",
73
- "a": "Localisation au MILLIMÈTRE PRÈS.\nCalcul des vitesses ABSOLUES (repères fixes) et RELATIVES (entre deux plaques)."},
74
  {"id": 14, "ch": "3", "section": "CH.3 — Résumé frontières",
75
  "q": "Quelle frontière présente une vallée axiale (rift) ? Quel mouvement ?",
76
- "hint": "Rift = fossé d'effondrement",
77
- "a": "La DORSALE : mouvement de DIVERGENCE.\nLa vallée axiale (rift) témoigne de l'extension de la croûte."},
78
  {"id": 15, "ch": "3", "section": "CH.3 — Structure générale",
79
  "q": "Lithosphère terrestre : structure de base et activité aux frontières.",
80
- "hint": "Découpe + concentration",
81
- "a": "Découpée en PLAQUES LITHOSPHÉRIQUES RIGIDES.\nFrontières concentrent l'essentiel de l'ACTIVITÉ SISMIQUE ET VOLCANIQUE."},
82
  {"id": 16, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
83
  "q": "Comment les basaltes enregistrent-ils le champ magnétique ?",
84
- "hint": "Moment clé = refroidissement",
85
- "a": "En se REFROIDISSANT, les minéraux magnétiques (magnétite) se figent dans la direction du champ magnétique terrestre du moment."},
86
  {"id": 17, "ch": "3", "section": "CH.3 — Dorsales",
87
  "q": "Que produit l'accrétion océanique à la dorsale ?",
88
- "hint": "Naît quoi ?",
89
- "a": "La CROÛTE OCÉANIQUE :\n- Basalte en surface\n- Gabbro en profondeur\n- Péridotite mantellique sous-jacente"},
90
  {"id": 18, "ch": "3", "section": "CH.3 — Fosses",
91
  "q": "Qu'est-ce qu'une fosse océanique ? Quel processus y est associé ?",
92
- "hint": "Plongement...",
93
- "a": "Dépression profonde marquant une ZONE DE SUBDUCTION :\nplongement d'une plaque océanique dense dans le manteau."},
94
  {"id": 19, "ch": "3", "section": "CH.3 — Collision",
95
  "q": "Qu'est-ce qu'une chaîne de montagnes en termes tectoniques ?",
96
- "hint": "Fermeture d'un océan",
97
- "a": "Zone de COLLISION CONTINENTALE : résultat de la convergence de deux plaques continentales après fermeture d'un océan."},
98
- # CH.4 — Dorsales rapides
99
  {"id": 20, "ch": "4", "section": "CH.4 — Dorsales rapides",
100
  "q": "Quelle est la vitesse d'écartement d'une dorsale rapide ?",
101
- "hint": "10–16 cm/an — Quel océan ?",
102
- "a": "10 à 16 CM/AN.\nExemple : dorsale du Pacifique Est (Pacifique)."},
103
  {"id": 21, "ch": "4", "section": "CH.4 — Dorsales rapides",
104
  "q": "Morphologie d'une dorsale rapide : rift, bombement ?",
105
- "hint": "Opposé de la dorsale lente",
106
- "a": "BOMBEMENT LARGE mais VALLÉE AXIALE QUASI ABSENTE (magmatisme abondant comble le rift)."},
107
  {"id": 22, "ch": "4", "section": "CH.4 — Dorsales rapides",
108
- "q": "Décris le processus de fusion partielle sous une dorsale (étapes).",
109
- "hint": "Convection → décompression → solidus",
110
- "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"},
111
  {"id": 23, "ch": "4", "section": "CH.4 — Dorsales rapides",
112
  "q": "Où se loge la chambre magmatique ? Quelles roches en sortent ?",
113
- "hint": "Profondeur + 2 roches",
114
- "a": "Entre 2 et 7 KM de profondeur.\nRefroidissement lent → GABBROS (grenu)\nRemontée + refroidissement rapide → BASALTES (microlithique)"},
115
  {"id": 24, "ch": "4", "section": "CH.4 — Dorsales rapides",
116
  "q": "Quelle épaisseur de croûte produit une dorsale rapide ?",
117
- "hint": "Chiffre précis en km",
118
- "a": "Croûte océanique régulière de 5 À 7 KM d'épaisseur."},
119
  {"id": 25, "ch": "4", "section": "CH.4 — Dorsales lentes",
120
  "q": "Quelle est la vitesse d'écartement d'une dorsale lente ?",
121
- "hint": "1–5 cm/an — Quel océan ?",
122
- "a": "1 à 5 CM/AN.\nExemple : dorsale médio-atlantique (Atlantique)."},
123
  {"id": 26, "ch": "4", "section": "CH.4 — Dorsales lentes",
124
  "q": "Morphologie d'une dorsale lente : rift, magmatisme, expansion.",
125
- "hint": "Opposé de rapide",
126
- "a": "RIFT TRÈS MARQUÉ + grandes failles de détachement.\nMagmatisme RÉDUIT ET DISCONTINU.\nExpansion parfois assurée par ÉTIREMENT TECTONIQUE."},
127
  {"id": 27, "ch": "4", "section": "CH.4 — Dorsales lentes",
128
  "q": "Qu'est-ce que la serpentinisation et quand se produit-elle ?",
129
- "hint": "Roche + eau + dorsale lente",
130
- "a": "Hydratation des PÉRIDOTITES (manteau) remontées en surface (manque de magmatisme).\nL'eau transforme l'olivine en SERPENTINE."},
131
  {"id": 28, "ch": "4", "section": "CH.4 — Vieillissement",
132
  "q": "Qu'est-ce que le métamorphisme hydrothermal ?",
133
- "hint": "Eau + chaleur + minéraux",
134
- "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."},
135
  {"id": 29, "ch": "4", "section": "CH.4 — Vieillissement",
136
  "q": "Séquence de faciès métamorphiques lors du vieillissement océanique ?",
137
- "hint": "Deux températures, deux faciès",
138
- "a": "> 700°C : faciès AMPHIBOLITE (hornblende)\n500-700°C : faciès SCHISTE VERT (chlorite + actinote)"},
139
  {"id": 30, "ch": "4", "section": "CH.4 — Vieillissement",
140
  "q": "Comment évolue la densité de la lithosphère océanique avec le temps ?",
141
- "hint": "Refroidissement → épaississement → densité",
142
- "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."},
143
  {"id": 31, "ch": "4", "section": "CH.4 — Vieillissement",
144
  "q": "Où et comment naissent les fumeurs noirs ?",
145
- "hint": "Cycle eau de mer",
146
- "a": "Eau de mer s'infiltre, se RÉCHAUFFE au contact du magma,\nse charge en minéraux et ressort : SOURCES HYDROTHERMALES CHAUDES."},
147
  {"id": 32, "ch": "4", "section": "CH.4 — Dorsales rapides",
148
  "q": "Qu'est-ce que la décompression adiabatique ? Pourquoi déclenche-t-elle la fusion ?",
149
- "hint": "Pas de perte de chaleur",
150
- "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."},
151
  {"id": 33, "ch": "4", "section": "CH.4 — Vieillissement",
152
  "q": "Quelle est la signification de l'isotherme 1300°C pour la lithosphère ?",
153
- "hint": "Limite LAB",
154
- "a": "LIMITE LITHOSPHÈRE-ASTHÉNOSPHÈRE (LAB).\nElle S'APPROFONDIT avec le refroidissement de la plaque éloignée de la dorsale."},
155
  {"id": 34, "ch": "4", "section": "CH.4 — Dorsales rapides",
156
  "q": "Basalte en coussin vs basalte en filon : quelle différence ?",
157
- "hint": "Eau vs fissure",
158
- "a": "COUSSIN : refroidissement très rapide au contact de l'eau (lave sous-marine).\nFILON (dyke) : injection de magma dans des fissures verticales."},
159
  {"id": 35, "ch": "4", "section": "CH.4 — Vieillissement",
160
  "q": "Quelle minéralogie correspond au faciès schiste vert ?",
161
- "hint": "Température < 500°C",
162
- "a": "CHLORITE + ACTINOTE (amphibole calcique) : minéraux hydratés de basse température."},
163
  {"id": 36, "ch": "4", "section": "CH.4 — Dorsales lentes",
164
  "q": "Pourquoi une dorsale lente a-t-elle un rift très marqué ?",
165
- "hint": "Magma insuffisant pour...",
166
- "a": "Magmatisme trop faible pour COMBLER L'ESPACE créé par l'écartement.\nL'extension tectonique forme un FOSSÉ D'EFFONDREMENT PROFOND."},
167
  {"id": 37, "ch": "4", "section": "CH.4 — Vieillissement",
168
- "q": "Transformation minérale : pyroxène + plagioclase → ? (hydrothermalisme > 700°C)",
169
- "hint": "Faciès amphibolite",
170
- "a": "Pyroxène + plagioclase (gabbro) → HORNBLENDE (amphibole)\nSous l'action de l'hydrothermalisme à > 700°C."},
171
  {"id": 38, "ch": "4", "section": "CH.4 — Dorsales rapides",
172
  "q": "La péridotite fond-elle entièrement sous une dorsale ? Quel pourcentage ?",
173
- "hint": "Partielle !",
174
- "a": "Non : seulement FUSION PARTIELLE (~15%).\nLe reste reste SOLIDE. C'est le liquide qui monte former la croûte."},
175
  {"id": 39, "ch": "4", "section": "CH.4 — Dorsales rapides",
176
  "q": "Rôle de la convection mantellique dans le fonctionnement d'une dorsale ?",
177
- "hint": "Mouvement ascendant",
178
- "a": "La CONVECTION assure la REMONTÉE DES PÉRIDOTITES sous la dorsale,\ndéclenchant la décompression adiabatique et la fusion partielle."},
179
  {"id": 40, "ch": "4", "section": "CH.4 — Dorsales lentes",
180
  "q": "Qu'est-ce qu'une faille de détachement aux dorsales lentes ?",
181
- "hint": "Grande faille, faible pendage",
182
- "a": "Grande FAILLE À FAIBLE PENDAGE (presque horizontale)\npermettant l'exhumation de péridotites directement sur le plancher océanique."},
183
- # CH.5 — Zones de convergence
184
  {"id": 41, "ch": "5", "section": "CH.5 — Marqueurs de subduction",
185
  "q": "Quels sont les 3 marqueurs principaux d'une zone de subduction ?",
186
- "hint": "1 relief / 1 alignement / 1 sismique",
187
- "a": "1. FOSSE OCÉANIQUE profonde\n2. ALIGNEMENT VOLCANIQUE (arc insulaire ou cordillère)\n3. PLAN DE WADATI-BENIOFF (sismicité profonde)"},
188
  {"id": 42, "ch": "5", "section": "CH.5 — Marqueurs de subduction",
189
  "q": "Pourquoi la sismicité suit-elle un plan incliné en subduction ?",
190
- "hint": "Rigidité de la plaque",
191
- "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."},
192
  {"id": 43, "ch": "5", "section": "CH.5 — Magmatisme",
193
  "q": "Pourquoi le volcanisme de subduction est-il explosif ?",
194
- "hint": "Composition du magma",
195
- "a": "Magma riche en SILICE (très visqueux) et en GAZ DISSOUS.\nViscosité élevée → dégazage brutal → EXPLOSION VIOLENTE."},
196
  {"id": 44, "ch": "5", "section": "CH.5 — Magmatisme",
197
  "q": "Quelle est la chaîne de réactions menant au volcanisme de subduction ?",
198
- "hint": "Eau → péridotite → magma",
199
- "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"},
200
  {"id": 45, "ch": "5", "section": "CH.5 — Magmatisme",
201
  "q": "Quelles sont les 4 roches de subduction et leur texture ?",
202
- "hint": "2 volcaniques + 2 plutoniques",
203
- "a": "Volcaniques (microlithique) : ANDÉSITE, RHYOLITE\nPlutoniques (grenue) : DIORITE, GRANITE"},
204
  {"id": 46, "ch": "5", "section": "CH.5 — Magmatisme",
205
  "q": "Pourquoi existe-t-il une telle diversité de roches en subduction ?",
206
- "hint": "2 processus",
207
- "a": "CRISTALLISATION FRACTIONNÉE (différenciation du magma)\n+ CONTAMINATION par la croûte continentale traversée."},
208
  {"id": 47, "ch": "5", "section": "CH.5 — Magmatisme",
209
  "q": "Quel est le signe minéralogique caractéristique des roches de subduction ?",
210
- "hint": "OH dans les minéraux",
211
- "a": "Présence de MINÉRAUX HYDRATÉS portant des groupements OH\n(ex. : amphibole, biotite, muscovite)."},
212
  {"id": 48, "ch": "5", "section": "CH.5 — Métamorphisme",
213
  "q": "Séquence de faciès métamorphiques de la plaque subduite ?",
214
- "hint": "3 faciès successifs",
215
- "a": "SCHISTE VERT → SCHISTE BLEU (glaucophane, HP-BT) → ÉCLOGITE (grenat + jadéite, > 50 km)"},
216
  {"id": 49, "ch": "5", "section": "CH.5 — Métamorphisme",
217
  "q": "Quelle minéralogie caractérise le faciès schiste bleu ?",
218
- "hint": "Minéral bleu, HP-BT",
219
- "a": "GLAUCOPHANE (amphibole sodique bleue) :\nindicateur de conditions HAUTE PRESSION – BASSE TEMPÉRATURE."},
220
  {"id": 50, "ch": "5", "section": "CH.5 — Métamorphisme",
221
  "q": "Minéralogie du faciès éclogite et profondeur de formation ?",
222
- "hint": "> 50 km",
223
- "a": "GRENAT + JADÉITE (pyroxène sodique)\nFormés à > 50 KM de profondeur, sous très haute pression."},
224
  {"id": 51, "ch": "5", "section": "CH.5 — Métamorphisme",
225
  "q": "En quoi le contexte P-T de la subduction est-il unique ?",
226
- "hint": "Comparer avec métamorphisme classique",
227
- "a": "HAUTE PRESSION / BASSE TEMPÉRATURE :\nLa plaque s'enfonce vite et reste froide.\nContraire du métamorphisme de contact (BT/BP)."},
228
  {"id": 52, "ch": "5", "section": "CH.5 — Moteur",
229
  "q": "Quel est le moteur principal du mouvement des plaques ?",
230
- "hint": "Force de tirage",
231
- "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."},
232
  {"id": 53, "ch": "5", "section": "CH.5 — Moteur",
233
  "q": "Quel lien y a-t-il entre subduction et convection mantellique ?",
234
- "hint": "Cause-conséquence",
235
- "a": "La subduction ENTRETIENT les courants descendants froids de la convection.\nLes plaques ne sont pas portées passivement : elles TIRENT ACTIVEMENT."},
236
  {"id": 54, "ch": "5", "section": "CH.5 — Collision",
237
  "q": "Comment se produit une collision continentale ?",
238
- "hint": "Après quoi ?",
239
- "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."},
240
  {"id": 55, "ch": "5", "section": "CH.5 — Collision",
241
  "q": "Quelles structures géologiques témoignent d'une collision en surface ?",
242
- "hint": "3 structures",
243
- "a": "PLIS + FAILLES INVERSES + NAPPES DE CHARRIAGE\n(grandes masses de roches déplacées horizontalement)"},
244
  {"id": 56, "ch": "5", "section": "CH.5 — Collision",
245
  "q": "Qu'est-ce qu'une racine crustale ? À quelle profondeur descend le Moho ?",
246
- "hint": "Iceberg crustal",
247
- "a": "Épaississement de la croûte vers le bas.\nLe MOHO peut descendre jusqu'à 70 KM (normal : ~35 km)."},
248
  {"id": 57, "ch": "5", "section": "CH.5 — Collision",
249
  "q": "Quels types de roches métamorphiques se forment en collision ?",
250
- "hint": "Roches foliées",
251
- "a": "Roches FOLIÉES :\n- MICASCHISTES (métamorphisme faible à modéré)\n- GNEISS (fort métamorphisme)"},
252
  {"id": 58, "ch": "5", "section": "CH.5 — Marqueurs",
253
  "q": "Différence entre arc insulaire et cordillère en subduction ?",
254
- "hint": "Océan-Océan vs Océan-Continent",
255
- "a": "ARC INSULAIRE : subduction Océan/Océan → volcanisme en MER.\nCORDILLÈRE : subduction Océan/Continent → volcanisme en BORDURE DE CONTINENT."},
256
  {"id": 59, "ch": "5", "section": "CH.5 — Magmatisme",
257
  "q": "Qu'est-ce que le coin mantellique et quel est son rôle ?",
258
- "hint": "Wedge",
259
- "a": "Portion du MANTEAU DE LA PLAQUE CHEVAUCHANTE entre les deux plaques.\nReçoit l'EAU libérée → péridotite fond partiellement → magmatisme."},
260
  {"id": 60, "ch": "5", "section": "CH.5 — Risques",
261
  "q": "Risque volcanique caractéristique des zones de subduction ?",
262
- "hint": "Nuage mortel",
263
- "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."},
264
- # SYNTHÈSE
265
  {"id": 61, "ch": "synth", "section": "SYNTHÈSE — Cycle lithosphérique",
266
  "q": "Résume en 5 étapes le cycle de la lithosphère océanique.",
267
- "hint": "Naissance → mort",
268
- "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"},
269
  {"id": 62, "ch": "synth", "section": "SYNTHÈSE — Moteur",
270
  "q": "Quel processus assure la continuité de la convection mantellique globale ?",
271
- "hint": "Traction + plongement",
272
- "a": "TRACTION DE LA PLAQUE PLONGEANTE (slab pull) :\nforce gravitaire exercée par la plaque froide et dense,\nmoteur principal du cycle convectif global."},
273
  {"id": 63, "ch": "synth", "section": "SYNTHÈSE — Eau",
274
  "q": "Quel est le rôle de l'eau dans le cycle de la lithosphère ?",
275
- "hint": "Partout : dorsale → subduction",
276
- "a": "DORSALE : hydratation hydrothermale (schistes verts)\nSUBDUCTION : expulsion par métamorphisme → abaissement du solidus des péridotites → magmatisme"},
277
  {"id": 64, "ch": "synth", "section": "SYNTHÈSE — Cycle",
278
  "q": "Où naît et où meurt la lithosphère océanique ?",
279
- "hint": "Deux lieux opposés",
280
- "a": "NAISSANCE : à la DORSALE (accrétion)\nMORT : à la FOSSE DE SUBDUCTION (réintégration dans le manteau)"},
281
  {"id": 65, "ch": "synth", "section": "SYNTHÈSE — Croûte continentale",
282
  "q": "Comment la croûte continentale est-elle créée via la subduction ?",
283
- "hint": "Eau → fusion → granite",
284
- "a": "Eau libérée → fusion coin mantellique → magma riche en silice → volcanisme + intrusions\n→ GRANITE ET DIORITE = croûte continentale"},
285
- # Notions complémentaires
286
  {"id": 66, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
287
  "q": "Qu'est-ce qu'une polarité magnétique normale vs inverse ?",
288
- "hint": "Pôles Nord et Sud",
289
- "a": "NORMALE : orientation actuelle (pôle magnétique Nord ≈ Nord géographique)\nINVERSE : pôles NORD ET SUD MAGNÉTIQUES INVERSÉS"},
290
  {"id": 67, "ch": "4", "section": "CH.4 — Vieillissement",
291
  "q": "Pourquoi la lithosphère océanique devient-elle plus dense avec le temps ?",
292
- "hint": "Refroidissement + manteau",
293
- "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³)."},
294
  {"id": 68, "ch": "5", "section": "CH.5 — Métamorphisme",
295
  "q": "Pourquoi le métamorphisme de subduction est-il HP-BT ?",
296
- "hint": "Vitesse d'enfouissement",
297
- "a": "La plaque S'ENFONCE RAPIDEMENT : pression augmente vite\nmais plaque froide ne se réchauffe pas assez → HAUTE PRESSION, BASSE TEMPÉRATURE."},
298
  {"id": 69, "ch": "5", "section": "CH.5 — Collision",
299
  "q": "Pourquoi la croûte continentale ne peut-elle pas subduire ?",
300
- "hint": "Densité vs manteau",
301
- "a": "Croûte continentale : ~2,7 g/cm³\nManteau : ~3,3 g/cm³\nElle est MOINS DENSE → FLOTTE → ne peut pas plonger → COLLISION."},
302
  {"id": 70, "ch": "4", "section": "CH.4 — Dorsales rapides",
303
  "q": "Qu'est-ce que le géotherme et le solidus dans le contexte des dorsales ?",
304
- "hint": "Deux courbes sur un graphique P-T",
305
- "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."},
306
  {"id": 71, "ch": "3", "section": "CH.3 — Preuves expansion",
307
  "q": "Quels indices prouvent l'expansion océanique (3 types) ?",
308
- "hint": "Magnétique + sédimentaire + géodésique",
309
- "a": "1. ANOMALIES MAGNÉTIQUES SYMÉTRIQUES\n2. ÂGE/ÉPAISSEUR croissants des sédiments\n3. MESURES GPS (géodésie spatiale)"},
310
  {"id": 72, "ch": "4", "section": "CH.4 — Dorsales rapides",
311
  "q": "Composition minéralogique initiale d'un gabbro (avant métamorphisme) ?",
312
- "hint": "2 minéraux principaux",
313
- "a": "PYROXÈNE (clinopyroxène) + PLAGIOCLASE (feldspath calcique).\nRemplacés par hornblende puis chlorite + actinote lors du métamorphisme hydrothermal."},
314
  {"id": 73, "ch": "5", "section": "CH.5 — Moteur",
315
  "q": "Quelle force initie le mouvement de la plaque vers la fosse ?",
316
- "hint": "Densité + gravité",
317
- "a": "Plaque océanique vieillie : PLUS DENSE QUE L'ASTHÉNOSPHÈRE.\nGravité → PLONGEMENT SPONTANÉ (slab pull) → tire le reste de la plaque."},
318
  {"id": 74, "ch": "5", "section": "CH.5 — Magmatisme",
319
  "q": "Quelle roche est l'équivalent plutonique de l'andésite ?",
320
- "hint": "Roche à grain visible",
321
- "a": "La DIORITE : même composition que l'andésite\nmais refroidissement lent en profondeur → texture GRENUE."},
322
  {"id": 75, "ch": "4", "section": "CH.4 — Vieillissement",
323
  "q": "À quelle température se place la limite faciès amphibolite / faciès schiste vert ?",
324
- "hint": "Deux seuils de température",
325
- "a": "AMPHIBOLITE : > 700°C\nSCHISTE VERT : entre 300 et 500°C\nTransition : 500–700°C"},
326
  {"id": 76, "ch": "synth", "section": "SYNTHÈSE — Les 3 faciès du voyage",
327
  "q": "Cite les 3 faciès jalonnant le parcours d'une plaque océanique (dorsale → arc).",
328
- "hint": "Dorsale → plaque âgée → subduction",
329
- "a": "1. SCHISTE VERT (hydrothermalisme dorsale + vieillissement)\n2. SCHISTE BLEU (début subduction, HP-BT)\n3. ÉCLOGITE (subduction profonde > 50 km)"},
330
  ]
331
 
332
- # ══════════════════════════════════════════════════════════
333
- # COULEURS ET CONSTANTES
334
- # ══════════════════════════════════════════════════════════
335
- SAVE_FILE = os.path.expanduser("~/.geo_revision_save.json")
336
-
337
- CH_LABELS = {
338
- "3": "CH.3 Mobilité",
339
- "4": "CH.4 Divergence",
340
- "5": "CH.5 Convergence",
341
- "synth": "SYNTHÈSE",
342
- }
343
-
344
- STATUS_LABELS = {None: "Non vu", 0: "À revoir", 1: "Hésitant", 2: "Maîtrisé"}
345
-
346
  WRONG_POOL = [
347
  "Cristallisation fractionnée", "Plan de Wadati-Benioff", "Décompression adiabatique",
348
  "Fusion partielle", "Serpentinisation", "Dorsale médio-atlantique", "Glaucophane",
@@ -354,635 +330,395 @@ WRONG_POOL = [
354
  "1 à 5 cm/an", "700 km de profondeur", "50 km de profondeur",
355
  "Fumeurs noirs", "Micaschiste", "Gneiss", "Péridotite", "Pyroxène",
356
  "Plagioclase", "Serpentine", "Nappe de charriage", "Racine crustale",
 
357
  ]
358
 
359
- # ══════════════════════════════════════════════════════════
360
- # SAUVEGARDE
361
- # ══════════════════════════════════════════════════════════
362
- def load_progress():
363
- if os.path.exists(SAVE_FILE):
364
- try:
365
- with open(SAVE_FILE) as f:
366
- data = json.load(f)
367
- return {int(k): v for k, v in data.items()}
368
- except Exception:
369
- pass
370
- return {}
371
-
372
- def save_progress(status):
373
- try:
374
- with open(SAVE_FILE, "w") as f:
375
- json.dump(status, f)
376
- except Exception:
377
- pass
378
 
379
  # ══════════════════════════════════════════════════════════
380
- # UTILITAIRES CURSES
381
  # ══════════════════════════════════════════════════════════
382
- def init_colors():
383
- curses.start_color()
384
- curses.use_default_colors()
385
- # Paires : (numéro, fg, bg)
386
- curses.init_pair(1, curses.COLOR_WHITE, -1) # normal
387
- curses.init_pair(2, curses.COLOR_CYAN, -1) # titre/accent
388
- curses.init_pair(3, curses.COLOR_GREEN, -1) # maîtrisé
389
- curses.init_pair(4, curses.COLOR_YELLOW, -1) # hésitant / ch.3
390
- curses.init_pair(5, curses.COLOR_RED, -1) # à revoir
391
- curses.init_pair(6, curses.COLOR_MAGENTA, -1) # ch.4 / hint
392
- curses.init_pair(7, curses.COLOR_BLUE, -1) # ch.5
393
- curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLUE) # sélectionné
394
- curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_GREEN) # correct
395
- curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_RED) # faux
396
- curses.init_pair(11, curses.COLOR_CYAN, curses.COLOR_BLACK) # header bg
397
- curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_CYAN) # bouton actif
398
-
399
- C_NORMAL = lambda: curses.color_pair(1)
400
- C_TITLE = lambda: curses.color_pair(2) | curses.A_BOLD
401
- C_GREEN = lambda: curses.color_pair(3) | curses.A_BOLD
402
- C_YELLOW = lambda: curses.color_pair(4) | curses.A_BOLD
403
- C_RED = lambda: curses.color_pair(5) | curses.A_BOLD
404
- C_MAGENTA = lambda: curses.color_pair(6)
405
- C_BLUE = lambda: curses.color_pair(7)
406
- C_SEL = lambda: curses.color_pair(8) | curses.A_BOLD
407
- C_OK = lambda: curses.color_pair(9) | curses.A_BOLD
408
- C_FAIL = lambda: curses.color_pair(10) | curses.A_BOLD
409
- C_HEADER = lambda: curses.color_pair(11) | curses.A_BOLD
410
- C_BTN = lambda: curses.color_pair(12) | curses.A_BOLD
411
-
412
- def ch_color(ch):
413
- return {
414
- "3": C_YELLOW(),
415
- "4": C_MAGENTA(),
416
- "5": C_BLUE(),
417
- "synth": C_GREEN(),
418
- }.get(ch, C_NORMAL())
419
-
420
- def safe_addstr(win, y, x, text, attr=0):
421
- h, w = win.getmaxyx()
422
- if y < 0 or y >= h or x < 0:
423
- return
424
- max_len = w - x - 1
425
- if max_len <= 0:
426
- return
427
- try:
428
- win.addstr(y, x, text[:max_len], attr)
429
- except curses.error:
430
- pass
431
-
432
- def draw_box(win, y, x, h, w, title="", color=None):
433
- attr = color or C_NORMAL()
434
- h2, w2 = win.getmaxyx()
435
- # top
436
- try:
437
- safe_addstr(win, y, x, "╔" + "═" * (w-2) + "╗", attr)
438
- for i in range(1, h-1):
439
- if y+i < h2:
440
- safe_addstr(win, y+i, x, "║", attr)
441
- safe_addstr(win, y+i, x+w-1, "║", attr)
442
- safe_addstr(win, y+h-1, x, "╚" + "═" * (w-2) + "╝", attr)
443
- if title:
444
- t = f" {title} "
445
- tx = x + (w - len(t)) // 2
446
- safe_addstr(win, y, tx, t, attr | curses.A_BOLD)
447
- except curses.error:
448
- pass
449
-
450
- def draw_header(win, title):
451
- h, w = win.getmaxyx()
452
- try:
453
- win.attron(C_HEADER())
454
- win.addstr(0, 0, " " * w)
455
- safe_addstr(win, 0, 0, f" GÉO-RÉVISION │ {title}", C_HEADER())
456
- win.attroff(C_HEADER())
457
- except curses.error:
458
- pass
459
-
460
- def wrap_text(text, width):
461
- lines = []
462
- for paragraph in text.split("\n"):
463
- if len(paragraph) <= width:
464
- lines.append(paragraph)
465
- else:
466
- wrapped = textwrap.wrap(paragraph, width)
467
- lines.extend(wrapped if wrapped else [""])
468
- return lines
469
-
470
- def get_stats(status):
471
- good = sum(1 for v in status.values() if v == 2)
472
- ok = sum(1 for v in status.values() if v == 1)
473
- bad = sum(1 for v in status.values() if v == 0)
474
- unseen = 76 - good - ok - bad
475
- return good, ok, bad, unseen
476
-
477
- def draw_stats_bar(win, y, status):
478
- good, ok, bad, unseen = get_stats(status)
479
- h, w = win.getmaxyx()
480
- bar = (
481
- f" ✓ Maîtrisés:{good:3} "
482
- f"~ Hésitants:{ok:3} "
483
- f"✗ À revoir:{bad:3} "
484
- f"? Non vus:{unseen:3} "
485
- f"Total: 76"
486
- )
487
- safe_addstr(win, y, 0, bar.ljust(w), C_NORMAL())
488
 
489
- # ══════════════════════════════════════════════════════════
490
- # ÉCRAN MENU PRINCIPAL
491
- # ══════════════════════════════════════════════════════════
492
- def menu_screen(stdscr, status):
493
- options = [
494
- ("1", "Flashcards", "Révision active par carte — toutes chapitres"),
495
- ("2", "Flashcards CH.3", "Chapitre 3 : Mobilité horizontale"),
496
- ("3", "Flashcards CH.4", "Chapitre 4 : Zones de divergence"),
497
- ("4", "Flashcards CH.5", "Chapitre 5 : Zones de convergence"),
498
- ("5", "Flashcards Synthèse", "Cycle lithosphérique complet"),
499
- ("6", "Flashcards À revoir", "Seulement les cartes non maîtrisées"),
500
- ("7", "Quiz QCM", "Entraînement avec 4 propositions"),
501
- ("8", "Récapitulatif", "Toutes les 76 notions en lecture"),
502
- ("9", "Carte de progression", "Visualisation de l'avancement"),
503
- ("0", "Réinitialiser", "Effacer la progression"),
504
- ("Q", "Quitter", ""),
505
- ]
506
- sel = 0
507
- while True:
508
- stdscr.clear()
509
- h, w = stdscr.getmaxyx()
510
- draw_header(stdscr, "MENU PRINCIPAL")
511
-
512
- # Logo
513
- logo = [
514
- "╔═══════════════════════════════════════════════════╗",
515
- "║ GÉO-RÉVISION — Tectonique des Plaques ║",
516
- "║ 76 notions · CH.3 · CH.4 · CH.5 · Synthèse ║",
517
- "║ Méthode : Ebbinghaus + Testing Effect ║",
518
- "╚═══════════════════════════════════════════════════╝",
519
- ]
520
- for i, line in enumerate(logo):
521
- safe_addstr(stdscr, 2+i, max(0,(w-len(line))//2), line, C_TITLE())
522
-
523
- # Stats
524
- draw_stats_bar(stdscr, 8, status)
525
-
526
- # Options
527
- for i, (key, name, desc) in enumerate(options):
528
- y = 10 + i
529
- attr = C_SEL() if i == sel else C_NORMAL()
530
- prefix = "▶ " if i == sel else " "
531
- line = f"{prefix}[{key}] {name:<30} {desc}"
532
- safe_addstr(stdscr, y, 2, line[:w-4], attr)
533
-
534
- safe_addstr(stdscr, h-2, 2, "↑↓ pour naviguer · ENTRÉE pour sélectionner", C_MAGENTA())
535
-
536
- k = stdscr.getch()
537
- if k == curses.KEY_UP:
538
- sel = (sel - 1) % len(options)
539
- elif k == curses.KEY_DOWN:
540
- sel = (sel + 1) % len(options)
541
- elif k in (curses.KEY_ENTER, 10, 13):
542
- key = options[sel][0]
543
- return key
544
- elif k in range(256):
545
- c = chr(k).upper()
546
- for i, (key, _, _) in enumerate(options):
547
- if c == key.upper():
548
- return key
549
 
550
- # ══════════════════════════════════════════════════════════
551
- # FLASHCARD
552
- # ══════════════════════════════════════════════════════════
553
- def flashcard_screen(stdscr, status, cards, title="Flashcards"):
554
- if not cards:
555
- stdscr.clear()
556
- draw_header(stdscr, title)
557
- safe_addstr(stdscr, 5, 4, "Aucune carte dans ce filtre.", C_YELLOW())
558
- safe_addstr(stdscr, 7, 4, "Appuie sur une touche pour revenir.", C_NORMAL())
559
- stdscr.getch()
560
- return status
561
-
562
- deck = cards[:]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
  random.shuffle(deck)
564
- idx = 0
565
- flipped = False
566
-
567
- while True:
568
- card = deck[idx]
569
- stdscr.clear()
570
- h, w = stdscr.getmaxyx()
571
- draw_header(stdscr, f"{title} [{idx+1}/{len(deck)}]")
572
- draw_stats_bar(stdscr, 1, status)
573
-
574
- # Barre progression
575
- pct = (idx + 1) / len(deck)
576
- bar_w = min(w - 4, 60)
577
- filled = int(pct * bar_w)
578
- bar = "█" * filled + "░" * (bar_w - filled)
579
- safe_addstr(stdscr, 2, 2, f"[{bar}] {int(pct*100)}%", C_CYAN() if hasattr(curses, 'COLOR_CYAN') else C_NORMAL())
580
-
581
- # Encart chapitre
582
- safe_addstr(stdscr, 4, 2, f"■ {card['section']}", ch_color(card["ch"]))
583
-
584
- # Statut de la carte
585
- s = status.get(card["id"])
586
- s_colors = {None: C_NORMAL(), 0: C_RED(), 1: C_YELLOW(), 2: C_GREEN()}
587
- s_label = STATUS_LABELS.get(s, "Non vu")
588
- safe_addstr(stdscr, 4, w-20, f"[#{card['id']:02d}] {s_label}", s_colors.get(s, C_NORMAL()))
589
-
590
- if not flipped:
591
- # Recto : question
592
- draw_box(stdscr, 5, 1, min(12, h-14), w-2, "QUESTION", C_TITLE())
593
- q_lines = wrap_text(card["q"], w-6)
594
- for i, line in enumerate(q_lines[:6]):
595
- safe_addstr(stdscr, 7+i, 4, line, C_NORMAL() | curses.A_BOLD)
596
- if card.get("hint"):
597
- safe_addstr(stdscr, 13, 4, f"💡 {card['hint']}", C_MAGENTA())
598
- safe_addstr(stdscr, 15, 4, "[ ESPACE ] Révéler la réponse", C_TITLE())
599
- safe_addstr(stdscr, 16, 4, "[ ← / → ] Carte précédente / suivante [ M ] Mélanger [ Q ] Menu", C_MAGENTA())
600
- else:
601
- # Verso : réponse
602
- draw_box(stdscr, 5, 1, 3, w-2, "QUESTION", C_YELLOW())
603
- q_short = card["q"][:w-6]
604
- safe_addstr(stdscr, 6, 4, q_short, C_YELLOW())
605
-
606
- draw_box(stdscr, 8, 1, min(11, h-16), w-2, "RÉPONSE", C_GREEN())
607
- a_lines = wrap_text(card["a"], w-6)
608
- for i, line in enumerate(a_lines[:7]):
609
- safe_addstr(stdscr, 10+i, 4, line, C_NORMAL())
610
-
611
- # Boutons notation
612
- y_btn = h - 6
613
- safe_addstr(stdscr, y_btn, 2, "[ 1 ] ✗ À REVOIR", C_RED())
614
- safe_addstr(stdscr, y_btn, 25, "[ 2 ] ~ HÉSITANT", C_YELLOW())
615
- safe_addstr(stdscr, y_btn, 50, "[ 3 ] ✓ MAÎTRISÉ", C_GREEN())
616
- safe_addstr(stdscr, y_btn+1, 2, "[ ← / → ] Naviguer [ ESPACE ] Retourner [ Q ] Menu", C_MAGENTA())
617
-
618
- k = stdscr.getch()
619
-
620
- if k == ord(' '):
621
- flipped = not flipped
622
- elif k == curses.KEY_RIGHT or k == ord('n') or k == ord('N'):
623
- idx = (idx + 1) % len(deck)
624
- flipped = False
625
- elif k == curses.KEY_LEFT or k == ord('p') or k == ord('P'):
626
- idx = (idx - 1) % len(deck)
627
- flipped = False
628
- elif k == ord('m') or k == ord('M'):
629
- random.shuffle(deck)
630
- idx = 0
631
- flipped = False
632
- elif k == ord('1') and flipped:
633
- status[card["id"]] = 0
634
- save_progress(status)
635
- flipped = False
636
- idx = (idx + 1) % len(deck)
637
- elif k == ord('2') and flipped:
638
- status[card["id"]] = 1
639
- save_progress(status)
640
- flipped = False
641
- idx = (idx + 1) % len(deck)
642
- elif k == ord('3') and flipped:
643
- status[card["id"]] = 2
644
- save_progress(status)
645
- flipped = False
646
- idx = (idx + 1) % len(deck)
647
- elif k == ord('q') or k == ord('Q'):
648
- break
649
-
650
- return status
651
-
652
- def C_CYAN():
653
- return curses.color_pair(2)
654
 
655
- # ══════════════════════════════════════════════════════════
656
- # QUIZ QCM
657
- # ══════════════════════════════════════════════════════════
658
- def build_quiz_question(card):
659
- """Crée une question QCM à partir d'une carte."""
660
- first_line = card["a"].split("\n")[0].strip()
661
- # Nettoie les annotations type "1. "
662
- correct = re.sub(r"^\d+\.\s*", "", first_line).strip()
663
- if len(correct) > 70:
664
- correct = correct[:70] + "..."
665
-
666
- # Génère 3 mauvaises réponses différentes
667
  pool = [w for w in WRONG_POOL if w.lower() not in correct.lower()]
668
  random.shuffle(pool)
669
- wrongs = pool[:3]
670
-
671
- options = wrongs + [correct]
672
- random.shuffle(options)
673
- return {
674
- "question": card["q"],
675
- "correct": correct,
676
- "options": options,
677
- "explanation": card["a"],
678
- "section": card["section"],
679
- "ch": card["ch"],
680
- }
681
 
682
- def quiz_screen(stdscr, status, cards=None):
683
- if cards is None:
684
- cards = CARDS[:]
685
- random.shuffle(cards)
686
- score = 0
687
- total = len(cards)
688
- wrong_ids = []
689
-
690
- for qi, card in enumerate(cards):
691
- qdata = build_quiz_question(card)
692
- sel = 0
693
- answered = False
694
- chosen = None
695
-
696
- while True:
697
- stdscr.clear()
698
- h, w = stdscr.getmaxyx()
699
- draw_header(stdscr, f"QUIZ [{qi+1}/{total}] Score: {score}/{qi}")
700
- draw_stats_bar(stdscr, 1, status)
701
-
702
- # Section
703
- safe_addstr(stdscr, 3, 2, f"■ {qdata['section']}", ch_color(qdata["ch"]))
704
-
705
- # Question
706
- draw_box(stdscr, 4, 1, 4, w-2, "QUESTION", C_TITLE())
707
- q_lines = wrap_text(qdata["question"], w-6)
708
- for i, line in enumerate(q_lines[:2]):
709
- safe_addstr(stdscr, 5+i, 4, line, C_NORMAL() | curses.A_BOLD)
710
-
711
- # Options
712
- for i, opt in enumerate(qdata["options"]):
713
- y = 9 + i * 2
714
- prefix = " "
715
- if not answered:
716
- attr = C_SEL() if i == sel else C_NORMAL()
717
- prefix = " " if i == sel else " "
718
- else:
719
- if opt == qdata["correct"]:
720
- attr = C_GREEN()
721
- prefix = " "
722
- elif opt == chosen and chosen != qdata["correct"]:
723
- attr = C_RED()
724
- prefix = "✗ "
725
- else:
726
- attr = C_NORMAL()
727
- prefix = " "
728
-
729
- opt_short = opt[:w-10]
730
- safe_addstr(stdscr, y, 2, f"{prefix}[{'ABCD'[i]}] {opt_short}", attr)
731
-
732
- if answered:
733
- # Explication
734
- exp_y = 18
735
- if chosen == qdata["correct"]:
736
- safe_addstr(stdscr, exp_y, 2, "✓ BONNE RÉPONSE !", C_GREEN())
737
- else:
738
- safe_addstr(stdscr, exp_y, 2, f" Réponse correcte : {qdata['correct'][:w-30]}", C_RED())
739
- exp_lines = wrap_text(qdata["explanation"], w-6)
740
- for i, line in enumerate(exp_lines[:3]):
741
- safe_addstr(stdscr, exp_y+1+i, 4, line, C_MAGENTA())
742
- safe_addstr(stdscr, h-2, 2, "[ ENTRÉE ou ESPACE ] Question suivante [ Q ] Arrêter", C_TITLE())
743
- else:
744
- safe_addstr(stdscr, h-2, 2, "[ ↑↓ ] Naviguer [ ENTRÉE ] Valider [ Q ] Arrêter", C_TITLE())
745
-
746
- k = stdscr.getch()
747
-
748
- if not answered:
749
- if k == curses.KEY_UP:
750
- sel = (sel - 1) % 4
751
- elif k == curses.KEY_DOWN:
752
- sel = (sel + 1) % 4
753
- elif k in range(ord('a'), ord('e')):
754
- sel = k - ord('a')
755
- elif k in range(ord('A'), ord('E')):
756
- sel = k - ord('A')
757
- elif k in (curses.KEY_ENTER, 10, 13, ord(' ')):
758
- answered = True
759
- chosen = qdata["options"][sel]
760
- if chosen == qdata["correct"]:
761
- score += 1
762
- status[card["id"]] = max(status.get(card["id"], 0) or 0, 2)
763
- else:
764
- wrong_ids.append(card["id"])
765
- status[card["id"]] = 0
766
- save_progress(status)
767
- elif k == ord('q') or k == ord('Q'):
768
- break
769
- else:
770
- if k in (curses.KEY_ENTER, 10, 13, ord(' ')):
771
- break
772
- elif k == ord('q') or k == ord('Q'):
773
- # Montre résultat intermédiaire
774
- qi = total
775
- break
776
-
777
- if k == ord('q') or k == ord('Q'):
778
- break
779
-
780
- # Résultat final
781
- stdscr.clear()
782
- h, w = stdscr.getmaxyx()
783
- draw_header(stdscr, "RÉSULTAT DU QUIZ")
784
-
785
- pct = int(score / total * 100) if total > 0 else 0
786
- color = C_GREEN() if pct >= 80 else C_YELLOW() if pct >= 50 else C_RED()
787
-
788
- draw_box(stdscr, 3, w//2-20, 12, 40, "SCORE FINAL", C_TITLE())
789
- score_str = f"{pct}%"
790
- safe_addstr(stdscr, 6, w//2 - len(score_str)//2, score_str, color | curses.A_BOLD)
791
- detail = f"{score} bonnes réponses sur {total}"
792
- safe_addstr(stdscr, 8, w//2 - len(detail)//2, detail, C_NORMAL())
793
-
794
- msg = ("🏆 Excellent ! Chapitres maîtrisés !" if pct >= 80 else
795
- "👍 Bien ! Quelques révisions restent utiles." if pct >= 60 else
796
- "⚠ Des lacunes — retourne aux flashcards !" if pct >= 40 else
797
- "📚 Reprends les flashcards et révise tout !")
798
- safe_addstr(stdscr, 10, w//2 - len(msg)//2, msg[:w-4], C_TITLE())
799
-
800
- if wrong_ids:
801
- safe_addstr(stdscr, 16, 2, f"Cartes à revoir : {', '.join(f'#{i}' for i in wrong_ids[:15])}", C_RED())
802
-
803
- safe_addstr(stdscr, h-2, 2, "Appuie sur une touche pour revenir au menu.", C_MAGENTA())
804
- stdscr.getch()
805
- return status
806
-
807
- # ══════════════════════════════════════════════════════════
808
- # RÉCAPITULATIF
809
- # ══════════════════════════════════════════════════════════
810
- def recap_screen(stdscr, status):
811
- lines = []
812
- groups = [
813
- ("3", "═══ CHAPITRE 3 — Mobilité horizontale des plaques ═══"),
814
- ("4", "═══ CHAPITRE 4 — Dynamique des zones de divergence ═══"),
815
- ("5", "═══ CHAPITRE 5 — Dynamique des zones de convergence ═══"),
816
- ("synth", "═══ SYNTHÈSE — Cycle de la lithosphère océanique ═══"),
817
- ]
818
- for ch, title in groups:
819
- lines.append(("title", ch, title))
820
- for c in CARDS:
821
- if c["ch"] == ch:
822
- lines.append(("section", ch, f" ─ {c['section']}"))
823
- lines.append(("question", ch, f" #{c['id']:02d} Q: {c['q']}"))
824
- for al in c["a"].split("\n"):
825
- lines.append(("answer", ch, f" → {al}"))
826
- lines.append(("sep", ch, ""))
827
-
828
- scroll = 0
829
- while True:
830
- stdscr.clear()
831
- h, w = stdscr.getmaxyx()
832
- draw_header(stdscr, "RÉCAPITULATIF COMPLET — 76 notions")
833
- draw_stats_bar(stdscr, 1, status)
834
-
835
- visible = h - 5
836
- for i, (ltype, ch, text) in enumerate(lines[scroll:scroll+visible]):
837
- y = 3 + i
838
- text_trunc = text[:w-2]
839
- if ltype == "title":
840
- safe_addstr(stdscr, y, 0, text_trunc.ljust(w-1), ch_color(ch) | curses.A_BOLD)
841
- elif ltype == "section":
842
- safe_addstr(stdscr, y, 0, text_trunc, C_CYAN())
843
- elif ltype == "question":
844
- safe_addstr(stdscr, y, 0, text_trunc, C_NORMAL() | curses.A_BOLD)
845
- elif ltype == "answer":
846
- safe_addstr(stdscr, y, 0, text_trunc, C_MAGENTA())
847
- else:
848
- pass
849
-
850
- pct_scroll = int(scroll / max(1, len(lines) - visible) * 100)
851
- safe_addstr(stdscr, h-1, 2, f"↑↓/PgUp/PgDn : défiler ({pct_scroll}%) Q : menu", C_TITLE())
852
-
853
- k = stdscr.getch()
854
- if k == curses.KEY_UP:
855
- scroll = max(0, scroll - 1)
856
- elif k == curses.KEY_DOWN:
857
- scroll = min(len(lines) - visible, scroll + 1)
858
- elif k == curses.KEY_PPAGE:
859
- scroll = max(0, scroll - visible)
860
- elif k == curses.KEY_NPAGE:
861
- scroll = min(len(lines) - visible, scroll + visible)
862
- elif k == ord('q') or k == ord('Q'):
863
- break
864
-
865
- # ══════════════════════════════════════════════════════════
866
- # CARTE DE PROGRESSION
867
- # ══════════════════════════════════════════════════════════
868
- def map_screen(stdscr, status):
869
- while True:
870
- stdscr.clear()
871
- h, w = stdscr.getmaxyx()
872
- draw_header(stdscr, "CARTE DE PROGRESSION")
873
- draw_stats_bar(stdscr, 1, status)
874
-
875
- safe_addstr(stdscr, 3, 2, "Légende :", C_NORMAL() | curses.A_BOLD)
876
- safe_addstr(stdscr, 3, 14, "[██] Non vu", C_NORMAL())
877
- safe_addstr(stdscr, 3, 28, "[██] À revoir", C_RED())
878
- safe_addstr(stdscr, 3, 44, "[~~] Hésitant", C_YELLOW())
879
- safe_addstr(stdscr, 3, 60, "[OK] Maîtrisé", C_GREEN())
880
-
881
- # Grille
882
- cols = min(19, (w - 4) // 5)
883
- for c in CARDS:
884
- idx = c["id"] - 1
885
- row = idx // cols
886
- col = idx % cols
887
- y = 5 + row * 2
888
- x = 2 + col * 5
889
- if y >= h - 3:
890
- break
891
- s = status.get(c["id"])
892
- if s == 2:
893
- attr = C_GREEN()
894
- label = f"OK "
895
- elif s == 1:
896
- attr = C_YELLOW()
897
- label = f"~~ "
898
- elif s == 0:
899
- attr = C_RED()
900
- label = f"✗ "
901
- else:
902
- attr = C_NORMAL()
903
- label = f" "
904
- safe_addstr(stdscr, y, x, f"{c['id']:02d}", attr | curses.A_BOLD)
905
- safe_addstr(stdscr, y+1, x, label, attr)
906
-
907
- # Résumé par chapitre
908
- y_sum = h - 8
909
- safe_addstr(stdscr, y_sum, 2, "Par chapitre :", C_TITLE())
910
- for ch in ["3", "4", "5", "synth"]:
911
- ch_cards = [c for c in CARDS if c["ch"] == ch]
912
- ch_good = sum(1 for c in ch_cards if status.get(c["id"]) == 2)
913
- ch_ok = sum(1 for c in ch_cards if status.get(c["id"]) == 1)
914
- line = f"{CH_LABELS[ch]:20s} ✓{ch_good:3d} ~{ch_ok:3d} /{len(ch_cards)}"
915
- safe_addstr(stdscr, y_sum+1 + list(["3","4","5","synth"]).index(ch),
916
- 4, line, ch_color(ch))
917
-
918
- safe_addstr(stdscr, h-1, 2, "Q : revenir au menu", C_MAGENTA())
919
-
920
- k = stdscr.getch()
921
- if k == ord('q') or k == ord('Q'):
922
- break
923
 
924
  # ════════════════════════════════════════════════���═════════
925
- # RÉINITIALISATION
926
  # ══════════════════════════════════════════════════════════
927
- def confirm_reset(stdscr, status):
928
- stdscr.clear()
929
- h, w = stdscr.getmaxyx()
930
- draw_header(stdscr, "RÉINITIALISATION")
931
- safe_addstr(stdscr, 5, 4, "⚠ Effacer toute la progression ?", C_RED())
932
- safe_addstr(stdscr, 7, 4, "[ O ] Oui, effacer [ N ] Annuler", C_NORMAL())
933
- k = stdscr.getch()
934
- if k in (ord('o'), ord('O')):
935
- status.clear()
936
- if os.path.exists(SAVE_FILE):
937
- os.remove(SAVE_FILE)
938
- safe_addstr(stdscr, 9, 4, "Progression effacée.", C_GREEN())
939
- stdscr.refresh()
940
- curses.napms(1000)
941
- return status
942
 
943
  # ══════════════════════════════════════════════════════════
944
- # MAIN
945
  # ══════════════════════════════════════════════════════════
946
- def main(stdscr):
947
- curses.curs_set(0)
948
- stdscr.keypad(True)
949
- init_colors()
950
- status = load_progress()
951
-
952
- while True:
953
- choice = menu_screen(stdscr, status)
954
-
955
- if choice == "1":
956
- status = flashcard_screen(stdscr, status, CARDS[:], "Flashcards — Tout")
957
- elif choice == "2":
958
- cards = [c for c in CARDS if c["ch"] == "3"]
959
- status = flashcard_screen(stdscr, status, cards, "Flashcards — CH.3")
960
- elif choice == "3":
961
- cards = [c for c in CARDS if c["ch"] == "4"]
962
- status = flashcard_screen(stdscr, status, cards, "Flashcards — CH.4")
963
- elif choice == "4":
964
- cards = [c for c in CARDS if c["ch"] == "5"]
965
- status = flashcard_screen(stdscr, status, cards, "Flashcards — CH.5")
966
- elif choice == "5":
967
- cards = [c for c in CARDS if c["ch"] == "synth"]
968
- status = flashcard_screen(stdscr, status, cards, "Flashcards — Synthèse")
969
- elif choice == "6":
970
- cards = [c for c in CARDS if status.get(c["id"]) in (None, 0)]
971
- if not cards:
972
- cards = CARDS[:]
973
- status = flashcard_screen(stdscr, status, cards, "Flashcards — À revoir")
974
- elif choice == "7":
975
- status = quiz_screen(stdscr, status)
976
- elif choice == "8":
977
- recap_screen(stdscr, status)
978
- elif choice == "9":
979
- map_screen(stdscr, status)
980
- elif choice == "0":
981
- status = confirm_reset(stdscr, status)
982
- elif choice in ("Q", "q"):
983
- break
984
-
985
- save_progress(status)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
986
 
987
  if __name__ == "__main__":
988
- curses.wrapper(main)
 
1
  #!/usr/bin/env python3
2
  """
3
+ GÉO-RÉVISION — Tectonique des Plaques
4
+ Application Gradio compatible HuggingFace Spaces
5
+ 76 notions · CH.3 · CH.4 · CH.5 · Synthèse
 
 
6
  """
7
 
8
+ import gradio as gr
9
  import random
 
 
10
  import re
 
11
 
12
  # ══════════════════════════════════════════════════════════
13
  # DONNÉES — 76 cartes
14
  # ══════════════════════════════════════════════════════════
15
  CARDS = [
 
16
  {"id": 1, "ch": "3", "section": "CH.3 — Limites de plaques",
17
  "q": "Quels sont les 3 grands types de limites de plaques ?",
18
+ "hint": "💡 Pense : naissance / destruction / collision",
19
+ "a": "**1. DORSALES** (divergence / accrétion océanique)\n**2. FOSSES OCÉANIQUES** (convergence / subduction)\n**3. CHAÎNES DE MONTAGNES** (convergence / collision)"},
20
  {"id": 2, "ch": "3", "section": "CH.3 — Dorsales",
21
  "q": "Quelles roches sont produites à la dorsale ? Quelle est leur texture ?",
22
+ "hint": "💡 Deux roches, deux textures",
23
+ "a": "**BASALTE** : texture microlithique (refroidissement *rapide* en surface)\n**GABBRO** : texture grenue (refroidissement *lent* en profondeur)"},
24
  {"id": 3, "ch": "3", "section": "CH.3 — Dorsales",
25
  "q": "Quel type de failles caractérise les dorsales ? Pourquoi ?",
26
+ "hint": "💡 Extension ou compression ?",
27
+ "a": "**FAILLES NORMALES** car la divergence crée une **EXTENSION** (étirement) de la croûte."},
28
  {"id": 4, "ch": "3", "section": "CH.3 — Dorsales",
29
  "q": "Caractéristiques sismiques et thermiques d'une dorsale ?",
30
+ "hint": "💡 Profondeur + intensité + flux",
31
+ "a": "Sismicité **SUPERFICIELLE** et **FAIBLE MAGNITUDE**.\nFlux géothermique **ÉLEVÉ** (magmatisme ascendant)."},
32
  {"id": 5, "ch": "3", "section": "CH.3 — Fosses",
33
  "q": "Quelles roches plutoniques et volcaniques sont produites en subduction ?",
34
+ "hint": "💡 4 roches, 2 textures différentes",
35
+ "a": "Plutoniques (grenues) : **DIORITE**, **GRANITE**\nVolcaniques (microlithiques) : **ANDÉSITE**, **RHYOLITE**"},
36
  {"id": 6, "ch": "3", "section": "CH.3 — Fosses",
37
  "q": "Comment se répartissent les foyers sismiques en zone de subduction ? Jusqu'où ?",
38
+ "hint": "💡 Un plan célèbre...",
39
+ "a": "Plan de **WADATI-BENIOFF** : plan incliné suivant la plaque plongeante.\nAtteint **700 KM** de profondeur."},
40
  {"id": 7, "ch": "3", "section": "CH.3 — Fosses",
41
  "q": "Décris le flux géothermique en zone de subduction (deux zones).",
42
+ "hint": "💡 Froid ici, chaud là-bas",
43
+ "a": "**BAS** au niveau de la plaque froide qui plonge.\n**FORT** au niveau de l'arc volcanique sus-jacent."},
44
  {"id": 8, "ch": "3", "section": "CH.3 — Collision",
45
  "q": "Caractéristiques géologiques et sismiques d'une zone de collision ?",
46
+ "hint": "💡 Type de failles + profondeur séismes",
47
+ "a": "**FAILLES INVERSES**, flux géothermique **FAIBLE**,\nsismicité principalement **SUPERFICIELLE**."},
48
  {"id": 9, "ch": "3", "section": "CH.3 — Points chauds",
49
  "q": "Comment un alignement de volcans inactifs prouve-t-il le mouvement des plaques ?",
50
+ "hint": "💡 Âge + distance",
51
+ "a": "Le point chaud est **FIXE** dans le manteau profond.\nL'âge des volcans **AUGMENTE avec la distance** au volcan actif."},
52
  {"id": 10, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
53
  "q": "Qu'est-ce qu'une anomalie magnétique positive vs négative ?",
54
+ "hint": "💡 Intensité mesurée vs moyenne",
55
+ "a": "**POSITIVE** : intensité > moyenne → polarité **NORMALE**\n**NÉGATIVE** : intensité < moyenne → polarité **INVERSE** (pôles inversés)"},
56
  {"id": 11, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
57
  "q": "Décris le patron des anomalies magnétiques autour d'une dorsale.",
58
+ "hint": "💡 Symétrie + image",
59
+ "a": "Bandes **PARALLÈLES ET SYMÉTRIQUES** par rapport à l'axe de la dorsale.\nComme un **DOUBLE TAPIS ROULANT**.\nBasaltes d'autant plus vieux qu'ils sont éloignés."},
60
  {"id": 12, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
61
  "q": "Que révèle l'âge et l'épaisseur des sédiments sur le plancher océanique ?",
62
+ "hint": "💡 Loi simple distance/âge",
63
+ "a": "Épaisseur et âge des sédiments **AUGMENTENT** avec la distance à la dorsale.\n→ Preuve de l'**expansion océanique**."},
64
  {"id": 13, "ch": "3", "section": "CH.3 — Géodésie spatiale",
65
  "q": "Que permet la géodésie spatiale (GPS) en tectonique ?",
66
+ "hint": "💡 Précision + deux types de vitesses",
67
+ "a": "Localisation au **MILLIMÈTRE PRÈS**.\nCalcul des vitesses **ABSOLUES** (repères fixes) et **RELATIVES** (entre deux plaques)."},
68
  {"id": 14, "ch": "3", "section": "CH.3 — Résumé frontières",
69
  "q": "Quelle frontière présente une vallée axiale (rift) ? Quel mouvement ?",
70
+ "hint": "💡 Rift = fossé d'effondrement",
71
+ "a": "La **DORSALE** : mouvement de **DIVERGENCE**.\nLa vallée axiale (rift) témoigne de l'extension de la croûte."},
72
  {"id": 15, "ch": "3", "section": "CH.3 — Structure générale",
73
  "q": "Lithosphère terrestre : structure de base et activité aux frontières.",
74
+ "hint": "💡 Découpe + concentration",
75
+ "a": "Découpée en **PLAQUES LITHOSPHÉRIQUES RIGIDES**.\nFrontières concentrent l'**ACTIVITÉ SISMIQUE ET VOLCANIQUE**."},
76
  {"id": 16, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
77
  "q": "Comment les basaltes enregistrent-ils le champ magnétique ?",
78
+ "hint": "💡 Moment clé = refroidissement",
79
+ "a": "En se **REFROIDISSANT**, les minéraux magnétiques (magnétite) se figent dans la direction du champ magnétique terrestre du moment."},
80
  {"id": 17, "ch": "3", "section": "CH.3 — Dorsales",
81
  "q": "Que produit l'accrétion océanique à la dorsale ?",
82
+ "hint": "💡 Naît quoi ?",
83
+ "a": "La **CROÛTE OCÉANIQUE** :\n- Basalte en surface\n- Gabbro en profondeur\n- Péridotite mantellique sous-jacente"},
84
  {"id": 18, "ch": "3", "section": "CH.3 — Fosses",
85
  "q": "Qu'est-ce qu'une fosse océanique ? Quel processus y est associé ?",
86
+ "hint": "💡 Plongement...",
87
+ "a": "Dépression profonde marquant une **ZONE DE SUBDUCTION** :\nplongement d'une plaque océanique dense dans le manteau."},
88
  {"id": 19, "ch": "3", "section": "CH.3 — Collision",
89
  "q": "Qu'est-ce qu'une chaîne de montagnes en termes tectoniques ?",
90
+ "hint": "💡 Fermeture d'un océan",
91
+ "a": "Zone de **COLLISION CONTINENTALE** : résultat de la convergence de deux plaques continentales après fermeture d'un océan."},
 
92
  {"id": 20, "ch": "4", "section": "CH.4 — Dorsales rapides",
93
  "q": "Quelle est la vitesse d'écartement d'une dorsale rapide ?",
94
+ "hint": "💡 10–16 cm/an — Quel océan ?",
95
+ "a": "**10 à 16 CM/AN**.\nExemple : dorsale du **Pacifique Est** (Pacifique)."},
96
  {"id": 21, "ch": "4", "section": "CH.4 — Dorsales rapides",
97
  "q": "Morphologie d'une dorsale rapide : rift, bombement ?",
98
+ "hint": "💡 Opposé de la dorsale lente",
99
+ "a": "**BOMBEMENT LARGE** mais **VALLÉE AXIALE QUASI ABSENTE** (magmatisme abondant comble le rift)."},
100
  {"id": 22, "ch": "4", "section": "CH.4 — Dorsales rapides",
101
+ "q": "Décris le processus de fusion partielle sous une dorsale (3 étapes).",
102
+ "hint": "💡 Convection → décompression → solidus",
103
+ "a": "**1.** Remontée des péridotites par **CONVECTION**\n**2.** **DÉCOMPRESSION ADIABATIQUE** : le géotherme recoupe le solidus\n**3.** **FUSION PARTIELLE** (~15%) → gouttelettes de magma"},
104
  {"id": 23, "ch": "4", "section": "CH.4 — Dorsales rapides",
105
  "q": "Où se loge la chambre magmatique ? Quelles roches en sortent ?",
106
+ "hint": "💡 Profondeur + 2 roches",
107
+ "a": "Entre **2 et 7 KM** de profondeur.\nRefroidissement lent → **GABBROS** (grenu)\nRefroidissement rapide → **BASALTES** (microlithique)"},
108
  {"id": 24, "ch": "4", "section": "CH.4 — Dorsales rapides",
109
  "q": "Quelle épaisseur de croûte produit une dorsale rapide ?",
110
+ "hint": "💡 Chiffre précis en km",
111
+ "a": "Croûte océanique régulière de **5 À 7 KM** d'épaisseur."},
112
  {"id": 25, "ch": "4", "section": "CH.4 — Dorsales lentes",
113
  "q": "Quelle est la vitesse d'écartement d'une dorsale lente ?",
114
+ "hint": "💡 1–5 cm/an — Quel océan ?",
115
+ "a": "**1 à 5 CM/AN**.\nExemple : dorsale **médio-atlantique** (Atlantique)."},
116
  {"id": 26, "ch": "4", "section": "CH.4 — Dorsales lentes",
117
  "q": "Morphologie d'une dorsale lente : rift, magmatisme, expansion.",
118
+ "hint": "💡 Opposé de rapide",
119
+ "a": "**RIFT TRÈS MARQUÉ** + grandes failles de détachement.\nMagmatisme **RÉDUIT ET DISCONTINU**.\nExpansion parfois assurée par **ÉTIREMENT TECTONIQUE**."},
120
  {"id": 27, "ch": "4", "section": "CH.4 — Dorsales lentes",
121
  "q": "Qu'est-ce que la serpentinisation et quand se produit-elle ?",
122
+ "hint": "💡 Roche + eau + dorsale lente",
123
+ "a": "Hydratation des **PÉRIDOTITES** (manteau) remontées en surface.\nL'eau transforme l'olivine en **SERPENTINE**."},
124
  {"id": 28, "ch": "4", "section": "CH.4 — Vieillissement",
125
  "q": "Qu'est-ce que le métamorphisme hydrothermal ?",
126
+ "hint": "💡 Eau + chaleur + minéraux",
127
+ "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**."},
128
  {"id": 29, "ch": "4", "section": "CH.4 — Vieillissement",
129
  "q": "Séquence de faciès métamorphiques lors du vieillissement océanique ?",
130
+ "hint": "💡 Deux températures, deux faciès",
131
+ "a": "**> 700°C** : faciès **AMPHIBOLITE** (hornblende)\n**500–700°C** : faciès **SCHISTE VERT** (chlorite + actinote)"},
132
  {"id": 30, "ch": "4", "section": "CH.4 — Vieillissement",
133
  "q": "Comment évolue la densité de la lithosphère océanique avec le temps ?",
134
+ "hint": "💡 Refroidissement → épaississement → densité",
135
+ "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."},
136
  {"id": 31, "ch": "4", "section": "CH.4 — Vieillissement",
137
  "q": "Où et comment naissent les fumeurs noirs ?",
138
+ "hint": "💡 Cycle eau de mer",
139
+ "a": "Eau de mer s'infiltre, se **RÉCHAUFFE** au contact du magma,\nse charge en minéraux et ressort : **SOURCES HYDROTHERMALES CHAUDES**."},
140
  {"id": 32, "ch": "4", "section": "CH.4 — Dorsales rapides",
141
  "q": "Qu'est-ce que la décompression adiabatique ? Pourquoi déclenche-t-elle la fusion ?",
142
+ "hint": "💡 Pas de perte de chaleur",
143
+ "a": "Remontée de matériel **SANS ÉCHANGE DE CHALEUR**.\nPression baisse → solidus baisse géotherme **RECOUPE LE SOLIDUS** → fusion."},
144
  {"id": 33, "ch": "4", "section": "CH.4 — Vieillissement",
145
  "q": "Quelle est la signification de l'isotherme 1300°C pour la lithosphère ?",
146
+ "hint": "💡 Limite LAB",
147
+ "a": "**LIMITE LITHOSPHÈRE-ASTHÉNOSPHÈRE (LAB)**.\nElle **S'APPROFONDIT** avec le refroidissement de la plaque éloignée de la dorsale."},
148
  {"id": 34, "ch": "4", "section": "CH.4 — Dorsales rapides",
149
  "q": "Basalte en coussin vs basalte en filon : quelle différence ?",
150
+ "hint": "💡 Eau vs fissure",
151
+ "a": "**COUSSIN** : refroidissement très rapide au contact de l'eau (lave sous-marine).\n**FILON (dyke)** : injection de magma dans des fissures verticales."},
152
  {"id": 35, "ch": "4", "section": "CH.4 — Vieillissement",
153
  "q": "Quelle minéralogie correspond au faciès schiste vert ?",
154
+ "hint": "💡 Température < 500°C",
155
+ "a": "**CHLORITE** + **ACTINOTE** (amphibole calcique) : minéraux hydratés de basse température."},
156
  {"id": 36, "ch": "4", "section": "CH.4 — Dorsales lentes",
157
  "q": "Pourquoi une dorsale lente a-t-elle un rift très marqué ?",
158
+ "hint": "💡 Magma insuffisant pour...",
159
+ "a": "Magmatisme trop faible pour **COMBLER L'ESPACE** créé par l'écartement.\nL'extension tectonique forme un **FOSSÉ D'EFFONDREMENT PROFOND**."},
160
  {"id": 37, "ch": "4", "section": "CH.4 — Vieillissement",
161
+ "q": "Transformation minérale lors de l'hydrothermalisme > 700°C ?",
162
+ "hint": "💡 Faciès amphibolite",
163
+ "a": "**Pyroxène + Plagioclase** (gabbro) → **HORNBLENDE** (amphibole)\nSous l'action de l'hydrothermalisme à > 700°C."},
164
  {"id": 38, "ch": "4", "section": "CH.4 — Dorsales rapides",
165
  "q": "La péridotite fond-elle entièrement sous une dorsale ? Quel pourcentage ?",
166
+ "hint": "💡 Partielle !",
167
+ "a": "Non : seulement **FUSION PARTIELLE (~15%)**.\nLe reste reste **SOLIDE**. C'est le liquide qui monte former la croûte."},
168
  {"id": 39, "ch": "4", "section": "CH.4 — Dorsales rapides",
169
  "q": "Rôle de la convection mantellique dans le fonctionnement d'une dorsale ?",
170
+ "hint": "💡 Mouvement ascendant",
171
+ "a": "La **CONVECTION** assure la **REMONTÉE DES PÉRIDOTITES** sous la dorsale,\ndéclenchant la décompression adiabatique et la fusion partielle."},
172
  {"id": 40, "ch": "4", "section": "CH.4 — Dorsales lentes",
173
  "q": "Qu'est-ce qu'une faille de détachement aux dorsales lentes ?",
174
+ "hint": "💡 Grande faille, faible pendage",
175
+ "a": "Grande **FAILLE À FAIBLE PENDAGE** (presque horizontale)\npermettant l'exhumation de péridotites directement sur le plancher océanique."},
 
176
  {"id": 41, "ch": "5", "section": "CH.5 — Marqueurs de subduction",
177
  "q": "Quels sont les 3 marqueurs principaux d'une zone de subduction ?",
178
+ "hint": "💡 1 relief / 1 alignement / 1 sismique",
179
+ "a": "**1.** FOSSE OCÉANIQUE profonde\n**2.** ALIGNEMENT VOLCANIQUE (arc insulaire ou cordillère)\n**3.** PLAN DE WADATI-BENIOFF (sismicité profonde)"},
180
  {"id": 42, "ch": "5", "section": "CH.5 — Marqueurs de subduction",
181
  "q": "Pourquoi la sismicité suit-elle un plan incliné en subduction ?",
182
+ "hint": "💡 Rigidité de la plaque",
183
+ "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**."},
184
  {"id": 43, "ch": "5", "section": "CH.5 — Magmatisme",
185
  "q": "Pourquoi le volcanisme de subduction est-il explosif ?",
186
+ "hint": "💡 Composition du magma",
187
+ "a": "Magma riche en **SILICE** (très visqueux) et en **GAZ DISSOUS**.\nViscosité élevée → dégazage brutal → **EXPLOSION VIOLENTE**."},
188
  {"id": 44, "ch": "5", "section": "CH.5 — Magmatisme",
189
  "q": "Quelle est la chaîne de réactions menant au volcanisme de subduction ?",
190
+ "hint": "💡 Eau → péridotite → magma",
191
+ "a": "**1.** Métamorphisme → libération d'**EAU**\n**2.** Eau monte dans le **COIN MANTELLIQUE**\n**3.** Abaisse le point de fusion des péridotites\n**4.** **FUSION PARTIELLE** → magma → volcanisme"},
192
  {"id": 45, "ch": "5", "section": "CH.5 — Magmatisme",
193
  "q": "Quelles sont les 4 roches de subduction et leur texture ?",
194
+ "hint": "💡 2 volcaniques + 2 plutoniques",
195
+ "a": "Volcaniques (microlithique) : **ANDÉSITE**, **RHYOLITE**\nPlutoniques (grenue) : **DIORITE**, **GRANITE**"},
196
  {"id": 46, "ch": "5", "section": "CH.5 — Magmatisme",
197
  "q": "Pourquoi existe-t-il une telle diversité de roches en subduction ?",
198
+ "hint": "💡 2 processus",
199
+ "a": "**CRISTALLISATION FRACTIONNÉE** (différenciation du magma)\n+ **CONTAMINATION** par la croûte continentale traversée."},
200
  {"id": 47, "ch": "5", "section": "CH.5 — Magmatisme",
201
  "q": "Quel est le signe minéralogique caractéristique des roches de subduction ?",
202
+ "hint": "💡 OH dans les minéraux",
203
+ "a": "Présence de **MINÉRAUX HYDRATÉS** portant des groupements **OH**\n(ex. : amphibole, biotite, muscovite)."},
204
  {"id": 48, "ch": "5", "section": "CH.5 — Métamorphisme",
205
  "q": "Séquence de faciès métamorphiques de la plaque subduite ?",
206
+ "hint": "💡 3 faciès successifs",
207
+ "a": "**SCHISTE VERT****SCHISTE BLEU** (glaucophane, HP-BT) → **ÉCLOGITE** (grenat + jadéite, > 50 km)"},
208
  {"id": 49, "ch": "5", "section": "CH.5 — Métamorphisme",
209
  "q": "Quelle minéralogie caractérise le faciès schiste bleu ?",
210
+ "hint": "💡 Minéral bleu, HP-BT",
211
+ "a": "**GLAUCOPHANE** (amphibole sodique bleue) :\nindicateur de conditions **HAUTE PRESSION – BASSE TEMPÉRATURE**."},
212
  {"id": 50, "ch": "5", "section": "CH.5 — Métamorphisme",
213
  "q": "Minéralogie du faciès éclogite et profondeur de formation ?",
214
+ "hint": "💡 > 50 km",
215
+ "a": "**GRENAT** + **JADÉITE** (pyroxène sodique)\nFormés à **> 50 KM** de profondeur, sous très haute pression."},
216
  {"id": 51, "ch": "5", "section": "CH.5 — Métamorphisme",
217
  "q": "En quoi le contexte P-T de la subduction est-il unique ?",
218
+ "hint": "💡 Comparer avec métamorphisme classique",
219
+ "a": "**HAUTE PRESSION / BASSE TEMPÉRATURE** :\nLa plaque s'enfonce vite et reste froide.\nContraire du métamorphisme de contact (BT/BP)."},
220
  {"id": 52, "ch": "5", "section": "CH.5 — Moteur",
221
  "q": "Quel est le moteur principal du mouvement des plaques ?",
222
+ "hint": "💡 Force de tirage",
223
+ "a": "**TRACTION DE LA PLAQUE SUBDUITE** (slab pull) :\nPlaque froide et dense → gravité → tire la plaque,\nentraîne les mouvements descendants de la convection mantellique."},
224
  {"id": 53, "ch": "5", "section": "CH.5 — Moteur",
225
  "q": "Quel lien y a-t-il entre subduction et convection mantellique ?",
226
+ "hint": "💡 Cause-conséquence",
227
+ "a": "La subduction **ENTRETIENT** les courants descendants froids de la convection.\nLes plaques ne sont pas portées passivement : elles **TIRENT ACTIVEMENT**."},
228
  {"id": 54, "ch": "5", "section": "CH.5 — Collision",
229
  "q": "Comment se produit une collision continentale ?",
230
+ "hint": "💡 Après quoi ?",
231
+ "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."},
232
  {"id": 55, "ch": "5", "section": "CH.5 — Collision",
233
  "q": "Quelles structures géologiques témoignent d'une collision en surface ?",
234
+ "hint": "💡 3 structures",
235
+ "a": "**PLIS** + **FAILLES INVERSES** + **NAPPES DE CHARRIAGE**\n(grandes masses de roches déplacées horizontalement)"},
236
  {"id": 56, "ch": "5", "section": "CH.5 — Collision",
237
  "q": "Qu'est-ce qu'une racine crustale ? À quelle profondeur descend le Moho ?",
238
+ "hint": "💡 Iceberg crustal",
239
+ "a": "Épaississement de la croûte vers le bas.\nLe **MOHO** peut descendre jusqu'à **70 KM** (normal : ~35 km)."},
240
  {"id": 57, "ch": "5", "section": "CH.5 — Collision",
241
  "q": "Quels types de roches métamorphiques se forment en collision ?",
242
+ "hint": "💡 Roches foliées",
243
+ "a": "Roches **FOLIÉES** :\n- **MICASCHISTES** (métamorphisme faible à modéré)\n- **GNEISS** (fort métamorphisme)"},
244
  {"id": 58, "ch": "5", "section": "CH.5 — Marqueurs",
245
  "q": "Différence entre arc insulaire et cordillère en subduction ?",
246
+ "hint": "💡 Océan-Océan vs Océan-Continent",
247
+ "a": "**ARC INSULAIRE** : subduction Océan/Océan → volcanisme en **MER**.\n**CORDILLÈRE** : subduction Océan/Continent → volcanisme en **BORDURE DE CONTINENT**."},
248
  {"id": 59, "ch": "5", "section": "CH.5 — Magmatisme",
249
  "q": "Qu'est-ce que le coin mantellique et quel est son rôle ?",
250
+ "hint": "💡 Wedge",
251
+ "a": "Portion du **MANTEAU DE LA PLAQUE CHEVAUCHANTE** entre les deux plaques.\nReçoit l'**EAU libérée** → péridotite fond partiellement → magmatisme."},
252
  {"id": 60, "ch": "5", "section": "CH.5 — Risques",
253
  "q": "Risque volcanique caractéristique des zones de subduction ?",
254
+ "hint": "💡 Nuage mortel",
255
+ "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."},
 
256
  {"id": 61, "ch": "synth", "section": "SYNTHÈSE — Cycle lithosphérique",
257
  "q": "Résume en 5 étapes le cycle de la lithosphère océanique.",
258
+ "hint": "💡 Naissance → mort",
259
+ "a": "**1.** Naissance à la **DORSALE** (HT, BP)\n**2.** Hydratation + refroidissement → schistes verts\n**3.** Densification → **SUBDUCTION**\n**4.** Métamorphisme HP-BT (schiste bleu → éclogite) + libération d'eau\n**5.** Déclenchement du **MAGMATISME** → croûte continentale"},
260
  {"id": 62, "ch": "synth", "section": "SYNTHÈSE — Moteur",
261
  "q": "Quel processus assure la continuité de la convection mantellique globale ?",
262
+ "hint": "💡 Traction + plongement",
263
+ "a": "**TRACTION DE LA PLAQUE PLONGEANTE** (slab pull) :\nforce gravitaire exercée par la plaque froide et dense,\nmoteur principal du cycle convectif global."},
264
  {"id": 63, "ch": "synth", "section": "SYNTHÈSE — Eau",
265
  "q": "Quel est le rôle de l'eau dans le cycle de la lithosphère ?",
266
+ "hint": "💡 Partout : dorsale → subduction",
267
+ "a": "**DORSALE** : hydratation hydrothermale (schistes verts)\n**SUBDUCTION** : expulsion par métamorphisme → abaissement du solidus → magmatisme"},
268
  {"id": 64, "ch": "synth", "section": "SYNTHÈSE — Cycle",
269
  "q": "Où naît et où meurt la lithosphère océanique ?",
270
+ "hint": "💡 Deux lieux opposés",
271
+ "a": "**NAISSANCE** : à la **DORSALE** (accrétion)\n**MORT** : à la **FOSSE DE SUBDUCTION** (réintégration dans le manteau)"},
272
  {"id": 65, "ch": "synth", "section": "SYNTHÈSE — Croûte continentale",
273
  "q": "Comment la croûte continentale est-elle créée via la subduction ?",
274
+ "hint": "💡 Eau → fusion → granite",
275
+ "a": "Eau libérée → fusion coin mantellique → magma riche en silice → volcanisme + intrusions\n→ **GRANITE ET DIORITE** = croûte continentale"},
 
276
  {"id": 66, "ch": "3", "section": "CH.3 — Anomalies magnétiques",
277
  "q": "Qu'est-ce qu'une polarité magnétique normale vs inverse ?",
278
+ "hint": "💡 Pôles Nord et Sud",
279
+ "a": "**NORMALE** : orientation actuelle (pôle magnétique Nord ≈ Nord géographique)\n**INVERSE** : pôles **NORD ET SUD MAGNÉTIQUES INVERSÉS**"},
280
  {"id": 67, "ch": "4", "section": "CH.4 — Vieillissement",
281
  "q": "Pourquoi la lithosphère océanique devient-elle plus dense avec le temps ?",
282
+ "hint": "💡 Refroidissement + manteau",
283
+ "a": "En se refroidissant, elle intègre du manteau refroidi par sa base.\nDensité dépasse celle de l'**ASTHÉNOSPHÈRE** (~3,3 g/cm³)."},
284
  {"id": 68, "ch": "5", "section": "CH.5 — Métamorphisme",
285
  "q": "Pourquoi le métamorphisme de subduction est-il HP-BT ?",
286
+ "hint": "💡 Vitesse d'enfouissement",
287
+ "a": "La plaque **S'ENFONCE RAPIDEMENT** : pression augmente vite\nmais plaque froide ne se réchauffe pas assez → **HAUTE PRESSION, BASSE TEMPÉRATURE**."},
288
  {"id": 69, "ch": "5", "section": "CH.5 — Collision",
289
  "q": "Pourquoi la croûte continentale ne peut-elle pas subduire ?",
290
+ "hint": "💡 Densité vs manteau",
291
+ "a": "Croûte continentale : ~**2,7 g/cm³**\nManteau : ~**3,3 g/cm³**\nElle est **MOINS DENSE****FLOTTE** → ne peut pas plonger → **COLLISION**."},
292
  {"id": 70, "ch": "4", "section": "CH.4 — Dorsales rapides",
293
  "q": "Qu'est-ce que le géotherme et le solidus dans le contexte des dorsales ?",
294
+ "hint": "💡 Deux courbes sur un graphique P-T",
295
+ "a": "**GÉOTHERME** : température en fonction de la profondeur.\n**SOLIDUS** : limite en dessous de laquelle la roche est solide.\nSi géotherme **RECOUPE** le solidus → **LA ROCHE FOND**."},
296
  {"id": 71, "ch": "3", "section": "CH.3 — Preuves expansion",
297
  "q": "Quels indices prouvent l'expansion océanique (3 types) ?",
298
+ "hint": "💡 Magnétique + sédimentaire + géodésique",
299
+ "a": "**1.** ANOMALIES MAGNÉTIQUES SYMÉTRIQUES\n**2.** ÂGE/ÉPAISSEUR croissants des sédiments\n**3.** MESURES GPS (géodésie spatiale)"},
300
  {"id": 72, "ch": "4", "section": "CH.4 — Dorsales rapides",
301
  "q": "Composition minéralogique initiale d'un gabbro (avant métamorphisme) ?",
302
+ "hint": "💡 2 minéraux principaux",
303
+ "a": "**PYROXÈNE** (clinopyroxène) + **PLAGIOCLASE** (feldspath calcique).\nRemplacés par hornblende puis chlorite + actinote lors du métamorphisme hydrothermal."},
304
  {"id": 73, "ch": "5", "section": "CH.5 — Moteur",
305
  "q": "Quelle force initie le mouvement de la plaque vers la fosse ?",
306
+ "hint": "💡 Densité + gravité",
307
+ "a": "Plaque océanique vieillie : **PLUS DENSE QUE L'ASTHÉNOSPHÈRE**.\nGravité → **PLONGEMENT SPONTANÉ** (slab pull) → tire le reste de la plaque."},
308
  {"id": 74, "ch": "5", "section": "CH.5 — Magmatisme",
309
  "q": "Quelle roche est l'équivalent plutonique de l'andésite ?",
310
+ "hint": "💡 Roche à grain visible",
311
+ "a": "La **DIORITE** : même composition que l'andésite\nmais refroidissement lent en profondeur → texture **GRENUE**."},
312
  {"id": 75, "ch": "4", "section": "CH.4 — Vieillissement",
313
  "q": "À quelle température se place la limite faciès amphibolite / faciès schiste vert ?",
314
+ "hint": "💡 Deux seuils de température",
315
+ "a": "**AMPHIBOLITE** : > 700°C\n**SCHISTE VERT** : entre 300 et 500°C\nTransition : **500–700°C**"},
316
  {"id": 76, "ch": "synth", "section": "SYNTHÈSE — Les 3 faciès du voyage",
317
  "q": "Cite les 3 faciès jalonnant le parcours d'une plaque océanique (dorsale → arc).",
318
+ "hint": "💡 Dorsale → plaque âgée → subduction",
319
+ "a": "**1. SCHISTE VERT** (hydrothermalisme dorsale + vieillissement)\n**2. SCHISTE BLEU** (début subduction, HP-BT)\n**3. ÉCLOGITE** (subduction profonde > 50 km)"},
320
  ]
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  WRONG_POOL = [
323
  "Cristallisation fractionnée", "Plan de Wadati-Benioff", "Décompression adiabatique",
324
  "Fusion partielle", "Serpentinisation", "Dorsale médio-atlantique", "Glaucophane",
 
330
  "1 à 5 cm/an", "700 km de profondeur", "50 km de profondeur",
331
  "Fumeurs noirs", "Micaschiste", "Gneiss", "Péridotite", "Pyroxène",
332
  "Plagioclase", "Serpentine", "Nappe de charriage", "Racine crustale",
333
+ "Rhyolite", "Granite", "Basalte en filon", "Moho à 70 km",
334
  ]
335
 
336
+ FILTER_CHOICES = [
337
+ "Tout (76)", "CH.3 — Mobilité (19)", "CH.4 — Divergence (21)",
338
+ "CH.5 — Convergence (20)", "Synthèse (5+)", "⚠ À revoir",
339
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
  # ══════════════════════════════════════════════════════════
342
+ # LOGIQUE MÉTIER
343
  # ══════════════════════════════════════════════════════════
344
+ def get_filtered(ch_filter, status_dict):
345
+ mapping = {
346
+ "Tout (76)": list(CARDS),
347
+ "CH.3 Mobilité (19)": [c for c in CARDS if c["ch"] == "3"],
348
+ "CH.4 — Divergence (21)":[c for c in CARDS if c["ch"] == "4"],
349
+ "CH.5 — Convergence (20)":[c for c in CARDS if c["ch"] == "5"],
350
+ "Synthèse (5+)": [c for c in CARDS if c["ch"] == "synth"],
351
+ "⚠ À revoir": [c for c in CARDS if status_dict.get(str(c["id"])) in (None, "bad")],
352
+ }
353
+ result = mapping.get(ch_filter, list(CARDS))
354
+ return result if result else list(CARDS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
+ def ch_badge(ch):
357
+ return {"3":"🔴 CH.3","4":"🔵 CH.4","5":"🟢 CH.5","synth":"🟡 SYNTH"}.get(ch, ch)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
 
359
+ def status_emoji(s):
360
+ return {"good":"✅ Maîtrisé","ok":"🟡 Hésitant","bad":"❌ À revoir"}.get(s, "⬜ Non vu")
361
+
362
+ def compute_stats(status_dict):
363
+ good = sum(1 for v in status_dict.values() if v == "good")
364
+ ok = sum(1 for v in status_dict.values() if v == "ok")
365
+ bad = sum(1 for v in status_dict.values() if v == "bad")
366
+ unseen = 76 - good - ok - bad
367
+ pct = int(good / 76 * 100)
368
+ filled = int(pct / 5)
369
+ bar = "█" * filled + "░" * (20 - filled)
370
+ return (f"✅ **{good}** maîtrisées &nbsp;|&nbsp; 🟡 **{ok}** hésitantes &nbsp;|&nbsp; "
371
+ f"❌ **{bad}** à revoir &nbsp;|&nbsp; ⬜ **{unseen}** non vues\n\n"
372
+ f"`[{bar}]` **{pct}%** complété")
373
+
374
+ # ── Flashcard ──
375
+ def init_flashcard(ch_filter, state):
376
+ status_dict = state.get("status", {})
377
+ deck = get_filtered(ch_filter, status_dict)
378
+ random.shuffle(deck)
379
+ state["deck"] = [c["id"] for c in deck]
380
+ state["fc_idx"] = 0
381
+ state["flipped"] = False
382
+ return _render_fc(state)
383
+
384
+ def _render_fc(state):
385
+ deck_ids = state.get("deck", [])
386
+ idx = state.get("fc_idx", 0)
387
+ flipped = state.get("flipped", False)
388
+ status_dict = state.get("status", {})
389
+ if not deck_ids:
390
+ return ("—", "### Aucune carte. Change de filtre.", "", "",
391
+ "👁 Voir la réponse", gr.update(visible=False),
392
+ compute_stats(status_dict), state)
393
+ idx = idx % len(deck_ids)
394
+ card = next(c for c in CARDS if c["id"] == deck_ids[idx])
395
+ s = status_dict.get(str(card["id"]))
396
+ prog = f"**{idx+1} / {len(deck_ids)}** &nbsp;·&nbsp; {ch_badge(card['ch'])} &nbsp;·&nbsp; {card['section']} &nbsp;·&nbsp; {status_emoji(s)}"
397
+ q_md = f"## ❓ {card['q']}\n\n{card['hint']}" if not flipped else f"## ❓ {card['q']}"
398
+ a_md = f"---\n### ✅ Réponse\n\n{card['a']}" if flipped else ""
399
+ flip_label = "🔄 Masquer la réponse" if flipped else "👁 Voir la réponse"
400
+ return (prog, q_md, a_md, "",
401
+ flip_label, gr.update(visible=flipped),
402
+ compute_stats(status_dict), state)
403
+
404
+ def flip_fc(state):
405
+ state["flipped"] = not state.get("flipped", False)
406
+ return _render_fc(state)
407
+
408
+ def next_fc(state):
409
+ deck_ids = state.get("deck", [c["id"] for c in CARDS])
410
+ state["fc_idx"] = (state.get("fc_idx", 0) + 1) % len(deck_ids)
411
+ state["flipped"] = False
412
+ return _render_fc(state)
413
+
414
+ def prev_fc(state):
415
+ deck_ids = state.get("deck", [c["id"] for c in CARDS])
416
+ state["fc_idx"] = (state.get("fc_idx", 0) - 1) % len(deck_ids)
417
+ state["flipped"] = False
418
+ return _render_fc(state)
419
+
420
+ def rate_fc(rating, state):
421
+ deck_ids = state.get("deck", [c["id"] for c in CARDS])
422
+ idx = state.get("fc_idx", 0) % len(deck_ids)
423
+ state.setdefault("status", {})[str(deck_ids[idx])] = rating
424
+ state["fc_idx"] = (idx + 1) % len(deck_ids)
425
+ state["flipped"] = False
426
+ return _render_fc(state)
427
+
428
+ def shuffle_fc(state):
429
+ deck = state.get("deck", [c["id"] for c in CARDS])
430
  random.shuffle(deck)
431
+ state["deck"] = deck
432
+ state["fc_idx"] = 0
433
+ state["flipped"] = False
434
+ return _render_fc(state)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
+ FC_OUTS = ["prog_md","q_md","a_md","_","flip_btn","rating_row","stats_md","state"]
437
+
438
+ # ── Quiz ──
439
+ def _build_mcq(card):
440
+ raw = re.sub(r"\*\*", "", card["a"].split("\n")[0])
441
+ correct = re.sub(r"^\d+\.\s*", "", raw).strip()[:80]
 
 
 
 
 
 
442
  pool = [w for w in WRONG_POOL if w.lower() not in correct.lower()]
443
  random.shuffle(pool)
444
+ opts = pool[:3] + [correct]
445
+ random.shuffle(opts)
446
+ return correct, opts
 
 
 
 
 
 
 
 
 
447
 
448
+ def start_quiz(ch_filter, state):
449
+ status_dict = state.get("status", {})
450
+ deck = get_filtered(ch_filter, status_dict)
451
+ random.shuffle(deck)
452
+ state["qz_deck"] = [c["id"] for c in deck]
453
+ state["qz_idx"] = 0
454
+ state["qz_score"] = 0
455
+ state["qz_answered"] = False
456
+ return _render_qz(state)
457
+
458
+ def _render_qz(state):
459
+ deck_ids = state.get("qz_deck", [c["id"] for c in CARDS])
460
+ idx = state.get("qz_idx", 0)
461
+ score = state.get("qz_score", 0)
462
+ if idx >= len(deck_ids):
463
+ total = len(deck_ids)
464
+ pct = int(score / total * 100) if total else 0
465
+ ico = "🏆" if pct>=80 else "👍" if pct>=60 else "⚠️" if pct>=40 else "📚"
466
+ msg = ("Excellent !" if pct>=80 else "Bien !" if pct>=60 else
467
+ "Des lacunes — flashcards !" if pct>=40 else "Reprends tout !")
468
+ q_md = f"# {ico} Score final : {pct}%\n\n**{score}/{total}** bonnes réponses\n\n*{msg}*"
469
+ return (q_md, gr.update(choices=[], visible=False),
470
+ gr.update(value="", visible=False),
471
+ gr.update(visible=False, value="✅ Valider"),
472
+ gr.update(visible=True), state)
473
+ card = next(c for c in CARDS if c["id"] == deck_ids[idx])
474
+ correct, opts = _build_mcq(card)
475
+ state["qz_correct"] = correct
476
+ state["qz_answered"] = False
477
+ state["qz_opts"] = opts
478
+ q_md = (f"**{idx+1}/{len(deck_ids)}** &nbsp;·&nbsp; {ch_badge(card['ch'])} &nbsp;·&nbsp; "
479
+ f"Score : **{score}/{idx}**\n\n---\n\n## ❓ {card['q']}")
480
+ return (q_md,
481
+ gr.update(choices=opts, value=None, visible=True, interactive=True),
482
+ gr.update(value="", visible=False),
483
+ gr.update(visible=True, value=" Valider"),
484
+ gr.update(visible=False), state)
485
+
486
+ def answer_qz(chosen, btn_label, state):
487
+ if btn_label != " Valider":
488
+ state["qz_idx"] = state.get("qz_idx", 0) + 1
489
+ state["qz_answered"] = False
490
+ return _render_qz(state)
491
+ if not chosen or state.get("qz_answered"):
492
+ return _render_qz(state)
493
+ correct = state.get("qz_correct", "")
494
+ is_ok = (chosen == correct)
495
+ deck_ids = state.get("qz_deck", [])
496
+ idx = state.get("qz_idx", 0)
497
+ card_id = deck_ids[idx] if idx < len(deck_ids) else None
498
+ card = next((c for c in CARDS if c["id"] == card_id), None)
499
+ state["qz_answered"] = True
500
+ if is_ok:
501
+ state["qz_score"] = state.get("qz_score", 0) + 1
502
+ if card:
503
+ state.setdefault("status", {})[str(card_id)] = "good"
504
+ fb = f"### Bonne réponse !\n\n{card['a'] if card else ''}"
505
+ else:
506
+ if card:
507
+ state.setdefault("status", {})[str(card_id)] = "bad"
508
+ fb = f"### Incorrect\n\n**Bonne réponse :** {correct}\n\n---\n\n{card['a'] if card else ''}"
509
+ deck_ids2 = state.get("qz_deck", [])
510
+ idx2 = state.get("qz_idx", 0)
511
+ score2 = state.get("qz_score", 0)
512
+ card2 = next(c for c in CARDS if c["id"] == deck_ids2[idx2])
513
+ _, opts2 = _build_mcq(card2)
514
+ q_md2 = (f"**{idx2+1}/{len(deck_ids2)}** &nbsp;·&nbsp; {ch_badge(card2['ch'])} &nbsp;·&nbsp; "
515
+ f"Score : **{score2}/{idx2}**\n\n---\n\n## ❓ {card2['q']}")
516
+ return (q_md2,
517
+ gr.update(choices=opts2, value=chosen, interactive=False),
518
+ gr.update(value=fb, visible=True),
519
+ gr.update(visible=True, value="Question suivante →"),
520
+ gr.update(visible=False), state)
521
+
522
+ # ── Récap ──
523
+ def build_recap(ch_filter):
524
+ groups = {
525
+ "Tout (76)": [("3","🔴 CH.3"), ("4","🔵 CH.4"), ("5","🟢 CH.5"), ("synth","🟡 SYNTHÈSE")],
526
+ "CH.3 Mobilité (19)": [("3","🔴 CH.3")],
527
+ "CH.4 Divergence (21)":[("4","🔵 CH.4")],
528
+ "CH.5 Convergence (20)":[("5","🟢 CH.5")],
529
+ "Synthèse (5+)": [("synth","🟡 SYNTHÈSE")],
530
+ }
531
+ chapters = groups.get(ch_filter, groups["Tout (76)"])
532
+ md = ""
533
+ for ch, label in chapters:
534
+ md += f"\n---\n# {label}\n\n"
535
+ for c in [x for x in CARDS if x["ch"] == ch]:
536
+ md += f"**#{c['id']:02d} · {c['section']}**\n\n❓ *{c['q']}*\n\n✅ {c['a']}\n\n"
537
+ return md
538
+
539
+ # ── Carte de progression ──
540
+ def build_map(state):
541
+ sd = state.get("status", {})
542
+ md = "## 🗺 Carte de progression\n\n"
543
+ for ch, label, ico in [("3","CH.3","🔴"),("4","CH.4","🔵"),("5","CH.5","🟢"),("synth","SYNTH","🟡")]:
544
+ cards = [c for c in CARDS if c["ch"] == ch]
545
+ g = sum(1 for c in cards if sd.get(str(c["id"]))=="good")
546
+ o = sum(1 for c in cards if sd.get(str(c["id"]))=="ok")
547
+ b = sum(1 for c in cards if sd.get(str(c["id"]))=="bad")
548
+ bar = "█"*int(g/len(cards)*10) + "░"*(10-int(g/len(cards)*10))
549
+ md += f"{ico} **{label}** `[{bar}]` ✅{g} 🟡{o} ❌{b} / {len(cards)}\n\n"
550
+ md += "\n---\n### Détail par carte\n\n"
551
+ row = []
552
+ for c in CARDS:
553
+ s = sd.get(str(c["id"]))
554
+ ico2 = {"good":"✅","ok":"🟡","bad":"❌"}.get(s,"⬜")
555
+ row.append(f"{ico2}`{c['id']:02d}`")
556
+ if len(row) == 10:
557
+ md += " ".join(row) + "\n\n"; row = []
558
+ if row: md += " ".join(row) + "\n\n"
559
+ md += "\n**Légende :** ✅ Maîtrisé &nbsp; 🟡 Hésitant &nbsp; ❌ À revoir &nbsp; ⬜ Non vu"
560
+ return md
561
+
562
+ def reset_progress(state):
563
+ state["status"] = {}
564
+ return compute_stats({}), build_map(state), state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
 
566
  # ════════════════════════════════════════════════���═════════
567
+ # CSS
568
  # ══════════════════════════════════════════════════════════
569
+ CSS = """
570
+ @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@700;800&display=swap');
571
+ body, .gradio-container { font-family: 'Space Mono', monospace !important; }
572
+ .app-header { background: linear-gradient(135deg,#111420,#181d2e); border:1px solid #252a3d;
573
+ border-radius:14px; padding:28px 36px; margin-bottom:12px; }
574
+ .app-header h1 { font-family:'Syne',sans-serif !important; font-size:2.4rem !important;
575
+ font-weight:800 !important; letter-spacing:-2px; margin:0 !important; }
576
+ .app-header h1 span { color:#e8440a; }
577
+ .app-header p { color:#6b7090; font-size:.73rem; letter-spacing:2px; margin:6px 0 0; }
578
+ """
 
 
 
 
 
579
 
580
  # ══════════════════════════════════════════════════════════
581
+ # UI GRADIO
582
  # ══════════════════════════════════════════════════════════
583
+ with gr.Blocks(css=CSS, title="GéoRévision — Tectonique des Plaques",
584
+ theme=gr.themes.Base(
585
+ primary_hue="orange", neutral_hue="slate",
586
+ font=[gr.themes.GoogleFont("Space Mono"), "monospace"]
587
+ )) as demo:
588
+
589
+ state = gr.State({})
590
+
591
+ gr.HTML("""
592
+ <div class="app-header">
593
+ <h1>GÉO<span>RÉVISION</span></h1>
594
+ <p>TECTONIQUE DES PLAQUES &nbsp;·&nbsp; 76 NOTIONS &nbsp;·&nbsp; CH.3 · CH.4 · CH.5 · SYNTHÈSE &nbsp;·&nbsp; MÉTHODE EBBINGHAUS + TESTING EFFECT</p>
595
+ </div>
596
+ """)
597
+
598
+ stats_md = gr.Markdown(compute_stats({}))
599
+
600
+ with gr.Tabs():
601
+
602
+ # ═══ FLASHCARDS ═══
603
+ with gr.Tab("🃏 Flashcards"):
604
+ with gr.Row():
605
+ fc_filter = gr.Dropdown(FILTER_CHOICES, value="Tout (76)", label="Filtre", scale=3)
606
+ fc_start = gr.Button("▶ Démarrer", variant="primary", scale=1)
607
+ fc_shuf = gr.Button("⟳ Mélanger", variant="secondary", scale=1)
608
+
609
+ fc_prog = gr.Markdown("*Sélectionne un filtre et clique sur ▶ Démarrer*")
610
+
611
+ with gr.Group():
612
+ fc_q = gr.Markdown("### ← Lance une session pour commencer !")
613
+
614
+ fc_flip = gr.Button("👁 Voir la réponse", variant="primary")
615
+
616
+ with gr.Group():
617
+ fc_a = gr.Markdown("")
618
+ fc_s = gr.Markdown("")
619
+
620
+ with gr.Row(visible=False) as fc_rating:
621
+ fc_bad = gr.Button("❌ À revoir", variant="secondary", scale=1)
622
+ fc_ok = gr.Button("🟡 Hésitant", variant="secondary", scale=1)
623
+ fc_good = gr.Button("✅ Maîtrisé", variant="secondary", scale=1)
624
+
625
+ with gr.Row():
626
+ fc_prev = gr.Button("← Précédent", variant="secondary", scale=1)
627
+ fc_next = gr.Button("Suivant →", variant="secondary", scale=1)
628
+
629
+ gr.Markdown("*💡 Méthode : lis la question, formule mentalement, puis révèle. Note honnêtement. Reviens sur le filtre **⚠ À revoir** régulièrement.*")
630
+
631
+ FC = [fc_prog, fc_q, fc_a, fc_s, fc_flip, fc_rating, stats_md, state]
632
+ fc_start.click(init_flashcard, [fc_filter, state], FC)
633
+ fc_shuf.click(shuffle_fc, [state], FC)
634
+ fc_flip.click(flip_fc, [state], FC)
635
+ fc_prev.click(prev_fc, [state], FC)
636
+ fc_next.click(next_fc, [state], FC)
637
+ fc_bad.click( lambda s: rate_fc("bad", s), [state], FC)
638
+ fc_ok.click( lambda s: rate_fc("ok", s), [state], FC)
639
+ fc_good.click(lambda s: rate_fc("good", s), [state], FC)
640
+
641
+ # ═══ QUIZ ═══
642
+ with gr.Tab("🎯 Quiz QCM"):
643
+ with gr.Row():
644
+ qz_filt = gr.Dropdown(FILTER_CHOICES, value="Tout (76)", label="Filtre", scale=3)
645
+ qz_start = gr.Button("▶ Lancer", variant="primary", scale=1)
646
+
647
+ qz_q = gr.Markdown("### ← Lance le quiz pour commencer !")
648
+ qz_opts = gr.Radio(choices=[], label="Ta réponse", visible=False)
649
+ qz_fb = gr.Markdown("", visible=False)
650
+
651
+ with gr.Row():
652
+ qz_val = gr.Button("✅ Valider", variant="primary", visible=False, scale=2)
653
+ qz_restart = gr.Button("🔁 Recommencer", variant="secondary", visible=False, scale=1)
654
+
655
+ QZ = [qz_q, qz_opts, qz_fb, qz_val, qz_restart, state]
656
+ qz_start.click(start_quiz, [qz_filt, state], QZ)
657
+ qz_val.click(answer_qz, [qz_opts, qz_val, state], QZ)
658
+ qz_restart.click(start_quiz, [qz_filt, state], QZ)
659
+
660
+ # ═══ RÉCAP ═══
661
+ with gr.Tab("📚 Récapitulatif"):
662
+ recap_filt = gr.Dropdown(
663
+ [c for c in FILTER_CHOICES if c != "⚠ À revoir"],
664
+ value="Tout (76)", label="Chapitre"
665
+ )
666
+ gr.Button("📖 Afficher", variant="primary").click(
667
+ build_recap, [recap_filt], gr.Markdown("*Clique sur Afficher.*")
668
+ )
669
+ recap_out = gr.Markdown("*Clique sur Afficher pour voir toutes les notions.*")
670
+ gr.Button("📖 Afficher", variant="primary", visible=False)
671
+ # Rebind correctement
672
+ recap_filt.change(build_recap, [recap_filt], recap_out)
673
+
674
+ # ═══ PROGRESSION ═══
675
+ with gr.Tab("🗺 Progression"):
676
+ with gr.Row():
677
+ map_btn = gr.Button("🔄 Actualiser", variant="primary", scale=2)
678
+ reset_btn = gr.Button("🗑 Réinitialiser", variant="stop", scale=1)
679
+ map_out = gr.Markdown(build_map({}))
680
+ map_btn.click(lambda s: build_map(s), [state], map_out)
681
+ reset_btn.click(reset_progress, [state], [stats_md, map_out, state])
682
+
683
+ # ═══ AIDE ═══
684
+ with gr.Tab("ℹ️ Aide"):
685
+ gr.Markdown("""
686
+ ## 🧠 Méthode scientifique
687
+
688
+ | Principe | Source | Application |
689
+ |----------|--------|-------------|
690
+ | **Testing Effect** | Roediger & Karpicke, 2006 | Se tester > relire → chaque flashcard force une récupération active |
691
+ | **Répétition espacée** | Ebbinghaus, 1885 | Filtre ⚠ À revoir → cible les lacunes au bon moment |
692
+ | **Interleaving** | Kornell & Bjork, 2008 | Mode Tout (76) mélangé → améliore la discrimination |
693
+
694
+ ---
695
+
696
+ ## 🃏 Flashcards
697
+ 1. Sélectionne un filtre → **▶ Démarrer**
698
+ 2. Lis la question, formule **mentalement** ta réponse
699
+ 3. Clique **👁 Voir la réponse** pour comparer
700
+ 4. Note honnêtement : ❌ / 🟡 / ✅
701
+ 5. Reviens régulièrement sur **⚠ À revoir**
702
+
703
+ ## 🎯 Quiz
704
+ - 4 propositions par question
705
+ - Feedback immédiat avec l'explication complète
706
+ - Met à jour automatiquement la progression
707
+
708
+ ## 📅 Plan de révision recommandé
709
+
710
+ | Quand | Action |
711
+ |-------|--------|
712
+ | J-7 | Flashcards CH.3 complet |
713
+ | J-5 | Flashcards CH.4 complet |
714
+ | J-3 | Flashcards CH.5 + Synthèse |
715
+ | J-2 | Quiz tout + ⚠ À revoir |
716
+ | J-1 | Récap complet + Quiz final |
717
+ | Jour J | Cartes ❌ uniquement |
718
+ """)
719
+
720
+ demo.load(lambda s: (compute_stats(s.get("status", {})), build_map(s)),
721
+ [state], [stats_md, map_out])
722
 
723
  if __name__ == "__main__":
724
+ demo.launch()