diff --git a/.gitignore b/.gitignore
index ee779db..6e8c876 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,4 @@ local.properties
DerivedData/
*.xcuserstate
xcuserdata/
-
+.DS_Store
diff --git a/AGENTS.md b/AGENTS.md
index 2b981cb..76e825a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -13,6 +13,7 @@
## Coding Guidelines
- Keep code simple and easy to read.
+ - Never use em dashes in app user-facing copy.
- Avoid excessive comments. We want code itself to be super readable. Only comment when absolutely necessary.
- Do not create or modify implementation code unless the user explicitly asks for code changes.
- Do not create or modify tests unless the user explicitly asks for tests.
diff --git a/README.md b/README.md
index 0afc8c7..7fb806c 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Bert's Atlas is an offline geography trivia game for Android and iOS. Product be
## Architecture
-- `domain`: pure Kotlin Multiplatform quiz, timing, scoring, mastery, streak, and selection rules
+- `domain`: pure Kotlin Multiplatform trivia, timing, scoring, mastery, streak, and selection rules
- `composeApp`: shared Compose UI, bundled resources, Room persistence, and platform adapters
- `catalogTool`: non-test JVM catalog validator and editorial report generator
- `androidApp`: thin Android application host
diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
index ee39898..65973db 100644
--- a/androidApp/src/main/AndroidManifest.xml
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -3,7 +3,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/androidApp/src/main/res/drawable/splash_background.xml b/androidApp/src/main/res/drawable/splash_background.xml
new file mode 100644
index 0000000..10fa406
--- /dev/null
+++ b/androidApp/src/main/res/drawable/splash_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..67a340f
--- /dev/null
+++ b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..67a340f
--- /dev/null
+++ b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..37d7d18
Binary files /dev/null and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..37d7d18
Binary files /dev/null and b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..2b0ac91
Binary files /dev/null and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..2b0ac91
Binary files /dev/null and b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..2718df9
Binary files /dev/null and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..2718df9
Binary files /dev/null and b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..084f148
Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..084f148
Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..5c77a07
Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..5c77a07
Binary files /dev/null and b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/androidApp/src/main/res/values-v31/styles.xml b/androidApp/src/main/res/values-v31/styles.xml
new file mode 100644
index 0000000..a85c6de
--- /dev/null
+++ b/androidApp/src/main/res/values-v31/styles.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/androidApp/src/main/res/values/colors.xml b/androidApp/src/main/res/values/colors.xml
new file mode 100644
index 0000000..0ec3d51
--- /dev/null
+++ b/androidApp/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #F6E9E1
+
diff --git a/androidApp/src/main/res/values/styles.xml b/androidApp/src/main/res/values/styles.xml
index 98d9277..bb6561a 100644
--- a/androidApp/src/main/res/values/styles.xml
+++ b/androidApp/src/main/res/values/styles.xml
@@ -3,8 +3,8 @@
-
diff --git a/catalogTool/src/main/kotlin/app/bertsatlas/catalogtool/Main.kt b/catalogTool/src/main/kotlin/app/bertsatlas/catalogtool/Main.kt
index cd1b496..0e618dc 100644
--- a/catalogTool/src/main/kotlin/app/bertsatlas/catalogtool/Main.kt
+++ b/catalogTool/src/main/kotlin/app/bertsatlas/catalogtool/Main.kt
@@ -123,6 +123,11 @@ private class Validator(private val path: Path) {
val label = "fact ${id(fact)}"
expectInt(fact, "schemaVersion", 1, label)
listOf("heading", "text").forEach { text(fact, it, label) }
+ val heading = value(fact, "heading")
+ val copy = value(fact, "text")
+ if (heading == "Atlas placement" || copy.contains("UN M49", ignoreCase = true)) {
+ errors += "$label exposes catalog metadata as lesson copy"
+ }
refs(label, strings(fact, "countryIds"), countryIds, "country")
val sourcesForFact = strings(fact, "sourceIds")
if (sourcesForFact.isEmpty()) errors += "$label has no factual source"
@@ -207,6 +212,9 @@ private class Validator(private val path: Path) {
}
val hints = strings(question, "supportedHints")
(hints - validHints).forEach { errors += "$label has unsupported hint $it" }
+ if ("factual_clue" in hints && value(question, "explanation").contains(answer, ignoreCase = true)) {
+ errors += "$label factual clue contains its answer"
+ }
if (format != "letter_bank" && "reveal_letter" in hints) errors += "$label has an incompatible letter hint"
if (format != "multiple_choice" && "remove_choices" in hints) errors += "$label has incompatible choice removal"
refs(label, strings(question, "countryIds"), countryIds, "country")
diff --git a/composeApp/src/commonMain/composeResources/drawable/bert_atlas_mark.svg b/composeApp/src/commonMain/composeResources/drawable/bert_atlas_mark.svg
new file mode 100644
index 0000000..ecd2101
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/bert_atlas_mark.svg
@@ -0,0 +1,17 @@
+
diff --git a/composeApp/src/commonMain/composeResources/drawable/bert_correct.svg b/composeApp/src/commonMain/composeResources/drawable/bert_correct.svg
index 8dd28d2..ae08126 100644
--- a/composeApp/src/commonMain/composeResources/drawable/bert_correct.svg
+++ b/composeApp/src/commonMain/composeResources/drawable/bert_correct.svg
@@ -1,7 +1,7 @@
diff --git a/composeApp/src/commonMain/composeResources/drawable/bert_fact.svg b/composeApp/src/commonMain/composeResources/drawable/bert_fact.svg
index caea540..8b9fa44 100644
--- a/composeApp/src/commonMain/composeResources/drawable/bert_fact.svg
+++ b/composeApp/src/commonMain/composeResources/drawable/bert_fact.svg
@@ -1,7 +1,7 @@
diff --git a/composeApp/src/commonMain/composeResources/drawable/bert_idle.svg b/composeApp/src/commonMain/composeResources/drawable/bert_idle.svg
index 1c919c4..14cf2a7 100644
--- a/composeApp/src/commonMain/composeResources/drawable/bert_idle.svg
+++ b/composeApp/src/commonMain/composeResources/drawable/bert_idle.svg
@@ -1,11 +1,11 @@
diff --git a/composeApp/src/commonMain/composeResources/drawable/bert_incorrect.svg b/composeApp/src/commonMain/composeResources/drawable/bert_incorrect.svg
index 8db5061..98b34c6 100644
--- a/composeApp/src/commonMain/composeResources/drawable/bert_incorrect.svg
+++ b/composeApp/src/commonMain/composeResources/drawable/bert_incorrect.svg
@@ -1,7 +1,7 @@
diff --git a/composeApp/src/commonMain/composeResources/files/bert_correct.json b/composeApp/src/commonMain/composeResources/files/bert_correct.json
index 9c9dfa3..5bcf5b2 100644
--- a/composeApp/src/commonMain/composeResources/files/bert_correct.json
+++ b/composeApp/src/commonMain/composeResources/files/bert_correct.json
@@ -1 +1,152 @@
-{"v":"5.7.4","fr":30,"ip":0,"op":45,"w":512,"h":512,"nm":"bert correct accent","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"success star","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[-12]},{"t":22,"s":[12]},{"t":45,"s":[-12]}]},"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[70,70,100]},{"t":22,"s":[110,110,100]},{"t":45,"s":[70,70,100]}]}},"shapes":[{"ty":"sr","sy":1,"pt":{"a":0,"k":5},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"or":{"a":0,"k":100},"os":{"a":0,"k":0},"ir":{"a":0,"k":45},"is":{"a":0,"k":0}},{"ty":"fl","c":{"a":0,"k":[0.08,0.46,0.41,1]},"o":{"a":0,"k":100},"r":1}],"ip":0,"op":45,"st":0,"bm":0}],"markers":[]}
+{
+ "v": "5.7.4",
+ "fr": 30,
+ "ip": 0,
+ "op": 45,
+ "w": 512,
+ "h": 512,
+ "nm": "bert correct accent",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "success star",
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ -12
+ ]
+ },
+ {
+ "t": 22,
+ "s": [
+ 12
+ ]
+ },
+ {
+ "t": 45,
+ "s": [
+ -12
+ ]
+ }
+ ]
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 420,
+ 95,
+ 0
+ ]
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 70,
+ 70,
+ 100
+ ]
+ },
+ {
+ "t": 22,
+ "s": [
+ 110,
+ 110,
+ 100
+ ]
+ },
+ {
+ "t": 45,
+ "s": [
+ 70,
+ 70,
+ 100
+ ]
+ }
+ ]
+ }
+ },
+ "shapes": [
+ {
+ "ty": "sr",
+ "sy": 1,
+ "pt": {
+ "a": 0,
+ "k": 5
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ]
+ },
+ "r": {
+ "a": 0,
+ "k": 0
+ },
+ "or": {
+ "a": 0,
+ "k": 42
+ },
+ "os": {
+ "a": 0,
+ "k": 0
+ },
+ "ir": {
+ "a": 0,
+ "k": 18
+ },
+ "is": {
+ "a": 0,
+ "k": 0
+ }
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.152941,
+ 0.388235,
+ 0.356863,
+ 1
+ ]
+ },
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": 1
+ }
+ ],
+ "ip": 0,
+ "op": 45,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
diff --git a/composeApp/src/commonMain/composeResources/files/bert_fact.json b/composeApp/src/commonMain/composeResources/files/bert_fact.json
index dd414bc..081f7e4 100644
--- a/composeApp/src/commonMain/composeResources/files/bert_fact.json
+++ b/composeApp/src/commonMain/composeResources/files/bert_fact.json
@@ -1 +1,115 @@
-{"v":"5.7.4","fr":30,"ip":0,"op":60,"w":512,"h":512,"nm":"bert fact accent","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"fact bubble","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"t":0,"s":[256,270,0]},{"t":30,"s":[256,242,0]},{"t":60,"s":[256,270,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"shapes":[{"ty":"el","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[190,150]}},{"ty":"fl","c":{"a":0,"k":[0.09,0.39,0.49,1]},"o":{"a":0,"k":100},"r":1}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]}
+{
+ "v": "5.7.4",
+ "fr": 30,
+ "ip": 0,
+ "op": 60,
+ "w": 512,
+ "h": 512,
+ "nm": "bert fact accent",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "fact bubble",
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": {
+ "a": 0,
+ "k": 0
+ },
+ "p": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 420,
+ 100,
+ 0
+ ]
+ },
+ {
+ "t": 30,
+ "s": [
+ 420,
+ 82,
+ 0
+ ]
+ },
+ {
+ "t": 60,
+ "s": [
+ 420,
+ 100,
+ 0
+ ]
+ }
+ ]
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ]
+ }
+ },
+ "shapes": [
+ {
+ "ty": "el",
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 72,
+ 54
+ ]
+ }
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.466667,
+ 0.545098,
+ 0.647059,
+ 1
+ ]
+ },
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": 1
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
diff --git a/composeApp/src/commonMain/composeResources/files/bert_hint.json b/composeApp/src/commonMain/composeResources/files/bert_hint.json
index ed18d02..b839f28 100644
--- a/composeApp/src/commonMain/composeResources/files/bert_hint.json
+++ b/composeApp/src/commonMain/composeResources/files/bert_hint.json
@@ -1 +1,113 @@
-{"v":"5.7.4","fr":30,"ip":0,"op":60,"w":512,"h":512,"nm":"bert hint accent","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"hint glow","ks":{"o":{"a":1,"k":[{"t":0,"s":[45]},{"t":30,"s":[100]},{"t":60,"s":[45]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"shapes":[{"ty":"el","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[180,180]}},{"ty":"fl","c":{"a":0,"k":[0.89,0.68,0.21,1]},"o":{"a":0,"k":100},"r":1}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]}
+{
+ "v": "5.7.4",
+ "fr": 30,
+ "ip": 0,
+ "op": 60,
+ "w": 512,
+ "h": 512,
+ "nm": "bert hint accent",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "hint glow",
+ "ks": {
+ "o": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 45
+ ]
+ },
+ {
+ "t": 30,
+ "s": [
+ 100
+ ]
+ },
+ {
+ "t": 60,
+ "s": [
+ 45
+ ]
+ }
+ ]
+ },
+ "r": {
+ "a": 0,
+ "k": 0
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 420,
+ 95,
+ 0
+ ]
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ]
+ }
+ },
+ "shapes": [
+ {
+ "ty": "el",
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 64,
+ 64
+ ]
+ }
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.913725,
+ 0.686275,
+ 0.545098,
+ 1
+ ]
+ },
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": 1
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
diff --git a/composeApp/src/commonMain/composeResources/files/bert_idle.json b/composeApp/src/commonMain/composeResources/files/bert_idle.json
index ae07e11..d559c8d 100644
--- a/composeApp/src/commonMain/composeResources/files/bert_idle.json
+++ b/composeApp/src/commonMain/composeResources/files/bert_idle.json
@@ -1 +1,157 @@
-{"v":"5.7.4","fr":30,"ip":0,"op":90,"w":512,"h":512,"nm":"bert idle","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"spark","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[-8]},{"t":45,"s":[8]},{"t":90,"s":[-8]}]},"p":{"a":0,"k":[410,105,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[85,85,100]},{"t":45,"s":[110,110,100]},{"t":90,"s":[85,85,100]}]}},"ao":0,"shapes":[{"ty":"el","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[48,48]},"nm":"spark circle"},{"ty":"fl","c":{"a":0,"k":[0.09,0.45,0.41,1]},"o":{"a":0,"k":100},"r":1,"nm":"teal fill"}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"bert","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"t":0,"s":[256,266,0]},{"t":45,"s":[256,258,0]},{"t":90,"s":[256,266,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"el","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[310,270]},"nm":"brain silhouette"},{"ty":"fl","c":{"a":0,"k":[0.91,0.43,0.36,1]},"o":{"a":0,"k":100},"r":1,"nm":"coral fill"},{"ty":"st","c":{"a":0,"k":[0.24,0.14,0.28,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":12},"lc":2,"lj":2,"nm":"plum stroke"}],"ip":0,"op":90,"st":0,"bm":0}],"markers":[]}
+{
+ "v": "5.7.4",
+ "fr": 30,
+ "ip": 0,
+ "op": 90,
+ "w": 512,
+ "h": 512,
+ "nm": "bert idle accent",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "curiosity spark",
+ "sr": 1,
+ "ks": {
+ "o": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 45
+ ]
+ },
+ {
+ "t": 45,
+ "s": [
+ 100
+ ]
+ },
+ {
+ "t": 90,
+ "s": [
+ 45
+ ]
+ }
+ ]
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ -8
+ ]
+ },
+ {
+ "t": 45,
+ "s": [
+ 8
+ ]
+ },
+ {
+ "t": 90,
+ "s": [
+ -8
+ ]
+ }
+ ]
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 412,
+ 102,
+ 0
+ ]
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 75,
+ 75,
+ 100
+ ]
+ },
+ {
+ "t": 45,
+ "s": [
+ 105,
+ 105,
+ 100
+ ]
+ },
+ {
+ "t": 90,
+ "s": [
+ 75,
+ 75,
+ 100
+ ]
+ }
+ ]
+ }
+ },
+ "ao": 0,
+ "shapes": [
+ {
+ "ty": "el",
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 34,
+ 34
+ ]
+ },
+ "nm": "spark dot"
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.152941,
+ 0.388235,
+ 0.356863,
+ 1
+ ]
+ },
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": 1,
+ "nm": "teal fill"
+ }
+ ],
+ "ip": 0,
+ "op": 90,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
diff --git a/composeApp/src/commonMain/composeResources/files/bert_incorrect.json b/composeApp/src/commonMain/composeResources/files/bert_incorrect.json
index 5d9b077..96bf750 100644
--- a/composeApp/src/commonMain/composeResources/files/bert_incorrect.json
+++ b/composeApp/src/commonMain/composeResources/files/bert_incorrect.json
@@ -1 +1,124 @@
-{"v":"5.7.4","fr":30,"ip":0,"op":45,"w":512,"h":512,"nm":"bert incorrect accent","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"kind wobble","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[-4]},{"t":15,"s":[4]},{"t":30,"s":[-4]},{"t":45,"s":[0]}]},"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"shapes":[{"ty":"el","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[170,170]}},{"ty":"st","c":{"a":0,"k":[0.61,0.24,0.2,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":18},"lc":2,"lj":2}],"ip":0,"op":45,"st":0,"bm":0}],"markers":[]}
+{
+ "v": "5.7.4",
+ "fr": 30,
+ "ip": 0,
+ "op": 45,
+ "w": 512,
+ "h": 512,
+ "nm": "bert incorrect accent",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "kind wobble",
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ -4
+ ]
+ },
+ {
+ "t": 15,
+ "s": [
+ 4
+ ]
+ },
+ {
+ "t": 30,
+ "s": [
+ -4
+ ]
+ },
+ {
+ "t": 45,
+ "s": [
+ 0
+ ]
+ }
+ ]
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 420,
+ 95,
+ 0
+ ]
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ]
+ }
+ },
+ "shapes": [
+ {
+ "ty": "el",
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 42,
+ 42
+ ]
+ }
+ },
+ {
+ "ty": "st",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.882353,
+ 0.505882,
+ 0.305882,
+ 1
+ ]
+ },
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "w": {
+ "a": 0,
+ "k": 18
+ },
+ "lc": 2,
+ "lj": 2
+ }
+ ],
+ "ip": 0,
+ "op": 45,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
diff --git a/composeApp/src/commonMain/composeResources/files/bert_overtime.json b/composeApp/src/commonMain/composeResources/files/bert_overtime.json
index 2b26a83..992c095 100644
--- a/composeApp/src/commonMain/composeResources/files/bert_overtime.json
+++ b/composeApp/src/commonMain/composeResources/files/bert_overtime.json
@@ -1 +1,112 @@
-{"v":"5.7.4","fr":30,"ip":0,"op":60,"w":512,"h":512,"nm":"bert overtime accent","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"clock","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0]},{"t":60,"s":[360]}]},"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"shapes":[{"ty":"el","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[190,190]}},{"ty":"st","c":{"a":0,"k":[0.89,0.68,0.21,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":20},"lc":2,"lj":2}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]}
+{
+ "v": "5.7.4",
+ "fr": 30,
+ "ip": 0,
+ "op": 60,
+ "w": 512,
+ "h": 512,
+ "nm": "bert overtime accent",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "clock",
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 0
+ ]
+ },
+ {
+ "t": 60,
+ "s": [
+ 360
+ ]
+ }
+ ]
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 420,
+ 95,
+ 0
+ ]
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ]
+ }
+ },
+ "shapes": [
+ {
+ "ty": "el",
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 70,
+ 70
+ ]
+ }
+ },
+ {
+ "ty": "st",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.913725,
+ 0.686275,
+ 0.545098,
+ 1
+ ]
+ },
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "w": {
+ "a": 0,
+ "k": 20
+ },
+ "lc": 2,
+ "lj": 2
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
diff --git a/composeApp/src/commonMain/composeResources/files/bert_results.json b/composeApp/src/commonMain/composeResources/files/bert_results.json
index e4ceab1..000a771 100644
--- a/composeApp/src/commonMain/composeResources/files/bert_results.json
+++ b/composeApp/src/commonMain/composeResources/files/bert_results.json
@@ -1 +1,131 @@
-{"v":"5.7.4","fr":30,"ip":0,"op":45,"w":512,"h":512,"nm":"bert results accent","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"result seal","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[-8]},{"t":22,"s":[8]},{"t":45,"s":[-8]}]},"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"shapes":[{"ty":"sr","sy":1,"pt":{"a":0,"k":12},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"or":{"a":0,"k":110},"os":{"a":0,"k":0},"ir":{"a":0,"k":90},"is":{"a":0,"k":0}},{"ty":"fl","c":{"a":0,"k":[0.09,0.39,0.49,1]},"o":{"a":0,"k":100},"r":1}],"ip":0,"op":45,"st":0,"bm":0}],"markers":[]}
+{
+ "v": "5.7.4",
+ "fr": 30,
+ "ip": 0,
+ "op": 45,
+ "w": 512,
+ "h": 512,
+ "nm": "bert results accent",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "result seal",
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ -8
+ ]
+ },
+ {
+ "t": 22,
+ "s": [
+ 8
+ ]
+ },
+ {
+ "t": 45,
+ "s": [
+ -8
+ ]
+ }
+ ]
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 420,
+ 95,
+ 0
+ ]
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ]
+ }
+ },
+ "shapes": [
+ {
+ "ty": "sr",
+ "sy": 1,
+ "pt": {
+ "a": 0,
+ "k": 12
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ]
+ },
+ "r": {
+ "a": 0,
+ "k": 0
+ },
+ "or": {
+ "a": 0,
+ "k": 42
+ },
+ "os": {
+ "a": 0,
+ "k": 0
+ },
+ "ir": {
+ "a": 0,
+ "k": 32
+ },
+ "is": {
+ "a": 0,
+ "k": 0
+ }
+ },
+ {
+ "ty": "fl",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.466667,
+ 0.545098,
+ 0.647059,
+ 1
+ ]
+ },
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": 1
+ }
+ ],
+ "ip": 0,
+ "op": 45,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
diff --git a/composeApp/src/commonMain/composeResources/files/bert_stamp.json b/composeApp/src/commonMain/composeResources/files/bert_stamp.json
index 9c501a9..a824f7b 100644
--- a/composeApp/src/commonMain/composeResources/files/bert_stamp.json
+++ b/composeApp/src/commonMain/composeResources/files/bert_stamp.json
@@ -1 +1,158 @@
-{"v":"5.7.4","fr":30,"ip":0,"op":30,"w":512,"h":512,"nm":"bert passport stamp accent","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"stamp","ks":{"o":{"a":1,"k":[{"t":0,"s":[0]},{"t":10,"s":[100]},{"t":30,"s":[100]}]},"r":{"a":1,"k":[{"t":0,"s":[-18]},{"t":10,"s":[3]},{"t":30,"s":[0]}]},"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[150,150,100]},{"t":10,"s":[95,95,100]},{"t":30,"s":[100,100,100]}]}},"shapes":[{"ty":"el","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[220,220]}},{"ty":"st","c":{"a":0,"k":[0.09,0.39,0.49,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":20},"lc":2,"lj":2}],"ip":0,"op":30,"st":0,"bm":0}],"markers":[]}
+{
+ "v": "5.7.4",
+ "fr": 30,
+ "ip": 0,
+ "op": 30,
+ "w": 512,
+ "h": 512,
+ "nm": "bert passport stamp accent",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "stamp",
+ "ks": {
+ "o": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 0
+ ]
+ },
+ {
+ "t": 10,
+ "s": [
+ 100
+ ]
+ },
+ {
+ "t": 30,
+ "s": [
+ 100
+ ]
+ }
+ ]
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ -18
+ ]
+ },
+ {
+ "t": 10,
+ "s": [
+ 3
+ ]
+ },
+ {
+ "t": 30,
+ "s": [
+ 0
+ ]
+ }
+ ]
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 420,
+ 95,
+ 0
+ ]
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 150,
+ 150,
+ 100
+ ]
+ },
+ {
+ "t": 10,
+ "s": [
+ 95,
+ 95,
+ 100
+ ]
+ },
+ {
+ "t": 30,
+ "s": [
+ 100,
+ 100,
+ 100
+ ]
+ }
+ ]
+ }
+ },
+ "shapes": [
+ {
+ "ty": "el",
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 72,
+ 72
+ ]
+ }
+ },
+ {
+ "ty": "st",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.466667,
+ 0.545098,
+ 0.647059,
+ 1
+ ]
+ },
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "w": {
+ "a": 0,
+ "k": 20
+ },
+ "lc": 2,
+ "lj": 2
+ }
+ ],
+ "ip": 0,
+ "op": 30,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
diff --git a/composeApp/src/commonMain/composeResources/files/bert_streak.json b/composeApp/src/commonMain/composeResources/files/bert_streak.json
index ed2a1a5..eccb5f6 100644
--- a/composeApp/src/commonMain/composeResources/files/bert_streak.json
+++ b/composeApp/src/commonMain/composeResources/files/bert_streak.json
@@ -1 +1,112 @@
-{"v":"5.7.4","fr":30,"ip":0,"op":60,"w":512,"h":512,"nm":"bert streak accent","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"streak ring","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"t":0,"s":[0]},{"t":60,"s":[360]}]},"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"shapes":[{"ty":"el","p":{"a":0,"k":[0,0]},"s":{"a":0,"k":[210,210]}},{"ty":"st","c":{"a":0,"k":[0.91,0.43,0.36,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":22},"lc":2,"lj":2}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]}
+{
+ "v": "5.7.4",
+ "fr": 30,
+ "ip": 0,
+ "op": 60,
+ "w": 512,
+ "h": 512,
+ "nm": "bert streak accent",
+ "ddd": 0,
+ "assets": [],
+ "layers": [
+ {
+ "ddd": 0,
+ "ind": 1,
+ "ty": 4,
+ "nm": "streak ring",
+ "ks": {
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "r": {
+ "a": 1,
+ "k": [
+ {
+ "t": 0,
+ "s": [
+ 0
+ ]
+ },
+ {
+ "t": 60,
+ "s": [
+ 360
+ ]
+ }
+ ]
+ },
+ "p": {
+ "a": 0,
+ "k": [
+ 420,
+ 95,
+ 0
+ ]
+ },
+ "a": {
+ "a": 0,
+ "k": [
+ 0,
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 100,
+ 100,
+ 100
+ ]
+ }
+ },
+ "shapes": [
+ {
+ "ty": "el",
+ "p": {
+ "a": 0,
+ "k": [
+ 0,
+ 0
+ ]
+ },
+ "s": {
+ "a": 0,
+ "k": [
+ 70,
+ 70
+ ]
+ }
+ },
+ {
+ "ty": "st",
+ "c": {
+ "a": 0,
+ "k": [
+ 0.968627,
+ 0.74902,
+ 0.745098,
+ 1
+ ]
+ },
+ "o": {
+ "a": 0,
+ "k": 100
+ },
+ "w": {
+ "a": 0,
+ "k": 22
+ },
+ "lc": 2,
+ "lj": 2
+ }
+ ],
+ "ip": 0,
+ "op": 60,
+ "st": 0,
+ "bm": 0
+ }
+ ],
+ "markers": []
+}
diff --git a/composeApp/src/commonMain/composeResources/files/catalog/catalog-v1.json b/composeApp/src/commonMain/composeResources/files/catalog/catalog-v1.json
index 5a9a4b5..e4553b4 100644
--- a/composeApp/src/commonMain/composeResources/files/catalog/catalog-v1.json
+++ b/composeApp/src/commonMain/composeResources/files/catalog/catalog-v1.json
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
- "catalogVersion": "1.0.0-draft.1",
+ "catalogVersion": "1.0.0-draft.2",
"countryCanon": "un193-plus-palestine-and-vatican-city",
"editorialPolicyVersion": "1.0.0",
"generatedFromPinnedDataAt": "2026-09-09",
@@ -52,6 +52,126 @@
"publisher": "Hampus Borgos and contributors",
"retrievedAt": "2026-09-09",
"revision": "c09927e63705529bbf59ca6684cd9b23225dddad"
+ },
+ {
+ "id": "source-rest-countries",
+ "title": "REST Countries V4 country data",
+ "url": "https://gitlab.com/restcountries/restcountries/-/tree/43f6260db10c54df9972e6e554ee14c2e7c85ef5",
+ "publisher": "REST Countries contributors",
+ "retrievedAt": "2026-09-10",
+ "revision": "43f6260db10c54df9972e6e554ee14c2e7c85ef5"
+ },
+ {
+ "id": "source-experience-oman-musandam",
+ "title": "Experience Oman: Musandam",
+ "url": "https://booking.experienceoman.om/en/destination/msd-93",
+ "publisher": "Ministry of Heritage and Tourism, Sultanate of Oman",
+ "retrievedAt": "2026-09-10",
+ "revision": "web snapshot 2026-09-10"
+ },
+ {
+ "id": "source-philippines-tourism-islands",
+ "title": "Island Hopping",
+ "url": "https://www.tourism.gov.ph/article/island-hopping-0-21/",
+ "publisher": "Department of Tourism, Republic of the Philippines",
+ "retrievedAt": "2026-09-10",
+ "revision": "web snapshot 2026-09-10"
+ },
+ {
+ "id": "source-geoscience-australia",
+ "title": "Australia's size compared",
+ "url": "https://www.ga.gov.au/scientific-topics/national-location-information/dimensions/australias-size-compared",
+ "publisher": "Geoscience Australia",
+ "retrievedAt": "2026-09-10",
+ "revision": "web snapshot 2026-09-10"
+ },
+ {
+ "id": "source-ibge-brazil",
+ "title": "Brazil: territory introduction",
+ "url": "https://educa.ibge.gov.br/jovens/conheca-o-brasil/territorio/20591-introducao.html",
+ "publisher": "Brazilian Institute of Geography and Statistics",
+ "retrievedAt": "2026-09-10",
+ "revision": "web snapshot 2026-09-10"
+ },
+ {
+ "id": "source-canada-coastlines",
+ "title": "Canada's coastlines",
+ "url": "https://www.canada.ca/en/services/environment/our-environment/nature-based-climate-solutions/coastlines.html",
+ "publisher": "Government of Canada",
+ "retrievedAt": "2026-09-10",
+ "revision": "web snapshot 2026-09-10"
+ },
+ {
+ "id": "source-incredible-india-dal-lake",
+ "title": "Dal Lake",
+ "url": "https://www.incredibleindia.gov.in/en/jammu-and-kashmir/srinagar/dal-lake",
+ "publisher": "Ministry of Tourism, Government of India",
+ "retrievedAt": "2026-09-10",
+ "revision": "web snapshot 2026-09-10"
+ },
+ {
+ "id": "source-gsi-japan-islands",
+ "title": "Re-counting the number of islands in Japan",
+ "url": "https://www.gsi.go.jp/kihonjohochousa/islands_index.html",
+ "publisher": "Geospatial Information Authority of Japan",
+ "retrievedAt": "2026-09-10",
+ "revision": "2022 digital-map recount published 2023-02-28"
+ },
+ {
+ "id": "source-mexico-flag-history",
+ "title": "Learn about the history of the Mexican flag",
+ "url": "https://www.gob.mx/sre/articulos/learn-about-the-history-of-the-mexican-flag-esmibandera",
+ "publisher": "Government of Mexico, Secretariat of Foreign Affairs",
+ "retrievedAt": "2026-09-10",
+ "revision": "web snapshot 2026-09-10"
+ },
+ {
+ "id": "source-south-africa-provinces",
+ "title": "South Africa's provinces",
+ "url": "https://www.gov.za/about-sa/south-africas-provinces",
+ "publisher": "Government of South Africa",
+ "retrievedAt": "2026-09-10",
+ "revision": "web snapshot 2026-09-10"
+ },
+ {
+ "id": "source-rmg-greenwich",
+ "title": "Discover the home of Greenwich Mean Time",
+ "url": "https://www.rmg.co.uk/plan-your-visit/visit-greenwich/discover-home-of-time",
+ "publisher": "Royal Museums Greenwich",
+ "retrievedAt": "2026-09-10",
+ "revision": "web snapshot 2026-09-10"
+ },
+ {
+ "id": "source-nz-womens-suffrage",
+ "title": "Women and the vote",
+ "url": "https://nzhistory.govt.nz/politics/womens-suffrage",
+ "publisher": "New Zealand Ministry for Culture and Heritage",
+ "retrievedAt": "2026-09-10",
+ "revision": "web snapshot 2026-09-10"
+ },
+ {
+ "id": "source-nepal-film-guide",
+ "title": "Filming in Nepal: a guide",
+ "url": "https://film.gov.np/media/filmgov/uploads/Filming_in_Nepal_2010_a_Guide_bOok_Chiranjibi_Guragain1.pdf",
+ "publisher": "Government of Nepal Film Development Board",
+ "retrievedAt": "2026-09-10",
+ "revision": "2010 guide"
+ },
+ {
+ "id": "source-netherlands-govtech",
+ "title": "GovTech in the Netherlands",
+ "url": "https://www.government.nl/binaries/government/documenten/reports/2021/06/30/govtech-in-the-netherlands/PUBLIC-GovTech-NL.pdf",
+ "publisher": "Government of the Netherlands",
+ "retrievedAt": "2026-09-10",
+ "revision": "2021-06 report"
+ },
+ {
+ "id": "source-cia-monaco-flag-archive",
+ "title": "Monaco flag",
+ "url": "https://www.cia.gov/the-world-factbook/about/archives/2023/countries/monaco/flag/",
+ "publisher": "CIA World Factbook archive",
+ "retrievedAt": "2026-09-10",
+ "revision": "2023 archive"
}
],
"countries": [
@@ -89,7 +209,7 @@
"letter_bank"
],
"factIds": [
- "fact-af-m49-region"
+ "fact-af-land-neighbours"
],
"review": {
"state": "draft",
@@ -131,7 +251,7 @@
"letter_bank"
],
"factIds": [
- "fact-al-m49-region"
+ "fact-al-land-neighbours"
],
"review": {
"state": "draft",
@@ -173,7 +293,7 @@
"letter_bank"
],
"factIds": [
- "fact-dz-m49-region"
+ "fact-dz-land-neighbours"
],
"review": {
"state": "draft",
@@ -215,7 +335,7 @@
"letter_bank"
],
"factIds": [
- "fact-ad-m49-region"
+ "fact-ad-land-neighbours"
],
"review": {
"state": "draft",
@@ -257,7 +377,7 @@
"letter_bank"
],
"factIds": [
- "fact-ao-m49-region"
+ "fact-ao-land-neighbours"
],
"review": {
"state": "draft",
@@ -301,7 +421,7 @@
"letter_bank"
],
"factIds": [
- "fact-ag-m49-region"
+ "fact-ag-land-neighbours"
],
"review": {
"state": "draft",
@@ -343,7 +463,7 @@
"letter_bank"
],
"factIds": [
- "fact-ar-m49-region"
+ "fact-ar-land-neighbours"
],
"review": {
"state": "draft",
@@ -385,7 +505,7 @@
"letter_bank"
],
"factIds": [
- "fact-am-m49-region"
+ "fact-am-land-neighbours"
],
"review": {
"state": "draft",
@@ -427,7 +547,7 @@
"letter_bank"
],
"factIds": [
- "fact-au-m49-region"
+ "fact-au-land-neighbours"
],
"review": {
"state": "draft",
@@ -469,7 +589,7 @@
"letter_bank"
],
"factIds": [
- "fact-at-m49-region"
+ "fact-at-land-neighbours"
],
"review": {
"state": "draft",
@@ -511,7 +631,7 @@
"letter_bank"
],
"factIds": [
- "fact-az-m49-region"
+ "fact-az-land-neighbours"
],
"review": {
"state": "draft",
@@ -553,7 +673,7 @@
"letter_bank"
],
"factIds": [
- "fact-bs-m49-region"
+ "fact-bs-land-neighbours"
],
"review": {
"state": "draft",
@@ -595,7 +715,7 @@
"letter_bank"
],
"factIds": [
- "fact-bh-m49-region"
+ "fact-bh-land-neighbours"
],
"review": {
"state": "draft",
@@ -637,7 +757,7 @@
"letter_bank"
],
"factIds": [
- "fact-bd-m49-region"
+ "fact-bd-land-neighbours"
],
"review": {
"state": "draft",
@@ -679,7 +799,7 @@
"letter_bank"
],
"factIds": [
- "fact-bb-m49-region"
+ "fact-bb-land-neighbours"
],
"review": {
"state": "draft",
@@ -721,7 +841,7 @@
"letter_bank"
],
"factIds": [
- "fact-by-m49-region"
+ "fact-by-land-neighbours"
],
"review": {
"state": "draft",
@@ -763,7 +883,7 @@
"letter_bank"
],
"factIds": [
- "fact-be-m49-region"
+ "fact-be-land-neighbours"
],
"review": {
"state": "draft",
@@ -805,7 +925,7 @@
"letter_bank"
],
"factIds": [
- "fact-bz-m49-region"
+ "fact-bz-land-neighbours"
],
"review": {
"state": "draft",
@@ -848,7 +968,7 @@
"letter_bank"
],
"factIds": [
- "fact-bj-m49-region"
+ "fact-bj-land-neighbours"
],
"review": {
"state": "draft",
@@ -890,7 +1010,7 @@
"letter_bank"
],
"factIds": [
- "fact-bt-m49-region"
+ "fact-bt-land-neighbours"
],
"review": {
"state": "draft",
@@ -935,7 +1055,7 @@
"letter_bank"
],
"factIds": [
- "fact-bo-m49-region"
+ "fact-bo-land-neighbours"
],
"review": {
"state": "draft",
@@ -979,7 +1099,7 @@
"letter_bank"
],
"factIds": [
- "fact-ba-m49-region"
+ "fact-ba-land-neighbours"
],
"review": {
"state": "draft",
@@ -1021,7 +1141,7 @@
"letter_bank"
],
"factIds": [
- "fact-bw-m49-region"
+ "fact-bw-land-neighbours"
],
"review": {
"state": "draft",
@@ -1063,7 +1183,7 @@
"letter_bank"
],
"factIds": [
- "fact-br-m49-region"
+ "fact-br-land-neighbours"
],
"review": {
"state": "draft",
@@ -1107,7 +1227,7 @@
"letter_bank"
],
"factIds": [
- "fact-bn-m49-region"
+ "fact-bn-land-neighbours"
],
"review": {
"state": "draft",
@@ -1149,7 +1269,7 @@
"letter_bank"
],
"factIds": [
- "fact-bg-m49-region"
+ "fact-bg-land-neighbours"
],
"review": {
"state": "draft",
@@ -1191,7 +1311,7 @@
"letter_bank"
],
"factIds": [
- "fact-bf-m49-region"
+ "fact-bf-land-neighbours"
],
"review": {
"state": "draft",
@@ -1233,7 +1353,7 @@
"letter_bank"
],
"factIds": [
- "fact-bi-m49-region"
+ "fact-bi-land-neighbours"
],
"review": {
"state": "draft",
@@ -1275,7 +1395,7 @@
"letter_bank"
],
"factIds": [
- "fact-kh-m49-region"
+ "fact-kh-land-neighbours"
],
"review": {
"state": "draft",
@@ -1317,7 +1437,7 @@
"letter_bank"
],
"factIds": [
- "fact-cm-m49-region"
+ "fact-cm-land-neighbours"
],
"review": {
"state": "draft",
@@ -1359,7 +1479,7 @@
"letter_bank"
],
"factIds": [
- "fact-ca-m49-region"
+ "fact-ca-land-neighbours"
],
"review": {
"state": "draft",
@@ -1401,7 +1521,7 @@
"letter_bank"
],
"factIds": [
- "fact-cv-m49-region"
+ "fact-cv-land-neighbours"
],
"review": {
"state": "draft",
@@ -1443,7 +1563,7 @@
"letter_bank"
],
"factIds": [
- "fact-cf-m49-region"
+ "fact-cf-land-neighbours"
],
"review": {
"state": "draft",
@@ -1485,7 +1605,7 @@
"letter_bank"
],
"factIds": [
- "fact-td-m49-region"
+ "fact-td-land-neighbours"
],
"review": {
"state": "draft",
@@ -1527,7 +1647,7 @@
"letter_bank"
],
"factIds": [
- "fact-cl-m49-region"
+ "fact-cl-land-neighbours"
],
"review": {
"state": "draft",
@@ -1569,7 +1689,7 @@
"letter_bank"
],
"factIds": [
- "fact-cn-m49-region"
+ "fact-cn-land-neighbours"
],
"review": {
"state": "draft",
@@ -1611,7 +1731,7 @@
"letter_bank"
],
"factIds": [
- "fact-co-m49-region"
+ "fact-co-land-neighbours"
],
"review": {
"state": "draft",
@@ -1653,7 +1773,7 @@
"letter_bank"
],
"factIds": [
- "fact-km-m49-region"
+ "fact-km-land-neighbours"
],
"review": {
"state": "draft",
@@ -1695,7 +1815,7 @@
"letter_bank"
],
"factIds": [
- "fact-cr-m49-region"
+ "fact-cr-land-neighbours"
],
"review": {
"state": "draft",
@@ -1737,7 +1857,7 @@
"letter_bank"
],
"factIds": [
- "fact-hr-m49-region"
+ "fact-hr-land-neighbours"
],
"review": {
"state": "draft",
@@ -1779,7 +1899,7 @@
"letter_bank"
],
"factIds": [
- "fact-cu-m49-region"
+ "fact-cu-land-neighbours"
],
"review": {
"state": "draft",
@@ -1821,7 +1941,7 @@
"letter_bank"
],
"factIds": [
- "fact-cy-m49-region"
+ "fact-cy-land-neighbours"
],
"review": {
"state": "draft",
@@ -1865,7 +1985,7 @@
"letter_bank"
],
"factIds": [
- "fact-cz-m49-region"
+ "fact-cz-land-neighbours"
],
"review": {
"state": "draft",
@@ -1911,7 +2031,7 @@
"letter_bank"
],
"factIds": [
- "fact-ci-m49-region"
+ "fact-ci-land-neighbours"
],
"review": {
"state": "draft",
@@ -1957,7 +2077,7 @@
"letter_bank"
],
"factIds": [
- "fact-cd-m49-region"
+ "fact-cd-land-neighbours"
],
"review": {
"state": "draft",
@@ -1999,7 +2119,7 @@
"letter_bank"
],
"factIds": [
- "fact-dk-m49-region"
+ "fact-dk-land-neighbours"
],
"review": {
"state": "draft",
@@ -2041,7 +2161,7 @@
"letter_bank"
],
"factIds": [
- "fact-dj-m49-region"
+ "fact-dj-land-neighbours"
],
"review": {
"state": "draft",
@@ -2083,7 +2203,7 @@
"letter_bank"
],
"factIds": [
- "fact-dm-m49-region"
+ "fact-dm-land-neighbours"
],
"review": {
"state": "draft",
@@ -2125,7 +2245,7 @@
"letter_bank"
],
"factIds": [
- "fact-do-m49-region"
+ "fact-do-land-neighbours"
],
"review": {
"state": "draft",
@@ -2167,7 +2287,7 @@
"letter_bank"
],
"factIds": [
- "fact-ec-m49-region"
+ "fact-ec-land-neighbours"
],
"review": {
"state": "draft",
@@ -2209,7 +2329,7 @@
"letter_bank"
],
"factIds": [
- "fact-eg-m49-region"
+ "fact-eg-land-neighbours"
],
"review": {
"state": "draft",
@@ -2251,7 +2371,7 @@
"letter_bank"
],
"factIds": [
- "fact-sv-m49-region"
+ "fact-sv-land-neighbours"
],
"review": {
"state": "draft",
@@ -2293,7 +2413,7 @@
"letter_bank"
],
"factIds": [
- "fact-gq-m49-region"
+ "fact-gq-land-neighbours"
],
"review": {
"state": "draft",
@@ -2335,7 +2455,7 @@
"letter_bank"
],
"factIds": [
- "fact-er-m49-region"
+ "fact-er-land-neighbours"
],
"review": {
"state": "draft",
@@ -2377,7 +2497,7 @@
"letter_bank"
],
"factIds": [
- "fact-ee-m49-region"
+ "fact-ee-land-neighbours"
],
"review": {
"state": "draft",
@@ -2420,7 +2540,7 @@
"letter_bank"
],
"factIds": [
- "fact-sz-m49-region"
+ "fact-sz-land-neighbours"
],
"review": {
"state": "draft",
@@ -2462,7 +2582,7 @@
"letter_bank"
],
"factIds": [
- "fact-et-m49-region"
+ "fact-et-land-neighbours"
],
"review": {
"state": "draft",
@@ -2504,7 +2624,7 @@
"letter_bank"
],
"factIds": [
- "fact-fj-m49-region"
+ "fact-fj-land-neighbours"
],
"review": {
"state": "draft",
@@ -2546,7 +2666,7 @@
"letter_bank"
],
"factIds": [
- "fact-fi-m49-region"
+ "fact-fi-land-neighbours"
],
"review": {
"state": "draft",
@@ -2588,7 +2708,7 @@
"letter_bank"
],
"factIds": [
- "fact-fr-m49-region"
+ "fact-fr-land-neighbours"
],
"review": {
"state": "draft",
@@ -2630,7 +2750,7 @@
"letter_bank"
],
"factIds": [
- "fact-ga-m49-region"
+ "fact-ga-land-neighbours"
],
"review": {
"state": "draft",
@@ -2674,7 +2794,7 @@
"letter_bank"
],
"factIds": [
- "fact-gm-m49-region"
+ "fact-gm-land-neighbours"
],
"review": {
"state": "draft",
@@ -2716,7 +2836,7 @@
"letter_bank"
],
"factIds": [
- "fact-ge-m49-region"
+ "fact-ge-land-neighbours"
],
"review": {
"state": "draft",
@@ -2758,7 +2878,7 @@
"letter_bank"
],
"factIds": [
- "fact-de-m49-region"
+ "fact-de-land-neighbours"
],
"review": {
"state": "draft",
@@ -2800,7 +2920,7 @@
"letter_bank"
],
"factIds": [
- "fact-gh-m49-region"
+ "fact-gh-land-neighbours"
],
"review": {
"state": "draft",
@@ -2842,7 +2962,7 @@
"letter_bank"
],
"factIds": [
- "fact-gr-m49-region"
+ "fact-gr-land-neighbours"
],
"review": {
"state": "draft",
@@ -2884,7 +3004,7 @@
"letter_bank"
],
"factIds": [
- "fact-gd-m49-region"
+ "fact-gd-land-neighbours"
],
"review": {
"state": "draft",
@@ -2926,7 +3046,7 @@
"letter_bank"
],
"factIds": [
- "fact-gt-m49-region"
+ "fact-gt-land-neighbours"
],
"review": {
"state": "draft",
@@ -2968,7 +3088,7 @@
"letter_bank"
],
"factIds": [
- "fact-gn-m49-region"
+ "fact-gn-land-neighbours"
],
"review": {
"state": "draft",
@@ -3010,7 +3130,7 @@
"letter_bank"
],
"factIds": [
- "fact-gw-m49-region"
+ "fact-gw-land-neighbours"
],
"review": {
"state": "draft",
@@ -3052,7 +3172,7 @@
"letter_bank"
],
"factIds": [
- "fact-gy-m49-region"
+ "fact-gy-land-neighbours"
],
"review": {
"state": "draft",
@@ -3094,7 +3214,7 @@
"letter_bank"
],
"factIds": [
- "fact-ht-m49-region"
+ "fact-ht-land-neighbours"
],
"review": {
"state": "draft",
@@ -3136,7 +3256,7 @@
"letter_bank"
],
"factIds": [
- "fact-hn-m49-region"
+ "fact-hn-land-neighbours"
],
"review": {
"state": "draft",
@@ -3178,7 +3298,7 @@
"letter_bank"
],
"factIds": [
- "fact-hu-m49-region"
+ "fact-hu-land-neighbours"
],
"review": {
"state": "draft",
@@ -3220,7 +3340,7 @@
"letter_bank"
],
"factIds": [
- "fact-is-m49-region"
+ "fact-is-land-neighbours"
],
"review": {
"state": "draft",
@@ -3262,7 +3382,7 @@
"letter_bank"
],
"factIds": [
- "fact-in-m49-region"
+ "fact-in-land-neighbours"
],
"review": {
"state": "draft",
@@ -3305,7 +3425,7 @@
"letter_bank"
],
"factIds": [
- "fact-id-m49-region"
+ "fact-id-land-neighbours"
],
"review": {
"state": "draft",
@@ -3349,7 +3469,7 @@
"letter_bank"
],
"factIds": [
- "fact-ir-m49-region"
+ "fact-ir-land-neighbours"
],
"review": {
"state": "draft",
@@ -3391,7 +3511,7 @@
"letter_bank"
],
"factIds": [
- "fact-iq-m49-region"
+ "fact-iq-land-neighbours"
],
"review": {
"state": "draft",
@@ -3433,7 +3553,7 @@
"letter_bank"
],
"factIds": [
- "fact-ie-m49-region"
+ "fact-ie-land-neighbours"
],
"review": {
"state": "draft",
@@ -3475,7 +3595,7 @@
"letter_bank"
],
"factIds": [
- "fact-il-m49-region"
+ "fact-il-land-neighbours"
],
"review": {
"state": "draft",
@@ -3517,7 +3637,7 @@
"letter_bank"
],
"factIds": [
- "fact-it-m49-region"
+ "fact-it-land-neighbours"
],
"review": {
"state": "draft",
@@ -3559,7 +3679,7 @@
"letter_bank"
],
"factIds": [
- "fact-jm-m49-region"
+ "fact-jm-land-neighbours"
],
"review": {
"state": "draft",
@@ -3601,7 +3721,7 @@
"letter_bank"
],
"factIds": [
- "fact-jp-m49-region"
+ "fact-jp-land-neighbours"
],
"review": {
"state": "draft",
@@ -3643,7 +3763,7 @@
"letter_bank"
],
"factIds": [
- "fact-jo-m49-region"
+ "fact-jo-land-neighbours"
],
"review": {
"state": "draft",
@@ -3685,7 +3805,7 @@
"letter_bank"
],
"factIds": [
- "fact-kz-m49-region"
+ "fact-kz-land-neighbours"
],
"review": {
"state": "draft",
@@ -3727,7 +3847,7 @@
"letter_bank"
],
"factIds": [
- "fact-ke-m49-region"
+ "fact-ke-land-neighbours"
],
"review": {
"state": "draft",
@@ -3769,7 +3889,7 @@
"letter_bank"
],
"factIds": [
- "fact-ki-m49-region"
+ "fact-ki-land-neighbours"
],
"review": {
"state": "draft",
@@ -3811,7 +3931,7 @@
"letter_bank"
],
"factIds": [
- "fact-kw-m49-region"
+ "fact-kw-land-neighbours"
],
"review": {
"state": "draft",
@@ -3853,7 +3973,7 @@
"letter_bank"
],
"factIds": [
- "fact-kg-m49-region"
+ "fact-kg-land-neighbours"
],
"review": {
"state": "draft",
@@ -3897,7 +4017,7 @@
"letter_bank"
],
"factIds": [
- "fact-la-m49-region"
+ "fact-la-land-neighbours"
],
"review": {
"state": "draft",
@@ -3939,7 +4059,7 @@
"letter_bank"
],
"factIds": [
- "fact-lv-m49-region"
+ "fact-lv-land-neighbours"
],
"review": {
"state": "draft",
@@ -3981,7 +4101,7 @@
"letter_bank"
],
"factIds": [
- "fact-lb-m49-region"
+ "fact-lb-land-neighbours"
],
"review": {
"state": "draft",
@@ -4023,7 +4143,7 @@
"letter_bank"
],
"factIds": [
- "fact-ls-m49-region"
+ "fact-ls-land-neighbours"
],
"review": {
"state": "draft",
@@ -4065,7 +4185,7 @@
"letter_bank"
],
"factIds": [
- "fact-lr-m49-region"
+ "fact-lr-land-neighbours"
],
"review": {
"state": "draft",
@@ -4107,7 +4227,7 @@
"letter_bank"
],
"factIds": [
- "fact-ly-m49-region"
+ "fact-ly-land-neighbours"
],
"review": {
"state": "draft",
@@ -4149,7 +4269,7 @@
"letter_bank"
],
"factIds": [
- "fact-li-m49-region"
+ "fact-li-land-neighbours"
],
"review": {
"state": "draft",
@@ -4191,7 +4311,7 @@
"letter_bank"
],
"factIds": [
- "fact-lt-m49-region"
+ "fact-lt-land-neighbours"
],
"review": {
"state": "draft",
@@ -4233,7 +4353,7 @@
"letter_bank"
],
"factIds": [
- "fact-lu-m49-region"
+ "fact-lu-land-neighbours"
],
"review": {
"state": "draft",
@@ -4275,7 +4395,7 @@
"letter_bank"
],
"factIds": [
- "fact-mg-m49-region"
+ "fact-mg-land-neighbours"
],
"review": {
"state": "draft",
@@ -4317,7 +4437,7 @@
"letter_bank"
],
"factIds": [
- "fact-mw-m49-region"
+ "fact-mw-land-neighbours"
],
"review": {
"state": "draft",
@@ -4360,7 +4480,7 @@
"letter_bank"
],
"factIds": [
- "fact-my-m49-region"
+ "fact-my-land-neighbours"
],
"review": {
"state": "draft",
@@ -4402,7 +4522,7 @@
"letter_bank"
],
"factIds": [
- "fact-mv-m49-region"
+ "fact-mv-land-neighbours"
],
"review": {
"state": "draft",
@@ -4444,7 +4564,7 @@
"letter_bank"
],
"factIds": [
- "fact-ml-m49-region"
+ "fact-ml-land-neighbours"
],
"review": {
"state": "draft",
@@ -4486,7 +4606,7 @@
"letter_bank"
],
"factIds": [
- "fact-mt-m49-region"
+ "fact-mt-land-neighbours"
],
"review": {
"state": "draft",
@@ -4528,7 +4648,7 @@
"letter_bank"
],
"factIds": [
- "fact-mh-m49-region"
+ "fact-mh-land-neighbours"
],
"review": {
"state": "draft",
@@ -4570,7 +4690,7 @@
"letter_bank"
],
"factIds": [
- "fact-mr-m49-region"
+ "fact-mr-land-neighbours"
],
"review": {
"state": "draft",
@@ -4612,7 +4732,7 @@
"letter_bank"
],
"factIds": [
- "fact-mu-m49-region"
+ "fact-mu-land-neighbours"
],
"review": {
"state": "draft",
@@ -4654,7 +4774,7 @@
"letter_bank"
],
"factIds": [
- "fact-mx-m49-region"
+ "fact-mx-land-neighbours"
],
"review": {
"state": "draft",
@@ -4698,7 +4818,7 @@
"letter_bank"
],
"factIds": [
- "fact-fm-m49-region"
+ "fact-fm-land-neighbours"
],
"review": {
"state": "draft",
@@ -4742,7 +4862,7 @@
"letter_bank"
],
"factIds": [
- "fact-md-m49-region"
+ "fact-md-land-neighbours"
],
"review": {
"state": "draft",
@@ -4784,7 +4904,7 @@
"letter_bank"
],
"factIds": [
- "fact-mc-m49-region"
+ "fact-mc-land-neighbours"
],
"review": {
"state": "draft",
@@ -4826,7 +4946,7 @@
"letter_bank"
],
"factIds": [
- "fact-mn-m49-region"
+ "fact-mn-land-neighbours"
],
"review": {
"state": "draft",
@@ -4868,7 +4988,7 @@
"letter_bank"
],
"factIds": [
- "fact-me-m49-region"
+ "fact-me-land-neighbours"
],
"review": {
"state": "draft",
@@ -4910,7 +5030,7 @@
"letter_bank"
],
"factIds": [
- "fact-ma-m49-region"
+ "fact-ma-land-neighbours"
],
"review": {
"state": "draft",
@@ -4952,7 +5072,7 @@
"letter_bank"
],
"factIds": [
- "fact-mz-m49-region"
+ "fact-mz-land-neighbours"
],
"review": {
"state": "draft",
@@ -4994,7 +5114,7 @@
"letter_bank"
],
"factIds": [
- "fact-mm-m49-region"
+ "fact-mm-land-neighbours"
],
"review": {
"state": "draft",
@@ -5036,7 +5156,7 @@
"letter_bank"
],
"factIds": [
- "fact-na-m49-region"
+ "fact-na-land-neighbours"
],
"review": {
"state": "draft",
@@ -5078,7 +5198,7 @@
"letter_bank"
],
"factIds": [
- "fact-nr-m49-region"
+ "fact-nr-land-neighbours"
],
"review": {
"state": "draft",
@@ -5120,7 +5240,7 @@
"letter_bank"
],
"factIds": [
- "fact-np-m49-region"
+ "fact-np-land-neighbours"
],
"review": {
"state": "draft",
@@ -5163,7 +5283,7 @@
"letter_bank"
],
"factIds": [
- "fact-nl-m49-region"
+ "fact-nl-land-neighbours"
],
"review": {
"state": "draft",
@@ -5205,7 +5325,7 @@
"letter_bank"
],
"factIds": [
- "fact-nz-m49-region"
+ "fact-nz-land-neighbours"
],
"review": {
"state": "draft",
@@ -5247,7 +5367,7 @@
"letter_bank"
],
"factIds": [
- "fact-ni-m49-region"
+ "fact-ni-land-neighbours"
],
"review": {
"state": "draft",
@@ -5289,7 +5409,7 @@
"letter_bank"
],
"factIds": [
- "fact-ne-m49-region"
+ "fact-ne-land-neighbours"
],
"review": {
"state": "draft",
@@ -5331,7 +5451,7 @@
"letter_bank"
],
"factIds": [
- "fact-ng-m49-region"
+ "fact-ng-land-neighbours"
],
"review": {
"state": "draft",
@@ -5375,7 +5495,7 @@
"letter_bank"
],
"factIds": [
- "fact-kp-m49-region"
+ "fact-kp-land-neighbours"
],
"review": {
"state": "draft",
@@ -5417,7 +5537,7 @@
"letter_bank"
],
"factIds": [
- "fact-mk-m49-region"
+ "fact-mk-land-neighbours"
],
"review": {
"state": "draft",
@@ -5459,7 +5579,7 @@
"letter_bank"
],
"factIds": [
- "fact-no-m49-region"
+ "fact-no-land-neighbours"
],
"review": {
"state": "draft",
@@ -5501,7 +5621,7 @@
"letter_bank"
],
"factIds": [
- "fact-om-m49-region"
+ "fact-om-musandam"
],
"review": {
"state": "draft",
@@ -5543,7 +5663,7 @@
"letter_bank"
],
"factIds": [
- "fact-pk-m49-region"
+ "fact-pk-land-neighbours"
],
"review": {
"state": "draft",
@@ -5585,7 +5705,7 @@
"letter_bank"
],
"factIds": [
- "fact-pw-m49-region"
+ "fact-pw-land-neighbours"
],
"review": {
"state": "draft",
@@ -5630,7 +5750,7 @@
"letter_bank"
],
"factIds": [
- "fact-ps-m49-region"
+ "fact-ps-land-neighbours"
],
"review": {
"state": "draft",
@@ -5672,7 +5792,7 @@
"letter_bank"
],
"factIds": [
- "fact-pa-m49-region"
+ "fact-pa-land-neighbours"
],
"review": {
"state": "draft",
@@ -5714,7 +5834,7 @@
"letter_bank"
],
"factIds": [
- "fact-pg-m49-region"
+ "fact-pg-land-neighbours"
],
"review": {
"state": "draft",
@@ -5756,7 +5876,7 @@
"letter_bank"
],
"factIds": [
- "fact-py-m49-region"
+ "fact-py-land-neighbours"
],
"review": {
"state": "draft",
@@ -5798,7 +5918,7 @@
"letter_bank"
],
"factIds": [
- "fact-pe-m49-region"
+ "fact-pe-land-neighbours"
],
"review": {
"state": "draft",
@@ -5840,7 +5960,7 @@
"letter_bank"
],
"factIds": [
- "fact-ph-m49-region"
+ "fact-ph-islands"
],
"review": {
"state": "draft",
@@ -5882,7 +6002,7 @@
"letter_bank"
],
"factIds": [
- "fact-pl-m49-region"
+ "fact-pl-land-neighbours"
],
"review": {
"state": "draft",
@@ -5924,7 +6044,7 @@
"letter_bank"
],
"factIds": [
- "fact-pt-m49-region"
+ "fact-pt-land-neighbours"
],
"review": {
"state": "draft",
@@ -5966,7 +6086,7 @@
"letter_bank"
],
"factIds": [
- "fact-qa-m49-region"
+ "fact-qa-land-neighbours"
],
"review": {
"state": "draft",
@@ -6012,7 +6132,7 @@
"letter_bank"
],
"factIds": [
- "fact-cg-m49-region"
+ "fact-cg-land-neighbours"
],
"review": {
"state": "draft",
@@ -6054,7 +6174,7 @@
"letter_bank"
],
"factIds": [
- "fact-ro-m49-region"
+ "fact-ro-land-neighbours"
],
"review": {
"state": "draft",
@@ -6098,7 +6218,7 @@
"letter_bank"
],
"factIds": [
- "fact-ru-m49-region"
+ "fact-ru-land-neighbours"
],
"review": {
"state": "draft",
@@ -6140,7 +6260,7 @@
"letter_bank"
],
"factIds": [
- "fact-rw-m49-region"
+ "fact-rw-land-neighbours"
],
"review": {
"state": "draft",
@@ -6184,7 +6304,7 @@
"letter_bank"
],
"factIds": [
- "fact-kn-m49-region"
+ "fact-kn-land-neighbours"
],
"review": {
"state": "draft",
@@ -6228,7 +6348,7 @@
"letter_bank"
],
"factIds": [
- "fact-lc-m49-region"
+ "fact-lc-land-neighbours"
],
"review": {
"state": "draft",
@@ -6272,7 +6392,7 @@
"letter_bank"
],
"factIds": [
- "fact-vc-m49-region"
+ "fact-vc-land-neighbours"
],
"review": {
"state": "draft",
@@ -6314,7 +6434,7 @@
"letter_bank"
],
"factIds": [
- "fact-ws-m49-region"
+ "fact-ws-land-neighbours"
],
"review": {
"state": "draft",
@@ -6356,7 +6476,7 @@
"letter_bank"
],
"factIds": [
- "fact-sm-m49-region"
+ "fact-sm-land-neighbours"
],
"review": {
"state": "draft",
@@ -6398,7 +6518,7 @@
"letter_bank"
],
"factIds": [
- "fact-sa-m49-region"
+ "fact-sa-land-neighbours"
],
"review": {
"state": "draft",
@@ -6440,7 +6560,7 @@
"letter_bank"
],
"factIds": [
- "fact-sn-m49-region"
+ "fact-sn-land-neighbours"
],
"review": {
"state": "draft",
@@ -6482,7 +6602,7 @@
"letter_bank"
],
"factIds": [
- "fact-rs-m49-region"
+ "fact-rs-land-neighbours"
],
"review": {
"state": "draft",
@@ -6524,7 +6644,7 @@
"letter_bank"
],
"factIds": [
- "fact-sc-m49-region"
+ "fact-sc-land-neighbours"
],
"review": {
"state": "draft",
@@ -6566,7 +6686,7 @@
"letter_bank"
],
"factIds": [
- "fact-sl-m49-region"
+ "fact-sl-land-neighbours"
],
"review": {
"state": "draft",
@@ -6608,7 +6728,7 @@
"letter_bank"
],
"factIds": [
- "fact-sg-m49-region"
+ "fact-sg-land-neighbours"
],
"review": {
"state": "draft",
@@ -6650,7 +6770,7 @@
"letter_bank"
],
"factIds": [
- "fact-sk-m49-region"
+ "fact-sk-land-neighbours"
],
"review": {
"state": "draft",
@@ -6692,7 +6812,7 @@
"letter_bank"
],
"factIds": [
- "fact-si-m49-region"
+ "fact-si-land-neighbours"
],
"review": {
"state": "draft",
@@ -6734,7 +6854,7 @@
"letter_bank"
],
"factIds": [
- "fact-sb-m49-region"
+ "fact-sb-land-neighbours"
],
"review": {
"state": "draft",
@@ -6776,7 +6896,7 @@
"letter_bank"
],
"factIds": [
- "fact-so-m49-region"
+ "fact-so-land-neighbours"
],
"review": {
"state": "draft",
@@ -6820,7 +6940,7 @@
"letter_bank"
],
"factIds": [
- "fact-za-m49-region"
+ "fact-za-land-neighbours"
],
"review": {
"state": "draft",
@@ -6864,7 +6984,7 @@
"letter_bank"
],
"factIds": [
- "fact-kr-m49-region"
+ "fact-kr-land-neighbours"
],
"review": {
"state": "draft",
@@ -6906,7 +7026,7 @@
"letter_bank"
],
"factIds": [
- "fact-ss-m49-region"
+ "fact-ss-land-neighbours"
],
"review": {
"state": "draft",
@@ -6948,7 +7068,7 @@
"letter_bank"
],
"factIds": [
- "fact-es-m49-region"
+ "fact-es-land-neighbours"
],
"review": {
"state": "draft",
@@ -6991,7 +7111,7 @@
"letter_bank"
],
"factIds": [
- "fact-lk-m49-region"
+ "fact-lk-land-neighbours"
],
"review": {
"state": "draft",
@@ -7033,7 +7153,7 @@
"letter_bank"
],
"factIds": [
- "fact-sd-m49-region"
+ "fact-sd-land-neighbours"
],
"review": {
"state": "draft",
@@ -7075,7 +7195,7 @@
"letter_bank"
],
"factIds": [
- "fact-sr-m49-region"
+ "fact-sr-land-neighbours"
],
"review": {
"state": "draft",
@@ -7117,7 +7237,7 @@
"letter_bank"
],
"factIds": [
- "fact-se-m49-region"
+ "fact-se-land-neighbours"
],
"review": {
"state": "draft",
@@ -7159,7 +7279,7 @@
"letter_bank"
],
"factIds": [
- "fact-ch-m49-region"
+ "fact-ch-land-neighbours"
],
"review": {
"state": "draft",
@@ -7203,7 +7323,7 @@
"letter_bank"
],
"factIds": [
- "fact-sy-m49-region"
+ "fact-sy-land-neighbours"
],
"review": {
"state": "draft",
@@ -7247,7 +7367,7 @@
"letter_bank"
],
"factIds": [
- "fact-st-m49-region"
+ "fact-st-land-neighbours"
],
"review": {
"state": "draft",
@@ -7289,7 +7409,7 @@
"letter_bank"
],
"factIds": [
- "fact-tj-m49-region"
+ "fact-tj-land-neighbours"
],
"review": {
"state": "draft",
@@ -7333,7 +7453,7 @@
"letter_bank"
],
"factIds": [
- "fact-tz-m49-region"
+ "fact-tz-land-neighbours"
],
"review": {
"state": "draft",
@@ -7375,7 +7495,7 @@
"letter_bank"
],
"factIds": [
- "fact-th-m49-region"
+ "fact-th-land-neighbours"
],
"review": {
"state": "draft",
@@ -7417,7 +7537,7 @@
"letter_bank"
],
"factIds": [
- "fact-tl-m49-region"
+ "fact-tl-land-neighbours"
],
"review": {
"state": "draft",
@@ -7459,7 +7579,7 @@
"letter_bank"
],
"factIds": [
- "fact-tg-m49-region"
+ "fact-tg-land-neighbours"
],
"review": {
"state": "draft",
@@ -7501,7 +7621,7 @@
"letter_bank"
],
"factIds": [
- "fact-to-m49-region"
+ "fact-to-land-neighbours"
],
"review": {
"state": "draft",
@@ -7545,7 +7665,7 @@
"letter_bank"
],
"factIds": [
- "fact-tt-m49-region"
+ "fact-tt-land-neighbours"
],
"review": {
"state": "draft",
@@ -7587,7 +7707,7 @@
"letter_bank"
],
"factIds": [
- "fact-tn-m49-region"
+ "fact-tn-land-neighbours"
],
"review": {
"state": "draft",
@@ -7629,7 +7749,7 @@
"letter_bank"
],
"factIds": [
- "fact-tm-m49-region"
+ "fact-tm-land-neighbours"
],
"review": {
"state": "draft",
@@ -7671,7 +7791,7 @@
"letter_bank"
],
"factIds": [
- "fact-tv-m49-region"
+ "fact-tv-land-neighbours"
],
"review": {
"state": "draft",
@@ -7713,7 +7833,7 @@
"letter_bank"
],
"factIds": [
- "fact-tr-m49-region"
+ "fact-tr-land-neighbours"
],
"review": {
"state": "draft",
@@ -7755,7 +7875,7 @@
"letter_bank"
],
"factIds": [
- "fact-ug-m49-region"
+ "fact-ug-land-neighbours"
],
"review": {
"state": "draft",
@@ -7797,7 +7917,7 @@
"letter_bank"
],
"factIds": [
- "fact-ua-m49-region"
+ "fact-ua-land-neighbours"
],
"review": {
"state": "draft",
@@ -7839,7 +7959,7 @@
"letter_bank"
],
"factIds": [
- "fact-ae-m49-region"
+ "fact-ae-land-neighbours"
],
"review": {
"state": "draft",
@@ -7884,7 +8004,7 @@
"letter_bank"
],
"factIds": [
- "fact-gb-m49-region"
+ "fact-gb-land-neighbours"
],
"review": {
"state": "draft",
@@ -7930,7 +8050,7 @@
"letter_bank"
],
"factIds": [
- "fact-us-m49-region"
+ "fact-us-land-neighbours"
],
"review": {
"state": "draft",
@@ -7972,7 +8092,7 @@
"letter_bank"
],
"factIds": [
- "fact-uy-m49-region"
+ "fact-uy-land-neighbours"
],
"review": {
"state": "draft",
@@ -8014,7 +8134,7 @@
"letter_bank"
],
"factIds": [
- "fact-uz-m49-region"
+ "fact-uz-land-neighbours"
],
"review": {
"state": "draft",
@@ -8056,7 +8176,7 @@
"letter_bank"
],
"factIds": [
- "fact-vu-m49-region"
+ "fact-vu-land-neighbours"
],
"review": {
"state": "draft",
@@ -8100,7 +8220,7 @@
"letter_bank"
],
"factIds": [
- "fact-va-m49-region"
+ "fact-va-land-neighbours"
],
"review": {
"state": "draft",
@@ -8144,7 +8264,7 @@
"letter_bank"
],
"factIds": [
- "fact-ve-m49-region"
+ "fact-ve-land-neighbours"
],
"review": {
"state": "draft",
@@ -8188,7 +8308,7 @@
"letter_bank"
],
"factIds": [
- "fact-vn-m49-region"
+ "fact-vn-land-neighbours"
],
"review": {
"state": "draft",
@@ -8230,7 +8350,7 @@
"letter_bank"
],
"factIds": [
- "fact-ye-m49-region"
+ "fact-ye-land-neighbours"
],
"review": {
"state": "draft",
@@ -8272,7 +8392,7 @@
"letter_bank"
],
"factIds": [
- "fact-zm-m49-region"
+ "fact-zm-land-neighbours"
],
"review": {
"state": "draft",
@@ -8314,7 +8434,7 @@
"letter_bank"
],
"factIds": [
- "fact-zw-m49-region"
+ "fact-zw-land-neighbours"
],
"review": {
"state": "draft",
@@ -8325,17 +8445,17 @@
],
"facts": [
{
- "id": "fact-af-m49-region",
+ "id": "fact-af-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"af"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Afghanistan",
- "text": "The UN M49 standard groups Afghanistan in Southern Asia.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Afghanistan is landlocked, yet it touches six other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8345,17 +8465,17 @@
}
},
{
- "id": "fact-al-m49-region",
+ "id": "fact-al-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"al"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Albania",
- "text": "The UN M49 standard groups Albania in Southern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Albania shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8365,17 +8485,17 @@
}
},
{
- "id": "fact-dz-m49-region",
+ "id": "fact-dz-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"dz"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Algeria",
- "text": "The UN M49 standard groups Algeria in Northern Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Algeria shares land borders with seven countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8385,17 +8505,17 @@
}
},
{
- "id": "fact-ad-m49-region",
+ "id": "fact-ad-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ad"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Andorra",
- "text": "The UN M49 standard groups Andorra in Southern Europe.",
+ "heading": "Landlocked, not isolated",
+ "text": "Andorra is landlocked, yet it touches two other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8405,17 +8525,17 @@
}
},
{
- "id": "fact-ao-m49-region",
+ "id": "fact-ao-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ao"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Angola",
- "text": "The UN M49 standard groups Angola in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Angola shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8425,17 +8545,17 @@
}
},
{
- "id": "fact-ag-m49-region",
+ "id": "fact-ag-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ag"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Antigua and Barbuda",
- "text": "The UN M49 standard groups Antigua and Barbuda in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Antigua and Barbuda shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8445,17 +8565,17 @@
}
},
{
- "id": "fact-ar-m49-region",
+ "id": "fact-ar-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ar"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Argentina",
- "text": "The UN M49 standard groups Argentina in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Argentina shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8465,17 +8585,17 @@
}
},
{
- "id": "fact-am-m49-region",
+ "id": "fact-am-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"am"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Armenia",
- "text": "The UN M49 standard groups Armenia in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Armenia is landlocked, yet it touches four other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8485,17 +8605,17 @@
}
},
{
- "id": "fact-au-m49-region",
+ "id": "fact-au-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"au"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Australia",
- "text": "The UN M49 standard groups Australia in Australia and New Zealand.",
+ "heading": "A continent of its own",
+ "text": "Australia is both a country and the smallest continental landmass.",
"sourceIds": [
- "source-un-m49"
+ "source-geoscience-australia"
],
"asOf": null,
"review": {
@@ -8505,17 +8625,17 @@
}
},
{
- "id": "fact-at-m49-region",
+ "id": "fact-at-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"at"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Austria",
- "text": "The UN M49 standard groups Austria in Western Europe.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Austria is landlocked, yet it touches eight other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8525,17 +8645,17 @@
}
},
{
- "id": "fact-az-m49-region",
+ "id": "fact-az-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"az"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Azerbaijan",
- "text": "The UN M49 standard groups Azerbaijan in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Azerbaijan is landlocked, yet it touches five other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8545,17 +8665,17 @@
}
},
{
- "id": "fact-bs-m49-region",
+ "id": "fact-bs-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bs"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Bahamas",
- "text": "The UN M49 standard groups Bahamas in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Bahamas shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8565,17 +8685,17 @@
}
},
{
- "id": "fact-bh-m49-region",
+ "id": "fact-bh-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bh"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Bahrain",
- "text": "The UN M49 standard groups Bahrain in Western Asia.",
+ "heading": "No neighbours by land",
+ "text": "Bahrain shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8585,17 +8705,17 @@
}
},
{
- "id": "fact-bd-m49-region",
+ "id": "fact-bd-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bd"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Bangladesh",
- "text": "The UN M49 standard groups Bangladesh in Southern Asia.",
+ "heading": "Across the border",
+ "text": "Bangladesh shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8605,17 +8725,17 @@
}
},
{
- "id": "fact-bb-m49-region",
+ "id": "fact-bb-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bb"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Barbados",
- "text": "The UN M49 standard groups Barbados in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Barbados shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8625,17 +8745,17 @@
}
},
{
- "id": "fact-by-m49-region",
+ "id": "fact-by-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"by"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Belarus",
- "text": "The UN M49 standard groups Belarus in Eastern Europe.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Belarus is landlocked, yet it touches five other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8645,17 +8765,17 @@
}
},
{
- "id": "fact-be-m49-region",
+ "id": "fact-be-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"be"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Belgium",
- "text": "The UN M49 standard groups Belgium in Western Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Belgium shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8665,17 +8785,17 @@
}
},
{
- "id": "fact-bz-m49-region",
+ "id": "fact-bz-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bz"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Belize",
- "text": "The UN M49 standard groups Belize in Latin America and the Caribbean.",
+ "heading": "Across the border",
+ "text": "Belize shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8685,17 +8805,17 @@
}
},
{
- "id": "fact-bj-m49-region",
+ "id": "fact-bj-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bj"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Benin",
- "text": "The UN M49 standard groups Benin in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Benin shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8705,17 +8825,17 @@
}
},
{
- "id": "fact-bt-m49-region",
+ "id": "fact-bt-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bt"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Bhutan",
- "text": "The UN M49 standard groups Bhutan in Southern Asia.",
+ "heading": "Landlocked, not isolated",
+ "text": "Bhutan is landlocked, yet it touches two other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8725,17 +8845,17 @@
}
},
{
- "id": "fact-bo-m49-region",
+ "id": "fact-bo-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bo"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Bolivia",
- "text": "The UN M49 standard groups Bolivia in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Bolivia is landlocked, yet it touches five other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8745,17 +8865,17 @@
}
},
{
- "id": "fact-ba-m49-region",
+ "id": "fact-ba-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ba"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Bosnia and Herzegovina",
- "text": "The UN M49 standard groups Bosnia and Herzegovina in Southern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Bosnia and Herzegovina shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8765,17 +8885,17 @@
}
},
{
- "id": "fact-bw-m49-region",
+ "id": "fact-bw-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bw"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Botswana",
- "text": "The UN M49 standard groups Botswana in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Botswana is landlocked, yet it touches four other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8785,17 +8905,17 @@
}
},
{
- "id": "fact-br-m49-region",
+ "id": "fact-br-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"br"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Brazil",
- "text": "The UN M49 standard groups Brazil in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Two landmark lines",
+ "text": "Both the Equator and the Tropic of Capricorn cross Brazil.",
"sourceIds": [
- "source-un-m49"
+ "source-ibge-brazil"
],
"asOf": null,
"review": {
@@ -8805,17 +8925,17 @@
}
},
{
- "id": "fact-bn-m49-region",
+ "id": "fact-bn-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bn"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Brunei",
- "text": "The UN M49 standard groups Brunei in South-eastern Asia.",
+ "heading": "A short border list",
+ "text": "Brunei has just one land neighbour: Malaysia.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8825,17 +8945,17 @@
}
},
{
- "id": "fact-bg-m49-region",
+ "id": "fact-bg-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bg"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Bulgaria",
- "text": "The UN M49 standard groups Bulgaria in Eastern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Bulgaria shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8845,17 +8965,17 @@
}
},
{
- "id": "fact-bf-m49-region",
+ "id": "fact-bf-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bf"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Burkina Faso",
- "text": "The UN M49 standard groups Burkina Faso in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Burkina Faso is landlocked, yet it touches six other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8865,17 +8985,17 @@
}
},
{
- "id": "fact-bi-m49-region",
+ "id": "fact-bi-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"bi"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Burundi",
- "text": "The UN M49 standard groups Burundi in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Burundi is landlocked, yet it touches three other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8885,17 +9005,17 @@
}
},
{
- "id": "fact-kh-m49-region",
+ "id": "fact-kh-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"kh"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Cambodia",
- "text": "The UN M49 standard groups Cambodia in South-eastern Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Cambodia shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8905,17 +9025,17 @@
}
},
{
- "id": "fact-cm-m49-region",
+ "id": "fact-cm-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cm"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Cameroon",
- "text": "The UN M49 standard groups Cameroon in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Cameroon shares land borders with six countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8925,17 +9045,17 @@
}
},
{
- "id": "fact-ca-m49-region",
+ "id": "fact-ca-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ca"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Canada",
- "text": "The UN M49 standard groups Canada in Northern America.",
+ "heading": "Three-ocean shores",
+ "text": "Canada's coastline reaches the Atlantic, Arctic and Pacific oceans.",
"sourceIds": [
- "source-un-m49"
+ "source-canada-coastlines"
],
"asOf": null,
"review": {
@@ -8945,17 +9065,17 @@
}
},
{
- "id": "fact-cv-m49-region",
+ "id": "fact-cv-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cv"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Cape Verde",
- "text": "The UN M49 standard groups Cape Verde in Sub-Saharan Africa.",
+ "heading": "No neighbours by land",
+ "text": "Cape Verde shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8965,17 +9085,17 @@
}
},
{
- "id": "fact-cf-m49-region",
+ "id": "fact-cf-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cf"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Central African Republic",
- "text": "The UN M49 standard groups Central African Republic in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Central African Republic is landlocked, yet it touches six other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -8985,17 +9105,17 @@
}
},
{
- "id": "fact-td-m49-region",
+ "id": "fact-td-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"td"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Chad",
- "text": "The UN M49 standard groups Chad in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Chad is landlocked, yet it touches six other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9005,17 +9125,17 @@
}
},
{
- "id": "fact-cl-m49-region",
+ "id": "fact-cl-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cl"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Chile",
- "text": "The UN M49 standard groups Chile in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Chile shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9025,17 +9145,17 @@
}
},
{
- "id": "fact-cn-m49-region",
+ "id": "fact-cn-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cn"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: China",
- "text": "The UN M49 standard groups China in Eastern Asia.",
+ "difficulty": "tricky",
+ "heading": "Many ways next door",
+ "text": "China shares land borders with 16 countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9045,17 +9165,17 @@
}
},
{
- "id": "fact-co-m49-region",
+ "id": "fact-co-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"co"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Colombia",
- "text": "The UN M49 standard groups Colombia in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Colombia shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9065,17 +9185,17 @@
}
},
{
- "id": "fact-km-m49-region",
+ "id": "fact-km-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"km"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Comoros",
- "text": "The UN M49 standard groups Comoros in Sub-Saharan Africa.",
+ "heading": "No neighbours by land",
+ "text": "Comoros shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9085,17 +9205,17 @@
}
},
{
- "id": "fact-cr-m49-region",
+ "id": "fact-cr-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cr"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Costa Rica",
- "text": "The UN M49 standard groups Costa Rica in Latin America and the Caribbean.",
+ "heading": "Across the border",
+ "text": "Costa Rica shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9105,17 +9225,17 @@
}
},
{
- "id": "fact-hr-m49-region",
+ "id": "fact-hr-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"hr"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Croatia",
- "text": "The UN M49 standard groups Croatia in Southern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Croatia shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9125,17 +9245,17 @@
}
},
{
- "id": "fact-cu-m49-region",
+ "id": "fact-cu-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cu"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Cuba",
- "text": "The UN M49 standard groups Cuba in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Cuba shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9145,17 +9265,17 @@
}
},
{
- "id": "fact-cy-m49-region",
+ "id": "fact-cy-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cy"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Cyprus",
- "text": "The UN M49 standard groups Cyprus in Western Asia.",
+ "heading": "No neighbours by land",
+ "text": "Cyprus shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9165,17 +9285,17 @@
}
},
{
- "id": "fact-cz-m49-region",
+ "id": "fact-cz-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cz"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Czechia",
- "text": "The UN M49 standard groups Czechia in Eastern Europe.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Czechia is landlocked, yet it touches four other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9185,17 +9305,17 @@
}
},
{
- "id": "fact-ci-m49-region",
+ "id": "fact-ci-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ci"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Côte d’Ivoire",
- "text": "The UN M49 standard groups Côte d’Ivoire in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Côte d’Ivoire shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9205,17 +9325,17 @@
}
},
{
- "id": "fact-cd-m49-region",
+ "id": "fact-cd-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cd"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Democratic Republic of the Congo",
- "text": "The UN M49 standard groups Democratic Republic of the Congo in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Many ways next door",
+ "text": "Democratic Republic of the Congo shares land borders with nine countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9225,17 +9345,17 @@
}
},
{
- "id": "fact-dk-m49-region",
+ "id": "fact-dk-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"dk"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Denmark",
- "text": "The UN M49 standard groups Denmark in Northern Europe.",
+ "heading": "A short border list",
+ "text": "Denmark has just one land neighbour: Germany.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9245,17 +9365,17 @@
}
},
{
- "id": "fact-dj-m49-region",
+ "id": "fact-dj-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"dj"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Djibouti",
- "text": "The UN M49 standard groups Djibouti in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Djibouti shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9265,17 +9385,17 @@
}
},
{
- "id": "fact-dm-m49-region",
+ "id": "fact-dm-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"dm"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Dominica",
- "text": "The UN M49 standard groups Dominica in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Dominica shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9285,17 +9405,17 @@
}
},
{
- "id": "fact-do-m49-region",
+ "id": "fact-do-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"do"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Dominican Republic",
- "text": "The UN M49 standard groups Dominican Republic in Latin America and the Caribbean.",
+ "heading": "A short border list",
+ "text": "Dominican Republic has just one land neighbour: Haiti.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9305,17 +9425,17 @@
}
},
{
- "id": "fact-ec-m49-region",
+ "id": "fact-ec-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ec"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Ecuador",
- "text": "The UN M49 standard groups Ecuador in Latin America and the Caribbean.",
+ "heading": "Across the border",
+ "text": "Ecuador shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9325,17 +9445,17 @@
}
},
{
- "id": "fact-eg-m49-region",
+ "id": "fact-eg-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"eg"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Egypt",
- "text": "The UN M49 standard groups Egypt in Northern Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Egypt shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9345,17 +9465,17 @@
}
},
{
- "id": "fact-sv-m49-region",
+ "id": "fact-sv-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sv"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: El Salvador",
- "text": "The UN M49 standard groups El Salvador in Latin America and the Caribbean.",
+ "heading": "Across the border",
+ "text": "El Salvador shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9365,17 +9485,17 @@
}
},
{
- "id": "fact-gq-m49-region",
+ "id": "fact-gq-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"gq"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Equatorial Guinea",
- "text": "The UN M49 standard groups Equatorial Guinea in Sub-Saharan Africa.",
+ "heading": "Across the border",
+ "text": "Equatorial Guinea shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9385,17 +9505,17 @@
}
},
{
- "id": "fact-er-m49-region",
+ "id": "fact-er-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"er"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Eritrea",
- "text": "The UN M49 standard groups Eritrea in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Eritrea shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9405,17 +9525,17 @@
}
},
{
- "id": "fact-ee-m49-region",
+ "id": "fact-ee-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ee"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Estonia",
- "text": "The UN M49 standard groups Estonia in Northern Europe.",
+ "heading": "Across the border",
+ "text": "Estonia shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9425,17 +9545,17 @@
}
},
{
- "id": "fact-sz-m49-region",
+ "id": "fact-sz-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sz"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Eswatini",
- "text": "The UN M49 standard groups Eswatini in Sub-Saharan Africa.",
+ "heading": "Landlocked, not isolated",
+ "text": "Eswatini is landlocked, yet it touches two other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9445,17 +9565,17 @@
}
},
{
- "id": "fact-et-m49-region",
+ "id": "fact-et-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"et"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Ethiopia",
- "text": "The UN M49 standard groups Ethiopia in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Ethiopia is landlocked, yet it touches six other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9465,17 +9585,17 @@
}
},
{
- "id": "fact-fj-m49-region",
+ "id": "fact-fj-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"fj"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Fiji",
- "text": "The UN M49 standard groups Fiji in Melanesia.",
+ "heading": "No neighbours by land",
+ "text": "Fiji shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9485,17 +9605,17 @@
}
},
{
- "id": "fact-fi-m49-region",
+ "id": "fact-fi-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"fi"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Finland",
- "text": "The UN M49 standard groups Finland in Northern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Finland shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9505,17 +9625,17 @@
}
},
{
- "id": "fact-fr-m49-region",
+ "id": "fact-fr-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"fr"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: France",
- "text": "The UN M49 standard groups France in Western Europe.",
+ "difficulty": "tricky",
+ "heading": "Many ways next door",
+ "text": "Counting its overseas territory in South America, France shares land borders with eight countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9525,17 +9645,17 @@
}
},
{
- "id": "fact-ga-m49-region",
+ "id": "fact-ga-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ga"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Gabon",
- "text": "The UN M49 standard groups Gabon in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Gabon shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9545,17 +9665,17 @@
}
},
{
- "id": "fact-gm-m49-region",
+ "id": "fact-gm-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"gm"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Gambia",
- "text": "The UN M49 standard groups Gambia in Sub-Saharan Africa.",
+ "heading": "A short border list",
+ "text": "Gambia has just one land neighbour: Senegal.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9565,17 +9685,17 @@
}
},
{
- "id": "fact-ge-m49-region",
+ "id": "fact-ge-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ge"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Georgia",
- "text": "The UN M49 standard groups Georgia in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Georgia shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9585,17 +9705,17 @@
}
},
{
- "id": "fact-de-m49-region",
+ "id": "fact-de-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"de"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Germany",
- "text": "The UN M49 standard groups Germany in Western Europe.",
+ "difficulty": "tricky",
+ "heading": "Many ways next door",
+ "text": "Germany shares land borders with nine countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9605,17 +9725,17 @@
}
},
{
- "id": "fact-gh-m49-region",
+ "id": "fact-gh-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"gh"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Ghana",
- "text": "The UN M49 standard groups Ghana in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Ghana shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9625,17 +9745,17 @@
}
},
{
- "id": "fact-gr-m49-region",
+ "id": "fact-gr-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"gr"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Greece",
- "text": "The UN M49 standard groups Greece in Southern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Greece shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9645,17 +9765,17 @@
}
},
{
- "id": "fact-gd-m49-region",
+ "id": "fact-gd-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"gd"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Grenada",
- "text": "The UN M49 standard groups Grenada in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Grenada shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9665,17 +9785,17 @@
}
},
{
- "id": "fact-gt-m49-region",
+ "id": "fact-gt-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"gt"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Guatemala",
- "text": "The UN M49 standard groups Guatemala in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Guatemala shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9685,17 +9805,17 @@
}
},
{
- "id": "fact-gn-m49-region",
+ "id": "fact-gn-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"gn"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Guinea",
- "text": "The UN M49 standard groups Guinea in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Guinea shares land borders with six countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9705,17 +9825,17 @@
}
},
{
- "id": "fact-gw-m49-region",
+ "id": "fact-gw-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"gw"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Guinea-Bissau",
- "text": "The UN M49 standard groups Guinea-Bissau in Sub-Saharan Africa.",
+ "heading": "Across the border",
+ "text": "Guinea-Bissau shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9725,17 +9845,17 @@
}
},
{
- "id": "fact-gy-m49-region",
+ "id": "fact-gy-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"gy"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Guyana",
- "text": "The UN M49 standard groups Guyana in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Guyana shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9745,17 +9865,17 @@
}
},
{
- "id": "fact-ht-m49-region",
+ "id": "fact-ht-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ht"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Haiti",
- "text": "The UN M49 standard groups Haiti in Latin America and the Caribbean.",
+ "heading": "A short border list",
+ "text": "Haiti has just one land neighbour: Dominican Republic.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9765,17 +9885,17 @@
}
},
{
- "id": "fact-hn-m49-region",
+ "id": "fact-hn-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"hn"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Honduras",
- "text": "The UN M49 standard groups Honduras in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Honduras shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9785,17 +9905,17 @@
}
},
{
- "id": "fact-hu-m49-region",
+ "id": "fact-hu-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"hu"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Hungary",
- "text": "The UN M49 standard groups Hungary in Eastern Europe.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Hungary is landlocked, yet it touches seven other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9805,17 +9925,17 @@
}
},
{
- "id": "fact-is-m49-region",
+ "id": "fact-is-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"is"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Iceland",
- "text": "The UN M49 standard groups Iceland in Northern Europe.",
+ "heading": "No neighbours by land",
+ "text": "Iceland shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9825,17 +9945,17 @@
}
},
{
- "id": "fact-in-m49-region",
+ "id": "fact-in-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"in"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: India",
- "text": "The UN M49 standard groups India in Southern Asia.",
+ "difficulty": "tricky",
+ "heading": "Post from the lake",
+ "text": "A floating post office on Srinagar's Dal Lake even has a small philately museum.",
"sourceIds": [
- "source-un-m49"
+ "source-incredible-india-dal-lake"
],
"asOf": null,
"review": {
@@ -9845,17 +9965,17 @@
}
},
{
- "id": "fact-id-m49-region",
+ "id": "fact-id-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"id"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Indonesia",
- "text": "The UN M49 standard groups Indonesia in South-eastern Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Indonesia shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9865,17 +9985,17 @@
}
},
{
- "id": "fact-ir-m49-region",
+ "id": "fact-ir-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ir"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Iran",
- "text": "The UN M49 standard groups Iran in Southern Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Iran shares land borders with seven countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9885,17 +10005,17 @@
}
},
{
- "id": "fact-iq-m49-region",
+ "id": "fact-iq-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"iq"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Iraq",
- "text": "The UN M49 standard groups Iraq in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Iraq shares land borders with six countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9905,17 +10025,17 @@
}
},
{
- "id": "fact-ie-m49-region",
+ "id": "fact-ie-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ie"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Ireland",
- "text": "The UN M49 standard groups Ireland in Northern Europe.",
+ "heading": "A short border list",
+ "text": "Ireland has just one land neighbour: United Kingdom.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9925,17 +10045,17 @@
}
},
{
- "id": "fact-il-m49-region",
+ "id": "fact-il-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"il"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Israel",
- "text": "The UN M49 standard groups Israel in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Israel shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9945,17 +10065,17 @@
}
},
{
- "id": "fact-it-m49-region",
+ "id": "fact-it-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"it"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Italy",
- "text": "The UN M49 standard groups Italy in Southern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Italy shares land borders with six countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9965,17 +10085,17 @@
}
},
{
- "id": "fact-jm-m49-region",
+ "id": "fact-jm-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"jm"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Jamaica",
- "text": "The UN M49 standard groups Jamaica in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Jamaica shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -9985,19 +10105,19 @@
}
},
{
- "id": "fact-jp-m49-region",
+ "id": "fact-jp-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"jp"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Japan",
- "text": "The UN M49 standard groups Japan in Eastern Asia.",
+ "heading": "An island recount",
+ "text": "Using its 2022 digital maps, Japan's mapping authority counted 14,125 islands.",
"sourceIds": [
- "source-un-m49"
+ "source-gsi-japan-islands"
],
- "asOf": null,
+ "asOf": "2022-01",
"review": {
"state": "draft",
"reviewer": null,
@@ -10005,17 +10125,17 @@
}
},
{
- "id": "fact-jo-m49-region",
+ "id": "fact-jo-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"jo"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Jordan",
- "text": "The UN M49 standard groups Jordan in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Jordan shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10025,17 +10145,17 @@
}
},
{
- "id": "fact-kz-m49-region",
+ "id": "fact-kz-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"kz"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Kazakhstan",
- "text": "The UN M49 standard groups Kazakhstan in Central Asia.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Kazakhstan is landlocked, yet it touches five other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10045,17 +10165,17 @@
}
},
{
- "id": "fact-ke-m49-region",
+ "id": "fact-ke-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ke"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Kenya",
- "text": "The UN M49 standard groups Kenya in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Kenya shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10065,17 +10185,17 @@
}
},
{
- "id": "fact-ki-m49-region",
+ "id": "fact-ki-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ki"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Kiribati",
- "text": "The UN M49 standard groups Kiribati in Micronesia.",
+ "heading": "No neighbours by land",
+ "text": "Kiribati shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10085,17 +10205,17 @@
}
},
{
- "id": "fact-kw-m49-region",
+ "id": "fact-kw-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"kw"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Kuwait",
- "text": "The UN M49 standard groups Kuwait in Western Asia.",
+ "heading": "Across the border",
+ "text": "Kuwait shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10105,17 +10225,17 @@
}
},
{
- "id": "fact-kg-m49-region",
+ "id": "fact-kg-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"kg"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Kyrgyzstan",
- "text": "The UN M49 standard groups Kyrgyzstan in Central Asia.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Kyrgyzstan is landlocked, yet it touches four other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10125,17 +10245,17 @@
}
},
{
- "id": "fact-la-m49-region",
+ "id": "fact-la-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"la"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Laos",
- "text": "The UN M49 standard groups Laos in South-eastern Asia.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Laos is landlocked, yet it touches five other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10145,17 +10265,17 @@
}
},
{
- "id": "fact-lv-m49-region",
+ "id": "fact-lv-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"lv"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Latvia",
- "text": "The UN M49 standard groups Latvia in Northern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Latvia shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10165,17 +10285,17 @@
}
},
{
- "id": "fact-lb-m49-region",
+ "id": "fact-lb-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"lb"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Lebanon",
- "text": "The UN M49 standard groups Lebanon in Western Asia.",
+ "heading": "Across the border",
+ "text": "Lebanon shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10185,17 +10305,17 @@
}
},
{
- "id": "fact-ls-m49-region",
+ "id": "fact-ls-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ls"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Lesotho",
- "text": "The UN M49 standard groups Lesotho in Sub-Saharan Africa.",
+ "heading": "A short border list",
+ "text": "Lesotho has just one land neighbour: South Africa.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10205,17 +10325,17 @@
}
},
{
- "id": "fact-lr-m49-region",
+ "id": "fact-lr-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"lr"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Liberia",
- "text": "The UN M49 standard groups Liberia in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Liberia shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10225,17 +10345,17 @@
}
},
{
- "id": "fact-ly-m49-region",
+ "id": "fact-ly-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ly"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Libya",
- "text": "The UN M49 standard groups Libya in Northern Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Libya shares land borders with six countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10245,17 +10365,17 @@
}
},
{
- "id": "fact-li-m49-region",
+ "id": "fact-li-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"li"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Liechtenstein",
- "text": "The UN M49 standard groups Liechtenstein in Western Europe.",
+ "heading": "Landlocked, not isolated",
+ "text": "Liechtenstein is landlocked, yet it touches two other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10265,17 +10385,17 @@
}
},
{
- "id": "fact-lt-m49-region",
+ "id": "fact-lt-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"lt"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Lithuania",
- "text": "The UN M49 standard groups Lithuania in Northern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Lithuania shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10285,17 +10405,17 @@
}
},
{
- "id": "fact-lu-m49-region",
+ "id": "fact-lu-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"lu"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Luxembourg",
- "text": "The UN M49 standard groups Luxembourg in Western Europe.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Luxembourg is landlocked, yet it touches three other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10305,17 +10425,17 @@
}
},
{
- "id": "fact-mg-m49-region",
+ "id": "fact-mg-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mg"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Madagascar",
- "text": "The UN M49 standard groups Madagascar in Sub-Saharan Africa.",
+ "heading": "No neighbours by land",
+ "text": "Madagascar shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10325,17 +10445,17 @@
}
},
{
- "id": "fact-mw-m49-region",
+ "id": "fact-mw-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mw"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Malawi",
- "text": "The UN M49 standard groups Malawi in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Malawi is landlocked, yet it touches three other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10345,17 +10465,17 @@
}
},
{
- "id": "fact-my-m49-region",
+ "id": "fact-my-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"my"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Malaysia",
- "text": "The UN M49 standard groups Malaysia in South-eastern Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Malaysia shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10365,17 +10485,17 @@
}
},
{
- "id": "fact-mv-m49-region",
+ "id": "fact-mv-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mv"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Maldives",
- "text": "The UN M49 standard groups Maldives in Southern Asia.",
+ "heading": "No neighbours by land",
+ "text": "Maldives shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10385,17 +10505,17 @@
}
},
{
- "id": "fact-ml-m49-region",
+ "id": "fact-ml-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ml"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Mali",
- "text": "The UN M49 standard groups Mali in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Mali is landlocked, yet it touches seven other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10405,17 +10525,17 @@
}
},
{
- "id": "fact-mt-m49-region",
+ "id": "fact-mt-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mt"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Malta",
- "text": "The UN M49 standard groups Malta in Southern Europe.",
+ "heading": "No neighbours by land",
+ "text": "Malta shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10425,17 +10545,17 @@
}
},
{
- "id": "fact-mh-m49-region",
+ "id": "fact-mh-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mh"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Marshall Islands",
- "text": "The UN M49 standard groups Marshall Islands in Micronesia.",
+ "heading": "No neighbours by land",
+ "text": "Marshall Islands shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10445,17 +10565,17 @@
}
},
{
- "id": "fact-mr-m49-region",
+ "id": "fact-mr-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mr"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Mauritania",
- "text": "The UN M49 standard groups Mauritania in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Mauritania shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10465,17 +10585,17 @@
}
},
{
- "id": "fact-mu-m49-region",
+ "id": "fact-mu-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mu"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Mauritius",
- "text": "The UN M49 standard groups Mauritius in Sub-Saharan Africa.",
+ "heading": "No neighbours by land",
+ "text": "Mauritius shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10485,17 +10605,17 @@
}
},
{
- "id": "fact-mx-m49-region",
+ "id": "fact-mx-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mx"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Mexico",
- "text": "The UN M49 standard groups Mexico in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "An origin story on a flag",
+ "text": "The eagle, cactus and snake on Mexico's flag recall the founding of Tenochtitlan.",
"sourceIds": [
- "source-un-m49"
+ "source-mexico-flag-history"
],
"asOf": null,
"review": {
@@ -10505,17 +10625,17 @@
}
},
{
- "id": "fact-fm-m49-region",
+ "id": "fact-fm-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"fm"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Micronesia",
- "text": "The UN M49 standard groups Micronesia in Micronesia.",
+ "heading": "No neighbours by land",
+ "text": "Micronesia shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10525,17 +10645,17 @@
}
},
{
- "id": "fact-md-m49-region",
+ "id": "fact-md-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"md"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Moldova",
- "text": "The UN M49 standard groups Moldova in Eastern Europe.",
+ "heading": "Landlocked, not isolated",
+ "text": "Moldova is landlocked, yet it touches two other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10545,19 +10665,19 @@
}
},
{
- "id": "fact-mc-m49-region",
+ "id": "fact-mc-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mc"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Monaco",
- "text": "The UN M49 standard groups Monaco in Western Europe.",
+ "heading": "Centuries in two colours",
+ "text": "Monaco's red-and-white flag colours have been associated with its ruling house since 1339.",
"sourceIds": [
- "source-un-m49"
+ "source-cia-monaco-flag-archive"
],
- "asOf": null,
+ "asOf": "1339",
"review": {
"state": "draft",
"reviewer": null,
@@ -10565,17 +10685,17 @@
}
},
{
- "id": "fact-mn-m49-region",
+ "id": "fact-mn-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mn"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Mongolia",
- "text": "The UN M49 standard groups Mongolia in Eastern Asia.",
+ "heading": "Landlocked, not isolated",
+ "text": "Mongolia is landlocked, yet it touches two other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10585,17 +10705,17 @@
}
},
{
- "id": "fact-me-m49-region",
+ "id": "fact-me-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"me"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Montenegro",
- "text": "The UN M49 standard groups Montenegro in Southern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Montenegro shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10605,17 +10725,17 @@
}
},
{
- "id": "fact-ma-m49-region",
+ "id": "fact-ma-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ma"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Morocco",
- "text": "The UN M49 standard groups Morocco in Northern Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Morocco shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10625,17 +10745,17 @@
}
},
{
- "id": "fact-mz-m49-region",
+ "id": "fact-mz-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mz"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Mozambique",
- "text": "The UN M49 standard groups Mozambique in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Mozambique shares land borders with six countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10645,17 +10765,17 @@
}
},
{
- "id": "fact-mm-m49-region",
+ "id": "fact-mm-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mm"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Myanmar",
- "text": "The UN M49 standard groups Myanmar in South-eastern Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Myanmar shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10665,17 +10785,17 @@
}
},
{
- "id": "fact-na-m49-region",
+ "id": "fact-na-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"na"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Namibia",
- "text": "The UN M49 standard groups Namibia in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Namibia shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10685,17 +10805,17 @@
}
},
{
- "id": "fact-nr-m49-region",
+ "id": "fact-nr-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"nr"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Nauru",
- "text": "The UN M49 standard groups Nauru in Micronesia.",
+ "heading": "No neighbours by land",
+ "text": "Nauru shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10705,17 +10825,17 @@
}
},
{
- "id": "fact-np-m49-region",
+ "id": "fact-np-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"np"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Nepal",
- "text": "The UN M49 standard groups Nepal in Southern Asia.",
+ "heading": "A flag with points",
+ "text": "Nepal has the world's only national flag that is not quadrilateral: it is formed from two joined pennants.",
"sourceIds": [
- "source-un-m49"
+ "source-nepal-film-guide"
],
"asOf": null,
"review": {
@@ -10725,19 +10845,19 @@
}
},
{
- "id": "fact-nl-m49-region",
+ "id": "fact-nl-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"nl"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Netherlands",
- "text": "The UN M49 standard groups Netherlands in Western Europe.",
+ "heading": "Living below the waves",
+ "text": "About 17 percent of the Netherlands lies below sea level.",
"sourceIds": [
- "source-un-m49"
+ "source-netherlands-govtech"
],
- "asOf": null,
+ "asOf": "2021-06",
"review": {
"state": "draft",
"reviewer": null,
@@ -10745,19 +10865,19 @@
}
},
{
- "id": "fact-nz-m49-region",
+ "id": "fact-nz-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"nz"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: New Zealand",
- "text": "The UN M49 standard groups New Zealand in Australia and New Zealand.",
+ "heading": "A voting first",
+ "text": "In 1893, New Zealand became the first self-governing country where women could vote in parliamentary elections.",
"sourceIds": [
- "source-un-m49"
+ "source-nz-womens-suffrage"
],
- "asOf": null,
+ "asOf": "1893",
"review": {
"state": "draft",
"reviewer": null,
@@ -10765,17 +10885,17 @@
}
},
{
- "id": "fact-ni-m49-region",
+ "id": "fact-ni-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ni"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Nicaragua",
- "text": "The UN M49 standard groups Nicaragua in Latin America and the Caribbean.",
+ "heading": "Across the border",
+ "text": "Nicaragua shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10785,17 +10905,17 @@
}
},
{
- "id": "fact-ne-m49-region",
+ "id": "fact-ne-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ne"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Niger",
- "text": "The UN M49 standard groups Niger in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Niger is landlocked, yet it touches seven other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10805,17 +10925,17 @@
}
},
{
- "id": "fact-ng-m49-region",
+ "id": "fact-ng-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ng"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Nigeria",
- "text": "The UN M49 standard groups Nigeria in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Nigeria shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10825,17 +10945,17 @@
}
},
{
- "id": "fact-kp-m49-region",
+ "id": "fact-kp-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"kp"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: North Korea",
- "text": "The UN M49 standard groups North Korea in Eastern Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "North Korea shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10845,17 +10965,17 @@
}
},
{
- "id": "fact-mk-m49-region",
+ "id": "fact-mk-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"mk"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: North Macedonia",
- "text": "The UN M49 standard groups North Macedonia in Southern Europe.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "North Macedonia is landlocked, yet it touches five other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10865,17 +10985,17 @@
}
},
{
- "id": "fact-no-m49-region",
+ "id": "fact-no-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"no"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Norway",
- "text": "The UN M49 standard groups Norway in Northern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Norway shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10885,17 +11005,17 @@
}
},
{
- "id": "fact-om-m49-region",
+ "id": "fact-om-musandam",
"schemaVersion": 1,
"countryIds": [
"om"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Oman",
- "text": "The UN M49 standard groups Oman in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "A peninsula apart",
+ "text": "Oman's Musandam Peninsula is separated from the rest of the country by the United Arab Emirates.",
"sourceIds": [
- "source-un-m49"
+ "source-experience-oman-musandam"
],
"asOf": null,
"review": {
@@ -10905,17 +11025,17 @@
}
},
{
- "id": "fact-pk-m49-region",
+ "id": "fact-pk-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"pk"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Pakistan",
- "text": "The UN M49 standard groups Pakistan in Southern Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Pakistan shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10925,17 +11045,17 @@
}
},
{
- "id": "fact-pw-m49-region",
+ "id": "fact-pw-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"pw"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Palau",
- "text": "The UN M49 standard groups Palau in Micronesia.",
+ "heading": "No neighbours by land",
+ "text": "Palau shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10945,17 +11065,17 @@
}
},
{
- "id": "fact-ps-m49-region",
+ "id": "fact-ps-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ps"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Palestine",
- "text": "The UN M49 standard groups Palestine in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Palestine shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10965,17 +11085,17 @@
}
},
{
- "id": "fact-pa-m49-region",
+ "id": "fact-pa-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"pa"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Panama",
- "text": "The UN M49 standard groups Panama in Latin America and the Caribbean.",
+ "heading": "Across the border",
+ "text": "Panama shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -10985,17 +11105,17 @@
}
},
{
- "id": "fact-pg-m49-region",
+ "id": "fact-pg-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"pg"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Papua New Guinea",
- "text": "The UN M49 standard groups Papua New Guinea in Melanesia.",
+ "heading": "A short border list",
+ "text": "Papua New Guinea has just one land neighbour: Indonesia.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11005,17 +11125,17 @@
}
},
{
- "id": "fact-py-m49-region",
+ "id": "fact-py-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"py"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Paraguay",
- "text": "The UN M49 standard groups Paraguay in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Paraguay is landlocked, yet it touches three other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11025,17 +11145,17 @@
}
},
{
- "id": "fact-pe-m49-region",
+ "id": "fact-pe-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"pe"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Peru",
- "text": "The UN M49 standard groups Peru in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Peru shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11045,17 +11165,17 @@
}
},
{
- "id": "fact-ph-m49-region",
+ "id": "fact-ph-islands",
"schemaVersion": 1,
"countryIds": [
"ph"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Philippines",
- "text": "The UN M49 standard groups Philippines in South-eastern Asia.",
+ "heading": "Thousands of islands",
+ "text": "The Philippines has 7,641 islands. enough to visit a different one every day for almost 21 years.",
"sourceIds": [
- "source-un-m49"
+ "source-philippines-tourism-islands"
],
"asOf": null,
"review": {
@@ -11065,17 +11185,17 @@
}
},
{
- "id": "fact-pl-m49-region",
+ "id": "fact-pl-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"pl"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Poland",
- "text": "The UN M49 standard groups Poland in Eastern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Poland shares land borders with seven countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11085,17 +11205,17 @@
}
},
{
- "id": "fact-pt-m49-region",
+ "id": "fact-pt-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"pt"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Portugal",
- "text": "The UN M49 standard groups Portugal in Southern Europe.",
+ "heading": "A short border list",
+ "text": "Portugal has just one land neighbour: Spain.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11105,17 +11225,17 @@
}
},
{
- "id": "fact-qa-m49-region",
+ "id": "fact-qa-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"qa"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Qatar",
- "text": "The UN M49 standard groups Qatar in Western Asia.",
+ "heading": "A short border list",
+ "text": "Qatar has just one land neighbour: Saudi Arabia.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11125,17 +11245,17 @@
}
},
{
- "id": "fact-cg-m49-region",
+ "id": "fact-cg-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"cg"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Republic of the Congo",
- "text": "The UN M49 standard groups Republic of the Congo in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Republic of the Congo shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11145,17 +11265,17 @@
}
},
{
- "id": "fact-ro-m49-region",
+ "id": "fact-ro-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ro"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Romania",
- "text": "The UN M49 standard groups Romania in Eastern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Romania shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11165,17 +11285,17 @@
}
},
{
- "id": "fact-ru-m49-region",
+ "id": "fact-ru-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ru"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Russia",
- "text": "The UN M49 standard groups Russia in Eastern Europe.",
+ "difficulty": "tricky",
+ "heading": "Many ways next door",
+ "text": "Russia shares land borders with fourteen countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11185,17 +11305,17 @@
}
},
{
- "id": "fact-rw-m49-region",
+ "id": "fact-rw-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"rw"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Rwanda",
- "text": "The UN M49 standard groups Rwanda in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Rwanda is landlocked, yet it touches four other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11205,17 +11325,17 @@
}
},
{
- "id": "fact-kn-m49-region",
+ "id": "fact-kn-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"kn"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Saint Kitts and Nevis",
- "text": "The UN M49 standard groups Saint Kitts and Nevis in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Saint Kitts and Nevis shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11225,17 +11345,17 @@
}
},
{
- "id": "fact-lc-m49-region",
+ "id": "fact-lc-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"lc"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Saint Lucia",
- "text": "The UN M49 standard groups Saint Lucia in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Saint Lucia shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11245,17 +11365,17 @@
}
},
{
- "id": "fact-vc-m49-region",
+ "id": "fact-vc-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"vc"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Saint Vincent and the Grenadines",
- "text": "The UN M49 standard groups Saint Vincent and the Grenadines in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Saint Vincent and the Grenadines shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11265,17 +11385,17 @@
}
},
{
- "id": "fact-ws-m49-region",
+ "id": "fact-ws-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ws"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Samoa",
- "text": "The UN M49 standard groups Samoa in Polynesia.",
+ "heading": "No neighbours by land",
+ "text": "Samoa shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11285,17 +11405,17 @@
}
},
{
- "id": "fact-sm-m49-region",
+ "id": "fact-sm-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sm"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: San Marino",
- "text": "The UN M49 standard groups San Marino in Southern Europe.",
+ "heading": "A short border list",
+ "text": "San Marino has just one land neighbour: Italy.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11305,17 +11425,17 @@
}
},
{
- "id": "fact-sa-m49-region",
+ "id": "fact-sa-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sa"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Saudi Arabia",
- "text": "The UN M49 standard groups Saudi Arabia in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Saudi Arabia shares land borders with seven countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11325,17 +11445,17 @@
}
},
{
- "id": "fact-sn-m49-region",
+ "id": "fact-sn-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sn"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Senegal",
- "text": "The UN M49 standard groups Senegal in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Senegal shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11345,17 +11465,17 @@
}
},
{
- "id": "fact-rs-m49-region",
+ "id": "fact-rs-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"rs"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Serbia",
- "text": "The UN M49 standard groups Serbia in Southern Europe.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Serbia is landlocked, yet it touches eight other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11365,17 +11485,17 @@
}
},
{
- "id": "fact-sc-m49-region",
+ "id": "fact-sc-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sc"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Seychelles",
- "text": "The UN M49 standard groups Seychelles in Sub-Saharan Africa.",
+ "heading": "No neighbours by land",
+ "text": "Seychelles shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11385,17 +11505,17 @@
}
},
{
- "id": "fact-sl-m49-region",
+ "id": "fact-sl-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sl"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Sierra Leone",
- "text": "The UN M49 standard groups Sierra Leone in Sub-Saharan Africa.",
+ "heading": "Across the border",
+ "text": "Sierra Leone shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11405,17 +11525,17 @@
}
},
{
- "id": "fact-sg-m49-region",
+ "id": "fact-sg-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sg"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Singapore",
- "text": "The UN M49 standard groups Singapore in South-eastern Asia.",
+ "heading": "No neighbours by land",
+ "text": "Singapore shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11425,17 +11545,17 @@
}
},
{
- "id": "fact-sk-m49-region",
+ "id": "fact-sk-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sk"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Slovakia",
- "text": "The UN M49 standard groups Slovakia in Eastern Europe.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Slovakia is landlocked, yet it touches five other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11445,17 +11565,17 @@
}
},
{
- "id": "fact-si-m49-region",
+ "id": "fact-si-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"si"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Slovenia",
- "text": "The UN M49 standard groups Slovenia in Southern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Slovenia shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11465,17 +11585,17 @@
}
},
{
- "id": "fact-sb-m49-region",
+ "id": "fact-sb-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sb"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Solomon Islands",
- "text": "The UN M49 standard groups Solomon Islands in Melanesia.",
+ "heading": "No neighbours by land",
+ "text": "Solomon Islands shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11485,17 +11605,17 @@
}
},
{
- "id": "fact-so-m49-region",
+ "id": "fact-so-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"so"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Somalia",
- "text": "The UN M49 standard groups Somalia in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Somalia shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11505,17 +11625,17 @@
}
},
{
- "id": "fact-za-m49-region",
+ "id": "fact-za-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"za"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: South Africa",
- "text": "The UN M49 standard groups South Africa in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Three capital cities",
+ "text": "South Africa divides national capital duties among Pretoria, Cape Town and Bloemfontein.",
"sourceIds": [
- "source-un-m49"
+ "source-south-africa-provinces"
],
"asOf": null,
"review": {
@@ -11525,17 +11645,17 @@
}
},
{
- "id": "fact-kr-m49-region",
+ "id": "fact-kr-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"kr"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: South Korea",
- "text": "The UN M49 standard groups South Korea in Eastern Asia.",
+ "heading": "A short border list",
+ "text": "South Korea has just one land neighbour: North Korea.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11545,17 +11665,17 @@
}
},
{
- "id": "fact-ss-m49-region",
+ "id": "fact-ss-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ss"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: South Sudan",
- "text": "The UN M49 standard groups South Sudan in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "South Sudan is landlocked, yet it touches six other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11565,17 +11685,17 @@
}
},
{
- "id": "fact-es-m49-region",
+ "id": "fact-es-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"es"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Spain",
- "text": "The UN M49 standard groups Spain in Southern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Spain shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11585,17 +11705,17 @@
}
},
{
- "id": "fact-lk-m49-region",
+ "id": "fact-lk-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"lk"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Sri Lanka",
- "text": "The UN M49 standard groups Sri Lanka in Southern Asia.",
+ "heading": "No neighbours by land",
+ "text": "Sri Lanka shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11605,17 +11725,17 @@
}
},
{
- "id": "fact-sd-m49-region",
+ "id": "fact-sd-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sd"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Sudan",
- "text": "The UN M49 standard groups Sudan in Northern Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Sudan shares land borders with seven countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11625,17 +11745,17 @@
}
},
{
- "id": "fact-sr-m49-region",
+ "id": "fact-sr-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sr"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Suriname",
- "text": "The UN M49 standard groups Suriname in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Suriname shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11645,17 +11765,17 @@
}
},
{
- "id": "fact-se-m49-region",
+ "id": "fact-se-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"se"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Sweden",
- "text": "The UN M49 standard groups Sweden in Northern Europe.",
+ "heading": "Across the border",
+ "text": "Sweden shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11665,17 +11785,17 @@
}
},
{
- "id": "fact-ch-m49-region",
+ "id": "fact-ch-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ch"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Switzerland",
- "text": "The UN M49 standard groups Switzerland in Western Europe.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Switzerland is landlocked, yet it touches five other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11685,17 +11805,17 @@
}
},
{
- "id": "fact-sy-m49-region",
+ "id": "fact-sy-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"sy"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Syria",
- "text": "The UN M49 standard groups Syria in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Syria shares land borders with five countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11705,17 +11825,17 @@
}
},
{
- "id": "fact-st-m49-region",
+ "id": "fact-st-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"st"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: São Tomé and Príncipe",
- "text": "The UN M49 standard groups São Tomé and Príncipe in Sub-Saharan Africa.",
+ "heading": "No neighbours by land",
+ "text": "São Tomé and Príncipe shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11725,17 +11845,17 @@
}
},
{
- "id": "fact-tj-m49-region",
+ "id": "fact-tj-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"tj"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Tajikistan",
- "text": "The UN M49 standard groups Tajikistan in Central Asia.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Tajikistan is landlocked, yet it touches four other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11745,17 +11865,17 @@
}
},
{
- "id": "fact-tz-m49-region",
+ "id": "fact-tz-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"tz"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Tanzania",
- "text": "The UN M49 standard groups Tanzania in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Many ways next door",
+ "text": "Tanzania shares land borders with eight countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11765,17 +11885,17 @@
}
},
{
- "id": "fact-th-m49-region",
+ "id": "fact-th-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"th"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Thailand",
- "text": "The UN M49 standard groups Thailand in South-eastern Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Thailand shares land borders with four countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11785,17 +11905,17 @@
}
},
{
- "id": "fact-tl-m49-region",
+ "id": "fact-tl-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"tl"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Timor-Leste",
- "text": "The UN M49 standard groups Timor-Leste in South-eastern Asia.",
+ "heading": "A short border list",
+ "text": "Timor-Leste has just one land neighbour: Indonesia.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11805,17 +11925,17 @@
}
},
{
- "id": "fact-tg-m49-region",
+ "id": "fact-tg-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"tg"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Togo",
- "text": "The UN M49 standard groups Togo in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Togo shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11825,17 +11945,17 @@
}
},
{
- "id": "fact-to-m49-region",
+ "id": "fact-to-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"to"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Tonga",
- "text": "The UN M49 standard groups Tonga in Polynesia.",
+ "heading": "No neighbours by land",
+ "text": "Tonga shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11845,17 +11965,17 @@
}
},
{
- "id": "fact-tt-m49-region",
+ "id": "fact-tt-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"tt"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Trinidad and Tobago",
- "text": "The UN M49 standard groups Trinidad and Tobago in Latin America and the Caribbean.",
+ "heading": "No neighbours by land",
+ "text": "Trinidad and Tobago shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11865,17 +11985,17 @@
}
},
{
- "id": "fact-tn-m49-region",
+ "id": "fact-tn-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"tn"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Tunisia",
- "text": "The UN M49 standard groups Tunisia in Northern Africa.",
+ "heading": "Across the border",
+ "text": "Tunisia shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11885,17 +12005,17 @@
}
},
{
- "id": "fact-tm-m49-region",
+ "id": "fact-tm-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"tm"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Turkmenistan",
- "text": "The UN M49 standard groups Turkmenistan in Central Asia.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Turkmenistan is landlocked, yet it touches four other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11905,17 +12025,17 @@
}
},
{
- "id": "fact-tv-m49-region",
+ "id": "fact-tv-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"tv"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Tuvalu",
- "text": "The UN M49 standard groups Tuvalu in Polynesia.",
+ "heading": "No neighbours by land",
+ "text": "Tuvalu shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11925,17 +12045,17 @@
}
},
{
- "id": "fact-tr-m49-region",
+ "id": "fact-tr-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"tr"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Türkiye",
- "text": "The UN M49 standard groups Türkiye in Western Asia.",
+ "difficulty": "tricky",
+ "heading": "Many ways next door",
+ "text": "Türkiye shares land borders with eight countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11945,17 +12065,17 @@
}
},
{
- "id": "fact-ug-m49-region",
+ "id": "fact-ug-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ug"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Uganda",
- "text": "The UN M49 standard groups Uganda in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Uganda is landlocked, yet it touches five other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11965,17 +12085,17 @@
}
},
{
- "id": "fact-ua-m49-region",
+ "id": "fact-ua-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ua"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Ukraine",
- "text": "The UN M49 standard groups Ukraine in Eastern Europe.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Ukraine shares land borders with seven countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -11985,17 +12105,17 @@
}
},
{
- "id": "fact-ae-m49-region",
+ "id": "fact-ae-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ae"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: United Arab Emirates",
- "text": "The UN M49 standard groups United Arab Emirates in Western Asia.",
+ "heading": "Across the border",
+ "text": "United Arab Emirates shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12005,17 +12125,17 @@
}
},
{
- "id": "fact-gb-m49-region",
+ "id": "fact-gb-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"gb"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: United Kingdom",
- "text": "The UN M49 standard groups United Kingdom in Northern Europe.",
+ "heading": "Where east meets west",
+ "text": "The historic Prime Meridian at Greenwich marks zero degrees longitude.",
"sourceIds": [
- "source-un-m49"
+ "source-rmg-greenwich"
],
"asOf": null,
"review": {
@@ -12025,17 +12145,17 @@
}
},
{
- "id": "fact-us-m49-region",
+ "id": "fact-us-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"us"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: United States",
- "text": "The UN M49 standard groups United States in Northern America.",
+ "heading": "Across the border",
+ "text": "United States shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12045,17 +12165,17 @@
}
},
{
- "id": "fact-uy-m49-region",
+ "id": "fact-uy-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"uy"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Uruguay",
- "text": "The UN M49 standard groups Uruguay in Latin America and the Caribbean.",
+ "heading": "Across the border",
+ "text": "Uruguay shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12065,17 +12185,17 @@
}
},
{
- "id": "fact-uz-m49-region",
+ "id": "fact-uz-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"uz"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Uzbekistan",
- "text": "The UN M49 standard groups Uzbekistan in Central Asia.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Uzbekistan is landlocked, yet it touches five other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12085,17 +12205,17 @@
}
},
{
- "id": "fact-vu-m49-region",
+ "id": "fact-vu-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"vu"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Vanuatu",
- "text": "The UN M49 standard groups Vanuatu in Melanesia.",
+ "heading": "No neighbours by land",
+ "text": "Vanuatu shares no land border with another country.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12105,17 +12225,17 @@
}
},
{
- "id": "fact-va-m49-region",
+ "id": "fact-va-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"va"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Vatican City",
- "text": "The UN M49 standard groups Vatican City in Southern Europe.",
+ "heading": "A short border list",
+ "text": "Vatican City has just one land neighbour: Italy.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12125,17 +12245,17 @@
}
},
{
- "id": "fact-ve-m49-region",
+ "id": "fact-ve-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ve"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Venezuela",
- "text": "The UN M49 standard groups Venezuela in Latin America and the Caribbean.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Venezuela shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12145,17 +12265,17 @@
}
},
{
- "id": "fact-vn-m49-region",
+ "id": "fact-vn-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"vn"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Vietnam",
- "text": "The UN M49 standard groups Vietnam in South-eastern Asia.",
+ "difficulty": "tricky",
+ "heading": "Across the border",
+ "text": "Vietnam shares land borders with three countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12165,17 +12285,17 @@
}
},
{
- "id": "fact-ye-m49-region",
+ "id": "fact-ye-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"ye"
],
"topic": "mixed_geography",
"difficulty": "casual",
- "heading": "Atlas placement: Yemen",
- "text": "The UN M49 standard groups Yemen in Western Asia.",
+ "heading": "Across the border",
+ "text": "Yemen shares land borders with two countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12185,17 +12305,17 @@
}
},
{
- "id": "fact-zm-m49-region",
+ "id": "fact-zm-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"zm"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Zambia",
- "text": "The UN M49 standard groups Zambia in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Zambia is landlocked, yet it touches eight other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12205,17 +12325,17 @@
}
},
{
- "id": "fact-zw-m49-region",
+ "id": "fact-zw-land-neighbours",
"schemaVersion": 1,
"countryIds": [
"zw"
],
"topic": "mixed_geography",
- "difficulty": "casual",
- "heading": "Atlas placement: Zimbabwe",
- "text": "The UN M49 standard groups Zimbabwe in Sub-Saharan Africa.",
+ "difficulty": "tricky",
+ "heading": "Landlocked, not isolated",
+ "text": "Zimbabwe is landlocked, yet it touches four other countries.",
"sourceIds": [
- "source-un-m49"
+ "source-rest-countries"
],
"asOf": null,
"review": {
@@ -12249,8 +12369,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Algiers is the capital of Algeria.",
"review": {
@@ -12282,8 +12401,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Kabul is the capital of Afghanistan.",
"review": {
@@ -12315,8 +12433,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Tirana is the capital of Albania.",
"review": {
@@ -12348,8 +12465,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "St. John's is the capital of Antigua and Barbuda.",
"review": {
@@ -12381,8 +12497,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Buenos Aires is the capital of Argentina.",
"review": {
@@ -12414,8 +12529,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Canberra is the capital of Australia.",
"review": {
@@ -12447,8 +12561,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Luanda is the capital of Angola.",
"review": {
@@ -12480,8 +12593,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Yerevan is the capital of Armenia.",
"review": {
@@ -12513,8 +12625,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Andorra la Vella is the capital of Andorra.",
"review": {
@@ -12546,8 +12657,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Nassau is the capital of Bahamas.",
"review": {
@@ -12579,8 +12689,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Brasilia is the capital of Brazil.",
"review": {
@@ -12612,8 +12721,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Suva is the capital of Fiji.",
"review": {
@@ -12645,8 +12753,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Gaborone is the capital of Botswana.",
"review": {
@@ -12678,8 +12785,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Baku is the capital of Azerbaijan.",
"review": {
@@ -12711,8 +12817,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Vienna is the capital of Austria.",
"review": {
@@ -12744,8 +12849,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Bridgetown is the capital of Barbados.",
"review": {
@@ -12777,8 +12881,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Santiago is the capital of Chile.",
"review": {
@@ -12810,8 +12913,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Tarawa is the capital of Kiribati.",
"review": {
@@ -12843,8 +12945,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Ouagadougou is the capital of Burkina Faso.",
"review": {
@@ -12876,8 +12977,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Manama is the capital of Bahrain.",
"review": {
@@ -12909,8 +13009,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Minsk is the capital of Belarus.",
"review": {
@@ -12942,8 +13041,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Belmopan is the capital of Belize.",
"review": {
@@ -12975,8 +13073,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Bogota is the capital of Colombia.",
"review": {
@@ -13008,8 +13105,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Majuro is the capital of Marshall Islands.",
"review": {
@@ -13041,8 +13137,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Gitega is the capital of Burundi.",
"review": {
@@ -13074,8 +13169,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Dhaka is the capital of Bangladesh.",
"review": {
@@ -13107,8 +13201,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Brussels is the capital of Belgium.",
"review": {
@@ -13140,8 +13233,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Ottawa is the capital of Canada.",
"review": {
@@ -13173,8 +13265,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Quito is the capital of Ecuador.",
"review": {
@@ -13206,8 +13297,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Palikir is the capital of Micronesia.",
"review": {
@@ -13239,8 +13329,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Yaounde is the capital of Cameroon.",
"review": {
@@ -13272,8 +13361,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Thimphu is the capital of Bhutan.",
"review": {
@@ -13305,8 +13393,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Sarajevo is the capital of Bosnia and Herzegovina.",
"review": {
@@ -13338,8 +13425,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "San Jose is the capital of Costa Rica.",
"review": {
@@ -13371,8 +13457,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Georgetown is the capital of Guyana.",
"review": {
@@ -13404,8 +13489,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Yaren is the capital of Nauru.",
"review": {
@@ -13437,8 +13521,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Praia is the capital of Cape Verde.",
"review": {
@@ -13470,8 +13553,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Bandar Seri Begawan is the capital of Brunei.",
"review": {
@@ -13503,8 +13585,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Sofia is the capital of Bulgaria.",
"review": {
@@ -13536,8 +13617,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Havana is the capital of Cuba.",
"review": {
@@ -13569,8 +13649,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Asuncion is the capital of Paraguay.",
"review": {
@@ -13602,8 +13681,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Wellington is the capital of New Zealand.",
"review": {
@@ -13635,8 +13713,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Bangui is the capital of Central African Republic.",
"review": {
@@ -13668,8 +13745,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Phnom Penh is the capital of Cambodia.",
"review": {
@@ -13701,8 +13777,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Zagreb is the capital of Croatia.",
"review": {
@@ -13734,8 +13809,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Roseau is the capital of Dominica.",
"review": {
@@ -13767,8 +13841,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Lima is the capital of Peru.",
"review": {
@@ -13800,8 +13873,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Melekeok is the capital of Palau.",
"review": {
@@ -13833,8 +13905,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "N'Djamena is the capital of Chad.",
"review": {
@@ -13866,8 +13937,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Beijing is the capital of China.",
"review": {
@@ -13899,8 +13969,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Prague is the capital of Czechia.",
"review": {
@@ -13932,8 +14001,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Santo Domingo is the capital of Dominican Republic.",
"review": {
@@ -13965,8 +14033,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Paramaribo is the capital of Suriname.",
"review": {
@@ -13998,8 +14065,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Port Moresby is the capital of Papua New Guinea.",
"review": {
@@ -14031,8 +14097,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Moroni is the capital of Comoros.",
"review": {
@@ -14064,8 +14129,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Nicosia is the capital of Cyprus.",
"review": {
@@ -14097,8 +14161,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Copenhagen is the capital of Denmark.",
"review": {
@@ -14130,8 +14193,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "San Salvador is the capital of El Salvador.",
"review": {
@@ -14163,8 +14225,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Montevideo is the capital of Uruguay.",
"review": {
@@ -14196,8 +14257,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Apia is the capital of Samoa.",
"review": {
@@ -14229,8 +14289,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Kinshasa is the capital of Democratic Republic of the Congo.",
"review": {
@@ -14262,8 +14321,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Tbilisi is the capital of Georgia.",
"review": {
@@ -14295,8 +14353,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Tallinn is the capital of Estonia.",
"review": {
@@ -14328,8 +14385,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "St. George's is the capital of Grenada.",
"review": {
@@ -14361,8 +14417,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Caracas is the capital of Venezuela.",
"review": {
@@ -14394,8 +14449,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Honiara is the capital of Solomon Islands.",
"review": {
@@ -14427,8 +14481,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Djibouti is the capital of Djibouti.",
"review": {
@@ -14460,8 +14513,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "New Delhi is the capital of India.",
"review": {
@@ -14493,8 +14545,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Helsinki is the capital of Finland.",
"review": {
@@ -14526,8 +14577,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Guatemala City is the capital of Guatemala.",
"review": {
@@ -14559,8 +14609,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Nuku'alofa is the capital of Tonga.",
"review": {
@@ -14592,8 +14641,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Cairo is the capital of Egypt.",
"review": {
@@ -14625,8 +14673,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Tehran is the capital of Iran.",
"review": {
@@ -14658,8 +14705,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Paris is the capital of France.",
"review": {
@@ -14691,8 +14737,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Port-au-Prince is the capital of Haiti.",
"review": {
@@ -14724,8 +14769,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Funafuti is the capital of Tuvalu.",
"review": {
@@ -14757,8 +14801,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Ciudad de la Paz is the capital of Equatorial Guinea.",
"review": {
@@ -14790,8 +14833,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Baghdad is the capital of Iraq.",
"review": {
@@ -14823,8 +14865,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Berlin is the capital of Germany.",
"review": {
@@ -14856,8 +14897,7 @@
],
"supportedHints": [
"remove_choices",
- "reveal_continent",
- "factual_clue"
+ "reveal_continent"
],
"explanation": "Tegucigalpa is the capital of Honduras.",
"review": {
@@ -15176,7 +15216,7 @@
"at",
"au"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Austria: Vienna; Australia: Canberra.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Austria: Vienna; Australia: Canberra.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15192,7 +15232,7 @@
"ne",
"ng"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Niger: Niamey; Nigeria: Abuja.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Niger: Niamey; Nigeria: Abuja.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15208,7 +15248,7 @@
"dm",
"do"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Dominica: Roseau; Dominican Republic: Santo Domingo.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Dominica: Roseau; Dominican Republic: Santo Domingo.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15224,7 +15264,7 @@
"sk",
"si"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Slovakia: Bratislava; Slovenia: Ljubljana.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Slovakia: Bratislava; Slovenia: Ljubljana.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15240,7 +15280,7 @@
"ie",
"is"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Ireland: Dublin; Iceland: Reykjavik.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Ireland: Dublin; Iceland: Reykjavik.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15256,7 +15296,7 @@
"se",
"ch"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Sweden: Stockholm; Switzerland: Bern.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Sweden: Stockholm; Switzerland: Bern.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15272,7 +15312,7 @@
"gm",
"zm"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Gambia: Banjul; Zambia: Lusaka.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Gambia: Banjul; Zambia: Lusaka.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15288,7 +15328,7 @@
"mr",
"mu"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Mauritania: Nouakchott; Mauritius: Port Louis.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Mauritania: Nouakchott; Mauritius: Port Louis.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15306,7 +15346,7 @@
"gq",
"pg"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Guinea: Conakry; Guinea-Bissau: Bissau; Equatorial Guinea: Ciudad de la Paz; Papua New Guinea: Port Moresby.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Guinea: Conakry; Guinea-Bissau: Bissau; Equatorial Guinea: Ciudad de la Paz; Papua New Guinea: Port Moresby.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15322,7 +15362,7 @@
"cg",
"cd"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Republic of the Congo: Brazzaville; Democratic Republic of the Congo: Kinshasa.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Republic of the Congo: Brazzaville; Democratic Republic of the Congo: Kinshasa.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15338,7 +15378,7 @@
"kp",
"kr"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — North Korea: Pyongyang; South Korea: Seoul.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. North Korea: Pyongyang; South Korea: Seoul.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15354,7 +15394,7 @@
"sd",
"ss"
],
- "text": "These similarly named countries are separate catalog entries. Capital memory hook — Sudan: Khartoum; South Sudan: Juba.",
+ "text": "These similarly named countries are separate catalog entries. Capital memory hook. Sudan: Khartoum; South Sudan: Juba.",
"sourceIds": [
"source-country-codes-snapshot"
],
@@ -15386,7 +15426,7 @@
"id",
"mc"
],
- "text": "Both are red over white; their official proportions differ, so quiz choices keep the original ratios.",
+ "text": "Both are red over white; their official proportions differ, so trivia choices keep the original ratios.",
"sourceIds": [
"source-country-flags"
],
@@ -17651,6 +17691,6 @@
"state": "draft",
"reviewer": null,
"reviewedAt": null,
- "note": "Machine assembled from pinned sources. Every record still requires human fact, copy, sensitivity, and asset review."
+ "note": "Machine assembled from pinned sources. Land-border facts require human fact, copy, ambiguity, and sensitivity review; every flag requires visual audit."
}
}
diff --git a/composeApp/src/commonMain/kotlin/app/bertsatlas/App.kt b/composeApp/src/commonMain/kotlin/app/bertsatlas/App.kt
index 6bd0f47..c712ba3 100644
--- a/composeApp/src/commonMain/kotlin/app/bertsatlas/App.kt
+++ b/composeApp/src/commonMain/kotlin/app/bertsatlas/App.kt
@@ -16,6 +16,7 @@ import app.bertsatlas.domain.model.HintType
import app.bertsatlas.domain.model.Question
import app.bertsatlas.domain.model.QuizDuration
import app.bertsatlas.domain.model.RunPhase
+import app.bertsatlas.domain.model.RunStatus
import app.bertsatlas.ui.AnswerState
import app.bertsatlas.ui.BertsAtlasTheme
import app.bertsatlas.ui.BootstrapScreen
@@ -27,6 +28,7 @@ import app.bertsatlas.ui.FlagUi
import app.bertsatlas.ui.FunFactScreen
import app.bertsatlas.ui.HintChooserScreen
import app.bertsatlas.ui.HomeScreen
+import app.bertsatlas.ui.LeaveQuizConfirmationScreen
import app.bertsatlas.ui.LetterSlotUi
import app.bertsatlas.ui.LetterTileUi
import app.bertsatlas.ui.OptionsScreen
@@ -66,12 +68,21 @@ fun App(
@Composable
private fun AtlasDestination(controller: AppController, state: AtlasAppState) {
val run = state.run
+ if (state.leaveConfirmationRequested) {
+ LeaveQuizConfirmationScreen(
+ onCancel = controller::cancelLeave,
+ onLeave = controller::confirmLeave,
+ )
+ return
+ }
when (val destination = state.destination) {
Destination.Bootstrap -> BootstrapScreen()
Destination.Home -> HomeScreen(
isFirstLaunch = state.firstLaunch,
streakDays = state.streak.currentDays,
progressSummary = "${state.attempts.size} questions explored",
+ totalPoints = state.attempts.sumOf { it.pointsAwarded } - state.attempts.sumOf { it.hintPointsDeducted },
+ hasStartedTrivia = !state.firstLaunch,
onStart = controller::start,
onOptions = controller::openOptions,
onProgress = { controller.navigate(Destination.Progress) },
@@ -88,7 +99,7 @@ private fun AtlasDestination(controller: AppController, state: AtlasAppState) {
score = run.score,
timeRemaining = run.sessionRemainingMillis.formatTime(),
onResume = controller::resume,
- onAbandon = controller::abandon,
+ onAbandon = controller::requestLeave,
)
Destination.Quiz -> if (run == null) BootstrapScreen() else RunDestination(controller, state)
Destination.Results -> {
@@ -98,19 +109,20 @@ private fun AtlasDestination(controller: AppController, state: AtlasAppState) {
correctAnswers = result.correctAnswers,
answeredQuestions = result.answeredQuestions,
accuracy = "${result.accuracyPercent}%",
- averageResponseTime = "${result.averageResponseMillis / 1_000.0}s",
+ averageResponseTime = result.averageResponseMillis.formatSeconds(),
bestCorrectStreak = result.bestCorrectStreak,
masteryImproved = result.improvedMasteryItems,
revisitCountries = result.revisitCountryIds.mapNotNull { id -> controller.catalog.countries.firstOrNull { it.id == id }?.displayName },
newPersonalBest = state.newPersonalBest,
onPlayAgain = controller::start,
onHome = { controller.navigate(Destination.Home) },
+ abandoned = result.status == RunStatus.Abandoned,
)
}
Destination.Progress -> ProgressScreen(
currentStreak = state.streak.currentDays,
bestStreak = state.streak.bestDays,
- overallAccuracy = if (state.attempts.isEmpty()) "—" else "${state.attempts.count { it.correct } * 100 / state.attempts.size}%",
+ overallAccuracy = if (state.attempts.isEmpty()) "Not yet" else "${state.attempts.count { it.correct } * 100 / state.attempts.size}%",
totalAnswered = state.attempts.size,
masteryGroups = controller.progressGroups,
strongCountries = countryNames(controller, state.mastery.filter { it.level.name in setOf("Familiar", "Mastered") }.map { it.countryId }),
@@ -163,28 +175,32 @@ private fun RunDestination(controller: AppController, state: AtlasAppState) {
prompt = question.prompt,
imageDescription = if (promptFlag == null) null else "A flag to identify",
score = run.score,
- sessionTime = if (run.configuration.duration == QuizDuration.Unlimited) {
- (run.attempts.sumOf { it.responseMillis } + run.activeQuestion.questionElapsedMillis).formatTime()
- } else run.sessionRemainingMillis.formatTime(),
questionTime = run.activeQuestion.questionRemainingMillis.formatTime(),
timerExpired = run.activeQuestion.questionRemainingMillis == 0L,
choices = (question as? Question.MultipleChoice)?.let {
run.activeQuestion.orderedChoices.map { choice ->
- val choiceCountry = controller.catalog.countries.firstOrNull { it.id == choice.id }
ChoiceUi(
id = choice.id,
label = choice.label,
state = if (choice.id in run.activeQuestion.removedChoiceIds) AnswerState.Eliminated else AnswerState.Available,
enabled = choice.id !in run.activeQuestion.removedChoiceIds && run.phase == RunPhase.Answering,
- flag = choice.assetId?.let { FlagUi(it.removePrefix("flag-"), choiceCountry?.displayName ?: choice.label) },
+ flag = choice.assetId?.let { FlagUi(it.removePrefix("flag-"), choice.label) },
)
}
}.orEmpty(),
- letterSlots = letterSlots(question, run.activeQuestion.placements, run.activeQuestion.letterTiles),
+ letterSlots = letterSlots(
+ question,
+ run.activeQuestion.placements,
+ run.activeQuestion.letterTiles,
+ run.activeQuestion.appliedHints.mapNotNullTo(linkedSetOf()) { it.revealedTileId },
+ ),
letterTiles = run.activeQuestion.letterTiles.map { tile ->
LetterTileUi(tile.id, tile.character.toString(), run.activeQuestion.placements.firstOrNull { it.tileId == tile.id }?.slotIndex, tile.id !in run.activeQuestion.removedTileIds)
},
- hintAvailable = run.activeQuestion.hint == null && run.phase == RunPhase.Answering,
+ appliedHintMessages = run.activeQuestion.appliedHints.mapNotNull { it.message },
+ hintAvailable = run.activeQuestion.question.supportedHints.any {
+ it !in run.activeQuestion.usedHintTypes
+ } && run.phase == RunPhase.Answering,
unlimited = run.configuration.duration == QuizDuration.Unlimited,
canSubmit = question is Question.LetterBank && run.activeQuestion.placements.size == question.displayAnswer.count { it.isLetter() },
onChoice = controller::answerChoice,
@@ -196,11 +212,25 @@ private fun RunDestination(controller: AppController, state: AtlasAppState) {
onFinish = controller::finishUnlimited,
onLeave = controller::requestLeave,
promptFlag = promptFlag,
+ runProgress = run.configuration.duration.activeDurationMillis?.let { duration ->
+ 1f - ((run.sessionRemainingMillis ?: duration).toFloat() / duration.toFloat())
+ },
+ runProgressDescription = "Trivia time remaining ${run.sessionRemainingMillis.formatTime()}",
)
}
RunPhase.HintChooser -> HintChooserScreen(
score = run.score,
- hints = run.activeQuestion.question.supportedHints.map { ChoiceUi(it.name, it.displayName()) },
+ hints = run.activeQuestion.question.supportedHints.map { hint ->
+ val used = hint in run.activeQuestion.usedHintTypes
+ ChoiceUi(
+ id = hint.name,
+ label = hint.displayName(),
+ supportingText = if (used) "✓ Used" else null,
+ state = if (used) AnswerState.Selected else AnswerState.Available,
+ enabled = !used,
+ )
+ },
+ chargedHintLabel = "−${run.score / 2} points",
onChooseHint = { id -> controller.chooseHint(HintType.valueOf(id)) },
onCancel = controller::cancelHint,
)
@@ -227,7 +257,7 @@ private fun RunDestination(controller: AppController, state: AtlasAppState) {
RunPhase.FunFact -> requireNotNull(run.visibleFact).let { fact ->
FunFactScreen(fact.heading, fact.body, countryNames(controller, fact.countryIds).joinToString(), onContinue = controller::continueRun)
}
- RunPhase.Completed -> BootstrapScreen("Finishing your field notes…")
+ RunPhase.Completed -> BootstrapScreen("Preparing your summary…")
}
}
@@ -241,7 +271,12 @@ private fun topicChoices() = listOf(
ChoiceUi("MixedGeography", "Mixed geography", "A broader grab bag"),
)
-private fun letterSlots(question: Question, placements: List, tiles: List): List {
+private fun letterSlots(
+ question: Question,
+ placements: List,
+ tiles: List,
+ revealedTileIds: Set,
+): List {
val letterBank = question as? Question.LetterBank ?: return emptyList()
val tileById = tiles.associateBy { it.id }
var editableIndex = 0
@@ -249,7 +284,12 @@ private fun letterSlots(question: Question, placements: List): List = ids.distinct().mapNotNull { id ->
controller.catalog.countries.firstOrNull { it.id == id }?.displayName
}
diff --git a/composeApp/src/commonMain/kotlin/app/bertsatlas/AppController.kt b/composeApp/src/commonMain/kotlin/app/bertsatlas/AppController.kt
index a5f2b74..856340f 100644
--- a/composeApp/src/commonMain/kotlin/app/bertsatlas/AppController.kt
+++ b/composeApp/src/commonMain/kotlin/app/bertsatlas/AppController.kt
@@ -43,6 +43,7 @@ import app.bertsatlas.ui.StampUi
import kotlin.time.Clock
import kotlin.time.TimeSource
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -85,6 +86,8 @@ data class AtlasAppState(
val completedRuns: List = emptyList(),
val streak: Streak = Streak(),
val stamps: List = emptyList(),
+ val leaveConfirmationRequested: Boolean = false,
+ val resumeRunAfterLeaveCancellation: Boolean = false,
)
class AppController(
@@ -97,6 +100,7 @@ class AppController(
) {
private val json = Json { ignoreUnknownKeys = true }
private val mutex = Mutex()
+ private var lastRunPersistence: Job? = null
private val monotonicOrigin = TimeSource.Monotonic.markNow()
private val monotonicClock = MonotonicClock { monotonicOrigin.elapsedNow().inWholeMilliseconds }
private val wallClock = WallClock { Clock.System.now().toEpochMilliseconds() }
@@ -114,7 +118,9 @@ class AppController(
delay(100)
val current = _state.value.run
if (current?.phase == RunPhase.Answering) {
- _state.value = _state.value.copy(run = engine.advanceClocks(current))
+ val advanced = engine.advanceClocks(current)
+ if (advanced.sessionExpired) acceptTransition(engine.finishExpiredRun(advanced))
+ else _state.value = _state.value.copy(run = advanced)
}
}
}
@@ -132,9 +138,11 @@ class AppController(
runCatching { engine.restoreRun(json.decodeFromString(it.snapshotJson)) }.getOrNull()
}
if (snapshot != null && restored == null) database.runWriter().clearActiveRun()
+ val hasStartedTrivia = preferences.hasStartedTrivia || restored != null || attempts.isNotEmpty() || runs.isNotEmpty()
+ if (hasStartedTrivia && !preferences.hasStartedTrivia) preferencesStore.markTriviaStarted()
_state.value = AtlasAppState(
destination = if (restored == null) Destination.Home else Destination.Recovery,
- firstLaunch = !preferences.hasPreviouslyLaunched,
+ firstLaunch = !hasStartedTrivia,
preferences = preferences,
selectedDuration = enumValueOrDefault(preferences.duration, QuizDuration.FiveMinutes),
run = restored,
@@ -144,7 +152,6 @@ class AppController(
streak = streak,
stamps = stamps,
)
- preferencesStore.markLaunched()
}
fun start() {
@@ -184,11 +191,38 @@ class AppController(
}
}
- fun back() {
- _state.value = when (val destination = _state.value.destination) {
- is Destination.Options -> _state.value.copy(destination = if (destination.step == 2) Destination.Options(1) else Destination.Home)
- Destination.Privacy, Destination.Credits, Destination.Reset, Destination.Debug -> _state.value.copy(destination = Destination.Settings)
- else -> _state.value.copy(destination = Destination.Home)
+ fun back() { handleBack() }
+
+ fun handleBack(): Boolean {
+ val current = _state.value
+ if (current.leaveConfirmationRequested) {
+ cancelLeave()
+ return true
+ }
+ return when (val destination = current.destination) {
+ Destination.Bootstrap, Destination.Home -> false
+ is Destination.Options -> {
+ _state.value = current.copy(
+ destination = if (destination.step == 2) Destination.Options(1) else Destination.Home,
+ )
+ true
+ }
+ Destination.Recovery -> {
+ requestLeave()
+ true
+ }
+ Destination.Quiz -> {
+ if (current.run?.phase == RunPhase.HintChooser) cancelHint() else requestLeave()
+ true
+ }
+ Destination.Results, Destination.Progress, Destination.Settings -> {
+ _state.value = current.copy(destination = Destination.Home)
+ true
+ }
+ Destination.Privacy, Destination.Credits, Destination.Reset, Destination.Debug -> {
+ _state.value = current.copy(destination = Destination.Settings)
+ true
+ }
}
}
@@ -196,6 +230,7 @@ class AppController(
val now = wallClock.nowEpochMillis()
val run = engine.startRun("run-$now", configuration, now xor monotonicClock.nowMillis(), _state.value.mastery)
_state.value = _state.value.copy(destination = Destination.Quiz, firstLaunch = false, run = run, result = null)
+ scope.launch { preferencesStore.markTriviaStarted() }
persistSnapshot(run)
}
@@ -206,9 +241,11 @@ class AppController(
persistSnapshot(resumed)
}
- fun answerChoice(choiceId: String) = mutateRun { engine.answerQuestion(it, SubmittedAnswer.ChoiceAnswer(choiceId)) }.also {
- playAnswerFeedback()
- persistAcceptedAnswer()
+ fun answerChoice(choiceId: String) {
+ if (mutateRun { engine.answerQuestion(it, SubmittedAnswer.ChoiceAnswer(choiceId)) }) {
+ playAnswerFeedback()
+ persistAcceptedAnswer()
+ }
}
fun placeLetter(tileId: String) = mutateRun { engine.placeLetter(it, tileId) }
@@ -220,14 +257,18 @@ class AppController(
fun clearLetters() = mutateRun(engine::clearLetters)
- fun submitLetters() = mutateRun(engine::submitLetters).also {
- playAnswerFeedback()
- persistAcceptedAnswer()
+ fun submitLetters() {
+ if (mutateRun(engine::submitLetters)) {
+ playAnswerFeedback()
+ persistAcceptedAnswer()
+ }
}
fun openHint() = mutateRun(engine::openHintChooser)
fun cancelHint() = mutateRun { state -> engine.resumeRun(state.copy(phase = RunPhase.Paused, pausedFromPhase = RunPhase.Answering)) }
- fun chooseHint(type: HintType) = mutateRun { engine.chooseHint(it, type) }.also { persistSnapshot(_state.value.run) }
+ fun chooseHint(type: HintType) {
+ if (mutateRun { engine.chooseHint(it, type) }) persistSnapshot(_state.value.run)
+ }
fun continueRun() {
val before = _state.value.run ?: return
@@ -244,17 +285,65 @@ class AppController(
acceptTransition(engine.finishUnlimitedRun(run, _state.value.mastery))
}
- fun requestLeave() { abandon() }
+ fun requestLeave() {
+ val current = _state.value
+ val run = current.run ?: return
+ if (run.status != RunStatus.Active || current.leaveConfirmationRequested) return
+ val shouldResumeOnCancel = run.phase != RunPhase.Paused
+ val paused = if (shouldResumeOnCancel) engine.pauseForInterruption(run) else run
+ _state.value = current.copy(
+ run = paused,
+ leaveConfirmationRequested = true,
+ resumeRunAfterLeaveCancellation = shouldResumeOnCancel,
+ )
+ persistSnapshot(paused)
+ }
+
+ fun cancelLeave() {
+ val current = _state.value
+ if (!current.leaveConfirmationRequested) return
+ val run = current.run
+ val resumed = if (
+ current.resumeRunAfterLeaveCancellation &&
+ run?.status == RunStatus.Active &&
+ run.phase == RunPhase.Paused
+ ) {
+ engine.resumeRun(run)
+ } else {
+ run
+ }
+ _state.value = current.copy(
+ run = resumed,
+ leaveConfirmationRequested = false,
+ resumeRunAfterLeaveCancellation = false,
+ )
+ persistSnapshot(resumed)
+ }
+
+ fun confirmLeave() { abandon() }
fun abandon() {
val run = _state.value.run ?: return
val abandoned = if (run.status == RunStatus.Active) engine.abandonRun(run) else run
- _state.value = _state.value.copy(destination = Destination.Home, run = null)
- scope.launch {
- mutex.withLock {
- database.runWriter().abandonRun(abandoned.toRunEntity())
- refreshProgress()
- }
+ if (abandoned.status != RunStatus.Abandoned) return
+ val summary = engine.abandonedRunSummary(
+ abandoned,
+ improvedMasteryItems = abandoned.attempts
+ .flatMap { attempt -> attempt.countryIds.map { it to attempt.skill } }
+ .distinct()
+ .size,
+ )
+ _state.value = _state.value.copy(
+ destination = Destination.Results,
+ run = abandoned,
+ result = summary,
+ newPersonalBest = false,
+ leaveConfirmationRequested = false,
+ resumeRunAfterLeaveCancellation = false,
+ )
+ enqueueRunPersistence {
+ database.runWriter().abandonRun(abandoned.toRunEntity(summary))
+ refreshProgress()
}
}
@@ -268,16 +357,18 @@ class AppController(
fun resumeAfterInterruption() {
val run = _state.value.run ?: return
+ if (_state.value.leaveConfirmationRequested) return
if (_state.value.destination != Destination.Quiz || run.phase != RunPhase.Paused) return
val resumed = engine.resumeRun(run)
_state.value = _state.value.copy(run = resumed)
persistSnapshot(resumed)
}
- private fun mutateRun(change: (QuizRunState) -> QuizRunState) {
- val current = _state.value.run ?: return
- val next = runCatching { change(current) }.getOrNull() ?: return
+ private fun mutateRun(change: (QuizRunState) -> QuizRunState): Boolean {
+ val current = _state.value.run ?: return false
+ val next = runCatching { change(current) }.getOrNull() ?: return false
_state.value = _state.value.copy(destination = destinationFor(next), run = next)
+ return true
}
private fun acceptTransition(next: QuizRunState) {
@@ -291,18 +382,16 @@ class AppController(
private fun persistAcceptedAnswer() {
val run = _state.value.run ?: return
val attempt = run.attempts.lastOrNull() ?: return
- scope.launch {
- mutex.withLock {
- val before = _state.value.mastery
- val after = MasteryRules.applyAttemptToCountries(before, attempt)
- val changed = after.filter { updated -> before.none { it.countryId == updated.countryId && it.skill == updated.skill && it == updated } }
- database.runWriter().recordAcceptedAnswer(
- attempt.toEntity(json),
- changed.map { it.toEntity() },
- run.toSnapshotEntity(engine, json),
- )
- _state.value = _state.value.copy(mastery = after)
- }
+ enqueueRunPersistence {
+ val before = _state.value.mastery
+ val after = MasteryRules.applyAttemptToCountries(before, attempt)
+ val changed = after.filter { updated -> before.none { it.countryId == updated.countryId && it.skill == updated.skill && it == updated } }
+ database.runWriter().recordAcceptedAnswer(
+ attempt.toEntity(json),
+ changed.map { it.toEntity() },
+ run.toSnapshotEntity(engine, json),
+ )
+ _state.value = _state.value.copy(mastery = after)
}
}
@@ -314,18 +403,15 @@ class AppController(
private fun persistSnapshot(run: QuizRunState?) {
if (run == null || run.status != RunStatus.Active) return
- scope.launch {
- mutex.withLock {
- database.runWriter().saveActiveRun(run.toSnapshotEntity(engine, json))
- }
+ enqueueRunPersistence {
+ database.runWriter().saveActiveRun(run.toSnapshotEntity(engine, json))
}
}
private fun complete(run: QuizRunState) {
val result = engine.runResult(run, improvedMasteryItems = run.attempts.map { it.countryIds.firstOrNull() to it.skill }.distinct().size)
_state.value = _state.value.copy(destination = Destination.Results, run = run, result = result)
- scope.launch {
- mutex.withLock {
+ enqueueRunPersistence {
val reader = database.progressReader()
val oldBest = reader.personalBestsNow().firstOrNull {
it.duration == run.configuration.duration.name && it.topic == run.configuration.topic.name
@@ -357,7 +443,14 @@ class AppController(
)
_state.value = _state.value.copy(newPersonalBest = isBest, streak = newStreak, stamps = existingStamps + newlyEarned)
refreshProgress()
- }
+ }
+ }
+
+ private fun enqueueRunPersistence(block: suspend () -> Unit) {
+ val precedingWrite = lastRunPersistence
+ lastRunPersistence = scope.launch {
+ precedingWrite?.join()
+ mutex.withLock { block() }
}
}
@@ -387,7 +480,7 @@ class AppController(
}
fun resetAll() {
- scope.launch {
+ enqueueRunPersistence {
database.runWriter().resetProgress()
preferencesStore.resetAll()
_state.value = AtlasAppState(destination = Destination.Home)
@@ -398,10 +491,12 @@ class AppController(
if (!debugBuild) return
when (id) {
"reset-first" -> scope.launch { preferencesStore.resetFirstLaunchAndConfiguration(); _state.value = _state.value.copy(firstLaunch = true, destination = Destination.Home) }
- "reset-progress" -> scope.launch { database.runWriter().resetProgress(); refreshProgress(); _state.value = _state.value.copy(destination = Destination.Home, run = null) }
+ "reset-progress" -> enqueueRunPersistence { database.runWriter().resetProgress(); refreshProgress(); _state.value = _state.value.copy(destination = Destination.Home, run = null) }
"reset-all" -> resetAll()
"question-zero" -> updateRunDebug { it.copy(activeQuestion = it.activeQuestion.copy(questionRemainingMillis = 0)) }
- "session-zero" -> updateRunDebug { it.copy(sessionRemainingMillis = 0, sessionExpired = true) }
+ "session-zero" -> updateRunDebug {
+ it.copy(sessionRemainingMillis = 0, sessionExpired = true, runCompletionPending = true)
+ }
"score-100" -> updateRunDebug { it.copy(score = 100) }
}
}
diff --git a/composeApp/src/commonMain/kotlin/app/bertsatlas/catalog/CatalogLoader.kt b/composeApp/src/commonMain/kotlin/app/bertsatlas/catalog/CatalogLoader.kt
index 5d45cb7..1a70185 100644
--- a/composeApp/src/commonMain/kotlin/app/bertsatlas/catalog/CatalogLoader.kt
+++ b/composeApp/src/commonMain/kotlin/app/bertsatlas/catalog/CatalogLoader.kt
@@ -42,6 +42,9 @@ object CatalogLoader {
val similarityGroupsByCountry = source.similarityGroups
.flatMap { group -> group.countryIds.map { it to group.id } }
.groupBy({ it.first }, { it.second })
+ val curatedCapitalCountryIds = source.curatedQuestions
+ .filter { it.id.startsWith("mixed-capital-") }
+ .flatMapTo(mutableSetOf()) { it.countryIds }
val domainCountries = source.countries.map { country ->
country.toDomain(similarityGroupsByCountry[country.id].orEmpty())
}
@@ -53,11 +56,10 @@ object CatalogLoader {
similarityGroupIds = similarityGroupsByCountry[country.id].orEmpty(),
similarityGroups = source.similarityGroups,
relatedFacts = factsByCountry[country.id].orEmpty(),
+ hasCuratedCapitalQuestion = country.id in curatedCapitalCountryIds,
)
}
- val curatedQuestions = source.curatedQuestions.map { question ->
- question.toDomain(countriesById)
- }
+ val curatedQuestions = source.curatedQuestions.map { question -> question.toDomain(countriesById) }
return Catalog(
schemaVersion = source.schemaVersion,
contentVersion = source.catalogVersion,
@@ -76,6 +78,7 @@ object CatalogLoader {
similarityGroupIds: List,
similarityGroups: List,
relatedFacts: List,
+ hasCuratedCapitalQuestion: Boolean,
): List {
val distractors = chooseDistractors(
country = country,
@@ -94,13 +97,22 @@ object CatalogLoader {
require(capitalChoices.size == 4) { "Not enough unique capital choices for ${country.id}" }
val difficulty = generatedDifficulty(countryIndex)
val continentHint = country.region.toContinent().displayName()
- val factualClue = relatedFacts.firstOrNull()?.text
+ val factualClue = relatedFacts.firstOrNull()?.text?.withoutAnswer(country)
+ val capitalFactualClue = factualClue?.takeUnless { clue ->
+ country.capital.names.any { capital -> clue.contains(capital, ignoreCase = true) }
+ }
val commonHintText = buildMap {
put(HintType.RevealContinent, continentHint)
factualClue?.let { put(HintType.FunnyFactualClue, it) }
}
val factualHint = if (factualClue == null) emptySet() else setOf(HintType.FunnyFactualClue)
val sharedHints = setOf(HintType.RevealContinent) + factualHint
+ val capitalHintText = buildMap {
+ put(HintType.RevealContinent, continentHint)
+ capitalFactualClue?.let { put(HintType.FunnyFactualClue, it) }
+ }
+ val capitalHints = setOf(HintType.RevealContinent) +
+ if (capitalFactualClue == null) emptySet() else setOf(HintType.FunnyFactualClue)
val flagToCountry = Question.MultipleChoice(
id = "generated-flag-to-country-${country.id}",
prompt = "Which country does this flag belong to?",
@@ -125,8 +137,12 @@ object CatalogLoader {
explanation = "This is the flag of ${country.displayName}.",
supportedHints = sharedHints + HintType.RemoveIncorrectOptions,
hintText = commonHintText,
- choices = choices.map {
- Choice(id = it.id, label = it.displayName, assetId = it.flagAssetId)
+ choices = choices.mapIndexed { choiceIndex, choice ->
+ Choice(
+ id = choice.id,
+ label = "Flag ${('A'.code + choiceIndex).toChar()}",
+ assetId = choice.flagAssetId,
+ )
},
correctChoiceId = country.id,
)
@@ -155,26 +171,34 @@ object CatalogLoader {
skill = Skill.CapitalRecall,
topic = QuizTopic.Capitals,
difficulty = difficulty,
- explanation = country.capital.note ?: "${country.capital.acceptedAnswer} is the capital answer used for ${country.displayName}.",
- supportedHints = sharedHints + HintType.RemoveIncorrectOptions,
- hintText = commonHintText,
+ explanation = country.capital.note ?: "${country.capital.acceptedAnswer} is the capital of ${country.displayName}.",
+ supportedHints = capitalHints + HintType.RemoveIncorrectOptions,
+ hintText = capitalHintText,
choices = capitalChoices.map { Choice(it.id, it.capital.acceptedAnswer) },
correctChoiceId = country.id,
)
val capitalToCountry = Question.MultipleChoice(
id = "generated-capital-to-country-${country.id}",
- prompt = "${country.capital.acceptedAnswer} is the capital answer for which country?",
+ prompt = "${country.capital.acceptedAnswer} is the capital of which country?",
countryIds = listOf(country.id),
skill = Skill.CapitalRecognition,
topic = QuizTopic.Capitals,
difficulty = difficulty,
- explanation = country.capital.note ?: "${country.capital.acceptedAnswer} is the capital answer used for ${country.displayName}.",
- supportedHints = sharedHints + HintType.RemoveIncorrectOptions,
- hintText = commonHintText,
+ explanation = country.capital.note ?: "${country.capital.acceptedAnswer} is the capital of ${country.displayName}.",
+ supportedHints = capitalHints + HintType.RemoveIncorrectOptions,
+ hintText = capitalHintText,
choices = choices.map { Choice(it.id, it.displayName) },
correctChoiceId = country.id,
)
- return listOf(flagToCountry, countryToFlag, letterBank, countryToCapital, capitalToCountry)
+ return buildList {
+ add(flagToCountry)
+ add(countryToFlag)
+ add(letterBank)
+ if (country.capital.questionEligible) {
+ if (!hasCuratedCapitalQuestion) add(countryToCapital)
+ add(capitalToCountry)
+ }
+ }
}
private fun chooseDistractors(
@@ -209,7 +233,11 @@ object CatalogLoader {
val answerIndex = choices.indexOf(answer)
require(answerIndex >= 0) { "Curated question $id does not contain its answer" }
val primaryCountry = countryIds.firstNotNullOfOrNull(countriesById::get)
- val mappedHints = supportedHints.mapTo(linkedSetOf()) { it.toHintType() }
+ val mappedHints = supportedHints
+ .mapTo(linkedSetOf()) { it.toHintType() }
+ .apply {
+ if (explanation.contains(answer, ignoreCase = true)) remove(HintType.FunnyFactualClue)
+ }
val hintText = buildMap {
if (HintType.RevealContinent in mappedHints && primaryCountry != null) {
put(HintType.RevealContinent, primaryCountry.region.toContinent().displayName())
@@ -335,6 +363,14 @@ object CatalogLoader {
private const val DECOY_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
private val decoder = Json { ignoreUnknownKeys = true }
+
+ private fun String.withoutAnswer(country: SourceCountry): String {
+ val names = (country.acceptedAliases + country.displayName).sortedByDescending(String::length)
+ val redacted = names.fold(this) { text, name ->
+ text.replace(Regex(Regex.escape(name), RegexOption.IGNORE_CASE), "this country")
+ }
+ return redacted.replaceFirstChar { it.uppercase() }
+ }
}
@Serializable
@@ -373,6 +409,7 @@ private data class SourceCapital(
val kind: String,
val names: List,
val acceptedAnswer: String,
+ val questionEligible: Boolean,
val note: String?,
)
diff --git a/composeApp/src/commonMain/kotlin/app/bertsatlas/data/PreferencesStore.kt b/composeApp/src/commonMain/kotlin/app/bertsatlas/data/PreferencesStore.kt
index e2a5f23..e33d68f 100644
--- a/composeApp/src/commonMain/kotlin/app/bertsatlas/data/PreferencesStore.kt
+++ b/composeApp/src/commonMain/kotlin/app/bertsatlas/data/PreferencesStore.kt
@@ -22,7 +22,7 @@ import kotlinx.coroutines.CancellationException
import okio.Path.Companion.toPath
data class SavedPreferences(
- val hasPreviouslyLaunched: Boolean = false,
+ val hasStartedTrivia: Boolean = false,
val duration: String = "FiveMinutes",
val topic: String = "MostlyFlags",
val soundEnabled: Boolean = true,
@@ -41,7 +41,7 @@ class PreferencesStore(
}
.map { preferences ->
SavedPreferences(
- hasPreviouslyLaunched = preferences[Keys.previouslyLaunched] ?: false,
+ hasStartedTrivia = preferences[Keys.hasStartedTrivia] ?: false,
duration = preferences[Keys.duration] ?: "FiveMinutes",
topic = preferences[Keys.topic] ?: "MostlyFlags",
soundEnabled = preferences[Keys.soundEnabled] ?: true,
@@ -51,10 +51,10 @@ class PreferencesStore(
)
}
- override suspend fun hasLaunchedBefore(): Boolean = values.first().hasPreviouslyLaunched
+ override suspend fun hasStartedTrivia(): Boolean = values.first().hasStartedTrivia
- override suspend fun markLaunched() {
- dataStore.edit { it[Keys.previouslyLaunched] = true }
+ override suspend fun markTriviaStarted() {
+ dataStore.edit { it[Keys.hasStartedTrivia] = true }
}
override suspend fun loadConfiguration(): QuizConfiguration {
@@ -70,8 +70,8 @@ class PreferencesStore(
}
suspend fun saveConfiguration(duration: String, topic: String) {
- require(QuizDuration.entries.any { it.name == duration }) { "Unknown quiz duration: $duration" }
- require(QuizTopic.entries.any { it.name == topic }) { "Unknown quiz topic: $topic" }
+ require(QuizDuration.entries.any { it.name == duration }) { "Unknown trivia duration: $duration" }
+ require(QuizTopic.entries.any { it.name == topic }) { "Unknown trivia topic: $topic" }
dataStore.edit {
it[Keys.duration] = duration
it[Keys.topic] = topic
@@ -114,7 +114,7 @@ class PreferencesStore(
}
suspend fun resetFirstLaunch() {
- dataStore.edit { it.remove(Keys.previouslyLaunched) }
+ dataStore.edit { it.remove(Keys.hasStartedTrivia) }
}
suspend fun resetConfiguration() {
@@ -135,7 +135,7 @@ class PreferencesStore(
suspend fun resetFirstLaunchAndConfiguration() {
dataStore.edit {
- it.remove(Keys.previouslyLaunched)
+ it.remove(Keys.hasStartedTrivia)
it.remove(Keys.duration)
it.remove(Keys.topic)
}
@@ -146,7 +146,7 @@ class PreferencesStore(
}
private object Keys {
- val previouslyLaunched = booleanPreferencesKey("previously_launched")
+ val hasStartedTrivia = booleanPreferencesKey("has_started_trivia")
val duration = stringPreferencesKey("quiz_duration")
val topic = stringPreferencesKey("quiz_topic")
val soundEnabled = booleanPreferencesKey("sound_enabled")
diff --git a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/AtlasComponents.kt b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/AtlasComponents.kt
index 1cb5b4f..639763c 100644
--- a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/AtlasComponents.kt
+++ b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/AtlasComponents.kt
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
@@ -23,14 +24,22 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -59,12 +68,6 @@ import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.InternalResourceApi
import org.jetbrains.compose.resources.ResourceItem
import org.jetbrains.compose.resources.painterResource
-import berts_atlas.composeapp.generated.resources.Res
-import io.github.alexzhirkevich.compottie.Compottie
-import io.github.alexzhirkevich.compottie.LottieCompositionSpec
-import io.github.alexzhirkevich.compottie.animateLottieCompositionAsState
-import io.github.alexzhirkevich.compottie.rememberLottieComposition
-import io.github.alexzhirkevich.compottie.rememberLottiePainter
private val ScreenShape = RoundedCornerShape(24.dp)
private val ControlShape = RoundedCornerShape(16.dp)
@@ -77,12 +80,16 @@ fun AtlasScreen(
content: @Composable BoxScope.() -> Unit,
) {
Box(
- modifier = modifier.fillMaxSize().background(AtlasTheme.colors.parchment),
+ modifier = modifier
+ .fillMaxSize()
+ .background(AtlasTheme.colors.parchment)
+ .windowInsetsPadding(WindowInsets.safeDrawing),
contentAlignment = Alignment.TopCenter,
) {
val contentModifier = Modifier
.fillMaxWidth()
- .widthIn(max = 760.dp)
+ .fillMaxHeight()
+ .widthIn(max = 720.dp)
.padding(contentPadding)
.let { if (scrollable) it.verticalScroll(rememberScrollState()) else it }
Box(modifier = contentModifier, content = content)
@@ -134,12 +141,22 @@ fun AtlasButton(
AtlasButtonStyle.Primary, AtlasButtonStyle.Destructive -> colors.white
AtlasButtonStyle.Secondary, AtlasButtonStyle.Quiet -> colors.plum
}
- val border = if (style == AtlasButtonStyle.Secondary) Modifier.border(2.dp, colors.atlasBlue, ControlShape) else Modifier
+ val border = when (style) {
+ AtlasButtonStyle.Secondary -> Modifier.border(1.5.dp, colors.atlasBlue.copy(alpha = .75f), ControlShape)
+ AtlasButtonStyle.Destructive -> Modifier.border(1.5.dp, colors.orangeAccent, ControlShape)
+ else -> Modifier
+ }
+ val resolvedBackground = when {
+ !enabled -> colors.outline.copy(alpha = .45f)
+ style == AtlasButtonStyle.Destructive -> colors.paper
+ else -> background
+ }
+ val resolvedForeground = if (style == AtlasButtonStyle.Destructive) colors.error else foreground
Box(
modifier = modifier
.heightIn(min = 52.dp)
.then(border)
- .background(if (enabled) background else colors.outline, ControlShape)
+ .background(resolvedBackground, ControlShape)
.semantics {
role = Role.Button
this.contentDescription = contentDescription
@@ -149,7 +166,7 @@ fun AtlasButton(
.padding(horizontal = 22.dp, vertical = 14.dp),
contentAlignment = Alignment.Center,
) {
- AtlasText(text, style = AtlasTheme.typography.label, color = foreground, textAlign = TextAlign.Center)
+ AtlasText(text, style = AtlasTheme.typography.label, color = resolvedForeground, textAlign = TextAlign.Center)
}
}
@@ -163,7 +180,7 @@ fun AtlasCard(
modifier = modifier
.fillMaxWidth()
.background(AtlasTheme.colors.paper, ScreenShape)
- .border(1.dp, AtlasTheme.colors.outline.copy(alpha = .45f), ScreenShape)
+ .border(1.dp, AtlasTheme.colors.outline.copy(alpha = .25f), ScreenShape)
.then(if (contentDescription == null) Modifier else Modifier.semantics(mergeDescendants = true) { this.contentDescription = contentDescription })
.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
@@ -180,24 +197,25 @@ fun AtlasChoice(
val colors = AtlasTheme.colors
val background = when (choice.state) {
AnswerState.Available -> colors.paper
- AnswerState.Selected -> colors.mustard.copy(alpha = .28f)
+ AnswerState.Selected -> colors.seaGlass
AnswerState.Correct -> colors.success.copy(alpha = .16f)
- AnswerState.Incorrect -> colors.error.copy(alpha = .14f)
+ AnswerState.Incorrect -> colors.orangeAccent.copy(alpha = .18f)
AnswerState.Eliminated -> colors.outline.copy(alpha = .12f)
}
val outline = when (choice.state) {
AnswerState.Available -> colors.outline
AnswerState.Selected -> colors.mustard
AnswerState.Correct -> colors.success
- AnswerState.Incorrect -> colors.error
+ AnswerState.Incorrect -> colors.orangeAccent
AnswerState.Eliminated -> colors.outline.copy(alpha = .5f)
}
- val stateText = when (choice.state) {
- AnswerState.Available -> "Available"
- AnswerState.Selected -> "Selected"
- AnswerState.Correct -> "Correct answer"
- AnswerState.Incorrect -> "Incorrect answer"
- AnswerState.Eliminated -> "Eliminated"
+ val stateText = when {
+ !choice.enabled && choice.state == AnswerState.Selected -> "Used"
+ choice.state == AnswerState.Available -> "Available"
+ choice.state == AnswerState.Selected -> "Selected"
+ choice.state == AnswerState.Correct -> "Correct answer"
+ choice.state == AnswerState.Incorrect -> "Incorrect answer"
+ else -> "Eliminated"
}
Row(
modifier = modifier
@@ -258,12 +276,13 @@ fun FlagImage(
),
)
}
+ val accessibleLabel = if (flag.countryName.startsWith("Flag ", ignoreCase = true)) flag.countryName else "Flag of ${flag.countryName}"
val imageModifier = modifier
.aspectRatio(3f / 2f)
.clip(RoundedCornerShape(8.dp))
.background(AtlasTheme.colors.paper, RoundedCornerShape(8.dp))
.border(1.dp, AtlasTheme.colors.outline.copy(alpha = .55f), RoundedCornerShape(8.dp))
- .then(if (accessible) Modifier.semantics { contentDescription = "Flag of ${flag.countryName}" } else Modifier)
+ .then(if (accessible) Modifier.semantics { contentDescription = accessibleLabel } else Modifier)
if (validCode) {
Image(
painter = painterResource(resource),
@@ -314,7 +333,7 @@ fun AtlasToggle(setting: SettingToggleUi, onToggle: (String, Boolean) -> Unit) {
}
Box(
Modifier.size(width = 54.dp, height = 32.dp)
- .background(if (setting.checked) AtlasTheme.colors.success else AtlasTheme.colors.outline, CircleShape)
+ .background(if (setting.checked) AtlasTheme.colors.success else AtlasTheme.colors.seaGlass, CircleShape)
.padding(4.dp),
contentAlignment = if (setting.checked) Alignment.CenterEnd else Alignment.CenterStart,
) {
@@ -351,50 +370,22 @@ fun BertIllustration(
StaticBertIllustration(pose, modifier, contentDescription)
return
}
- val compositionResult = rememberLottieComposition(pose) {
- LottieCompositionSpec.JsonString(
- Res.readBytes("files/${pose.animationFileName}").decodeToString(),
- )
- }
- val composition = compositionResult.value
- if (composition == null || compositionResult.isFailure) {
- StaticBertIllustration(pose, modifier, contentDescription)
- return
- }
- val progress by animateLottieCompositionAsState(
- composition = composition,
- iterations = Compottie.IterateForever,
+ val transition = rememberInfiniteTransition(label = "Bert bulb")
+ val rayLength by transition.animateFloat(
+ initialValue = .82f,
+ targetValue = 1.08f,
+ animationSpec = infiniteRepeatable(tween(800), RepeatMode.Reverse),
+ label = "Bulb ray length",
)
- Box(
- modifier = modifier.size(180.dp).semantics { this.contentDescription = contentDescription },
- ) {
- StaticBertIllustration(pose, Modifier.matchParentSize(), contentDescription = null)
- Image(
- painter = rememberLottiePainter(composition = composition, progress = { progress }),
- contentDescription = null,
- modifier = Modifier.matchParentSize(),
- )
- }
+ StaticBertIllustration(pose, modifier, contentDescription, rayLength)
}
-private val BertPose.animationFileName: String
- get() = when (this) {
- BertPose.Idle -> "bert_idle.json"
- BertPose.Correct -> "bert_correct.json"
- BertPose.Incorrect -> "bert_incorrect.json"
- BertPose.Overtime -> "bert_overtime.json"
- BertPose.Hint -> "bert_hint.json"
- BertPose.Fact -> "bert_fact.json"
- BertPose.Results -> "bert_results.json"
- BertPose.Streak -> "bert_streak.json"
- BertPose.Stamp -> "bert_stamp.json"
- }
-
@Composable
private fun StaticBertIllustration(
pose: BertPose,
modifier: Modifier,
contentDescription: String?,
+ rayLength: Float = 1f,
) {
val colors = AtlasTheme.colors
val accessibleModifier = if (contentDescription == null) Modifier else Modifier.semantics { this.contentDescription = contentDescription }
@@ -437,24 +428,75 @@ private fun StaticBertIllustration(
quadraticTo(90f * scale, (if (pose == BertPose.Incorrect) 106f else 130f) * scale, 109f * scale, mouthY * scale)
}
drawPath(mouth, colors.plum, style = Stroke(4f * scale, cap = StrokeCap.Round))
- val accent = when (pose) {
- BertPose.Correct, BertPose.Streak, BertPose.Stamp -> colors.success
- BertPose.Incorrect -> colors.error
- BertPose.Overtime, BertPose.Hint -> colors.mustard
- BertPose.Fact, BertPose.Results, BertPose.Idle -> colors.atlasBlue
- }
+ val accent = colors.orangeAccent
drawCircle(accent, 10f * scale, p(145f, 39f))
- drawLine(accent, p(145f, 17f), p(145f, 7f), 4f * scale, StrokeCap.Round)
- drawLine(accent, p(161f, 25f), p(169f, 18f), 4f * scale, StrokeCap.Round)
+ drawLine(accent, p(145f, 17f), p(145f, 17f - 10f * rayLength), 4f * scale, StrokeCap.Round)
+ drawLine(accent, p(161f, 25f), p(161f + 8f * rayLength, 25f - 7f * rayLength), 4f * scale, StrokeCap.Round)
}
}
@Composable
fun ScreenTopBar(title: String, onBack: (() -> Unit)? = null, trailing: (@Composable () -> Unit)? = null) {
- Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
- if (onBack != null) AtlasButton("Back", onBack, style = AtlasButtonStyle.Quiet)
- AtlasText(title, Modifier.weight(1f).semantics { heading() }, style = AtlasTheme.typography.heading)
- trailing?.invoke()
+ Row(Modifier.fillMaxWidth().heightIn(min = 52.dp), verticalAlignment = Alignment.CenterVertically) {
+ if (onBack != null) AtlasBackButton(onBack) else Spacer(Modifier.size(48.dp))
+ AtlasText(
+ title,
+ Modifier.weight(1f).semantics { heading() },
+ style = AtlasTheme.typography.heading,
+ textAlign = TextAlign.Center,
+ )
+ if (trailing == null) Spacer(Modifier.size(48.dp)) else Box(Modifier.widthIn(min = 48.dp), contentAlignment = Alignment.CenterEnd) { trailing() }
+ }
+}
+
+@Composable
+fun AtlasBackButton(onClick: () -> Unit, contentDescription: String = "Back") {
+ val color = AtlasTheme.colors.plum
+ Box(
+ Modifier.size(48.dp)
+ .semantics { role = Role.Button; this.contentDescription = contentDescription }
+ .clickable(onClick = onClick),
+ contentAlignment = Alignment.Center,
+ ) {
+ Canvas(Modifier.size(24.dp)) {
+ drawLine(color, Offset(size.width * .7f, size.height * .16f), Offset(size.width * .3f, size.height * .5f), 3.2.dp.toPx(), StrokeCap.Round)
+ drawLine(color, Offset(size.width * .3f, size.height * .5f), Offset(size.width * .7f, size.height * .84f), 3.2.dp.toPx(), StrokeCap.Round)
+ }
+ }
+}
+
+@Composable
+fun AtlasExitButton(onClick: () -> Unit, contentDescription: String = "Leave trivia") {
+ val color = AtlasTheme.colors.plum
+ Box(
+ Modifier.size(48.dp)
+ .semantics { role = Role.Button; this.contentDescription = contentDescription }
+ .clickable(onClick = onClick),
+ contentAlignment = Alignment.Center,
+ ) {
+ Canvas(Modifier.size(26.dp)) {
+ val stroke = 2.6.dp.toPx()
+ drawLine(color, Offset(size.width * .18f, size.height * .12f), Offset(size.width * .18f, size.height * .88f), stroke, StrokeCap.Round)
+ drawLine(color, Offset(size.width * .18f, size.height * .12f), Offset(size.width * .56f, size.height * .12f), stroke, StrokeCap.Round)
+ drawLine(color, Offset(size.width * .18f, size.height * .88f), Offset(size.width * .56f, size.height * .88f), stroke, StrokeCap.Round)
+ drawLine(color, Offset(size.width * .48f, size.height * .5f), Offset(size.width * .9f, size.height * .5f), stroke, StrokeCap.Round)
+ drawLine(color, Offset(size.width * .73f, size.height * .32f), Offset(size.width * .9f, size.height * .5f), stroke, StrokeCap.Round)
+ drawLine(color, Offset(size.width * .9f, size.height * .5f), Offset(size.width * .73f, size.height * .68f), stroke, StrokeCap.Round)
+ }
+ }
+}
+
+@Composable
+fun AtlasLinkRow(text: String, onClick: () -> Unit, destructive: Boolean = false) {
+ Row(
+ Modifier.fillMaxWidth().heightIn(min = 56.dp)
+ .semantics { role = Role.Button; contentDescription = text }
+ .clickable(onClick = onClick)
+ .padding(horizontal = 4.dp, vertical = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ AtlasText(text, Modifier.weight(1f), style = AtlasTheme.typography.label, color = if (destructive) AtlasTheme.colors.error else AtlasTheme.colors.plum)
+ AtlasText("›", style = AtlasTheme.typography.heading, color = AtlasTheme.colors.mutedPlum)
}
}
diff --git a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/AtlasTheme.kt b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/AtlasTheme.kt
index e22207e..ec1efa1 100644
--- a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/AtlasTheme.kt
+++ b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/AtlasTheme.kt
@@ -23,6 +23,8 @@ data class AtlasColors(
val mutedPlum: Color,
val atlasBlue: Color,
val atlasBluePressed: Color,
+ val seaGlass: Color,
+ val orangeAccent: Color,
val coral: Color,
val mustard: Color,
val success: Color,
@@ -42,19 +44,30 @@ data class AtlasTypography(
val timer: TextStyle,
)
+private val AtlasDeepGreen = Color(0xFF27635B)
+private val AtlasBlueGray = Color(0xFF778BA5)
+private val AtlasSeaGlass = Color(0xFFB7CFC8)
+private val AtlasShell = Color(0xFFF6E9E1)
+private val AtlasSand = Color(0xFFF1DCC7)
+private val AtlasPink = Color(0xFFF7BFBE)
+private val AtlasPeach = Color(0xFFE9AF8B)
+private val AtlasOrange = Color(0xFFE1814E)
+
private val LightAtlasColors = AtlasColors(
- parchment = Color(0xFFF7F0DF),
- paper = Color(0xFFFFFBF2),
- plum = Color(0xFF3E2447),
- mutedPlum = Color(0xFF69536F),
- atlasBlue = Color(0xFF17647E),
- atlasBluePressed = Color(0xFF0E4B60),
- coral = Color(0xFFE96E5B),
- mustard = Color(0xFFE3AD35),
- success = Color(0xFF147568),
- error = Color(0xFF9C3E32),
- outline = Color(0xFF8A748E),
- white = Color(0xFFFFFFFF),
+ parchment = AtlasShell,
+ paper = AtlasSand,
+ plum = AtlasDeepGreen,
+ mutedPlum = AtlasDeepGreen,
+ atlasBlue = AtlasDeepGreen,
+ atlasBluePressed = AtlasDeepGreen,
+ seaGlass = AtlasSeaGlass,
+ orangeAccent = AtlasOrange,
+ coral = AtlasPink,
+ mustard = AtlasPeach,
+ success = AtlasDeepGreen,
+ error = AtlasDeepGreen,
+ outline = AtlasBlueGray,
+ white = AtlasShell,
)
private fun atlasTypography(fontFamily: FontFamily) = AtlasTypography(
diff --git a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/HomeAndOptionsScreens.kt b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/HomeAndOptionsScreens.kt
index 1776015..86970a0 100644
--- a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/HomeAndOptionsScreens.kt
+++ b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/HomeAndOptionsScreens.kt
@@ -1,5 +1,7 @@
package app.bertsatlas.ui
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -8,10 +10,18 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.heading
+import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@@ -21,6 +31,8 @@ fun HomeScreen(
isFirstLaunch: Boolean,
streakDays: Int = 0,
progressSummary: String = "",
+ totalPoints: Int = 0,
+ hasStartedTrivia: Boolean = !isFirstLaunch,
onStart: () -> Unit,
onOptions: () -> Unit = {},
onProgress: () -> Unit = {},
@@ -31,9 +43,16 @@ fun HomeScreen(
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(14.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
) {
- BertIllustration(BertPose.Idle, contentDescription = "Bert smiles behind oversized glasses")
+ if (hasStartedTrivia) {
+ Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ PointsBadge(totalPoints, Modifier.weight(1f))
+ SettingsIconButton(onSettings)
+ }
+ }
+ Spacer(Modifier.height(if (hasStartedTrivia) 8.dp else 24.dp))
+ HomeBertIllustration(large = !hasStartedTrivia)
AtlasText(
"Bert's Atlas",
Modifier.semantics { heading() },
@@ -41,21 +60,85 @@ fun HomeScreen(
textAlign = TextAlign.Center,
)
AtlasText("Getting nerdier every day.", color = AtlasTheme.colors.mutedPlum, textAlign = TextAlign.Center)
- Spacer(Modifier.height(12.dp))
+ Spacer(Modifier.height(20.dp))
AtlasButton("Start", onStart, Modifier.fillMaxWidth().widthIn(max = 420.dp))
- if (!isFirstLaunch) {
+ if (hasStartedTrivia) {
AtlasButton("Options", onOptions, Modifier.fillMaxWidth().widthIn(max = 420.dp), AtlasButtonStyle.Secondary)
+ SectionGap()
AtlasCard(Modifier.widthIn(max = 420.dp), "Current streak: $streakDays days. $progressSummary") {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
- AtlasText("$streakDays", style = AtlasTheme.typography.title, color = AtlasTheme.colors.coral)
+ AtlasText("✦", style = AtlasTheme.typography.title, color = AtlasTheme.colors.coral)
Column(Modifier.weight(1f).padding(horizontal = 12.dp)) {
- AtlasText(if (streakDays == 1) "day curious" else "days curious", style = AtlasTheme.typography.label)
+ AtlasText(if (streakDays == 1) "1 curious day" else "$streakDays curious days", style = AtlasTheme.typography.label)
AtlasText(progressSummary, style = AtlasTheme.typography.detail, color = AtlasTheme.colors.mutedPlum)
}
- AtlasButton("Progress", onProgress, style = AtlasButtonStyle.Quiet)
}
}
- AtlasButton("Settings", onSettings, style = AtlasButtonStyle.Quiet)
+ AtlasButton("See your progress", onProgress, style = AtlasButtonStyle.Quiet)
+ }
+ }
+ }
+}
+
+@Composable
+private fun HomeBertIllustration(large: Boolean) {
+ val size = if (large) 270.dp else 200.dp
+ val colors = AtlasTheme.colors
+ androidx.compose.foundation.layout.Box(Modifier.size(size), contentAlignment = Alignment.Center) {
+ BertIllustration(BertPose.Idle, Modifier.fillMaxWidth(), contentDescription = "Bert smiles with a bright idea and a folded map")
+ Canvas(Modifier.fillMaxWidth().size(size)) {
+ val w = this.size.width
+ val h = this.size.height
+ val outline = 4.dp.toPx()
+ val map = Path().apply {
+ moveTo(w * .04f, h * .68f)
+ lineTo(w * .14f, h * .63f)
+ lineTo(w * .25f, h * .68f)
+ lineTo(w * .36f, h * .63f)
+ lineTo(w * .36f, h * .88f)
+ lineTo(w * .25f, h * .93f)
+ lineTo(w * .14f, h * .88f)
+ lineTo(w * .04f, h * .93f)
+ close()
+ }
+ drawPath(map, colors.paper)
+ drawPath(map, colors.plum, style = Stroke(outline, join = androidx.compose.ui.graphics.StrokeJoin.Round))
+ drawLine(colors.plum, Offset(w * .14f, h * .63f), Offset(w * .14f, h * .88f), outline)
+ drawLine(colors.plum, Offset(w * .25f, h * .68f), Offset(w * .25f, h * .93f), outline)
+ drawLine(colors.orangeAccent, Offset(w * .06f, h * .83f), Offset(w * .34f, h * .76f), outline * .7f, StrokeCap.Round)
+ }
+ }
+}
+
+@Composable
+private fun PointsBadge(points: Int, modifier: Modifier = Modifier) {
+ val colors = AtlasTheme.colors
+ Row(modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Canvas(Modifier.size(24.dp)) {
+ drawCircle(colors.mustard, size.minDimension * .40f)
+ drawCircle(colors.plum, size.minDimension * .40f, style = Stroke(2.4.dp.toPx()))
+ drawCircle(colors.plum, size.minDimension * .10f)
+ }
+ AtlasText(points.toString(), style = AtlasTheme.typography.heading, color = colors.plum)
+ }
+}
+
+@Composable
+private fun SettingsIconButton(onClick: () -> Unit) {
+ val color = AtlasTheme.colors.plum
+ androidx.compose.foundation.layout.Box(
+ Modifier.size(48.dp).semantics { role = Role.Button; contentDescription = "Open settings" }.clickable(onClick = onClick),
+ contentAlignment = Alignment.Center,
+ ) {
+ Canvas(Modifier.size(26.dp)) {
+ val center = Offset(size.width / 2, size.height / 2)
+ drawCircle(color, size.minDimension * .27f, center, style = Stroke(3.dp.toPx()))
+ drawCircle(color, size.minDimension * .07f, center)
+ repeat(8) { index ->
+ val angle = index * kotlin.math.PI.toFloat() / 4f
+ val start = Offset(center.x + kotlin.math.cos(angle) * size.width * .31f, center.y + kotlin.math.sin(angle) * size.height * .31f)
+ val end = Offset(center.x + kotlin.math.cos(angle) * size.width * .43f, center.y + kotlin.math.sin(angle) * size.height * .43f)
+ drawLine(color, start, end, 3.dp.toPx(), StrokeCap.Round)
}
}
}
@@ -120,7 +203,7 @@ fun RecoveryScreen(
AtlasText(questionSummary, style = AtlasTheme.typography.detail, color = AtlasTheme.colors.mutedPlum)
}
AtlasButton("Resume run", onResume, Modifier.fillMaxWidth())
- AtlasButton("Leave this run", onAbandon, Modifier.fillMaxWidth(), AtlasButtonStyle.Quiet)
+ AtlasButton("Leave this trivia run", onAbandon, Modifier.fillMaxWidth(), AtlasButtonStyle.Quiet)
AtlasText("Answered questions still count toward what Bert revisits next.", style = AtlasTheme.typography.detail, color = AtlasTheme.colors.mutedPlum, textAlign = TextAlign.Center)
}
}
diff --git a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/QuizScreens.kt b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/QuizScreens.kt
index b64fdfe..9fa6cc0 100644
--- a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/QuizScreens.kt
+++ b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/QuizScreens.kt
@@ -7,7 +7,9 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
@@ -31,12 +33,12 @@ fun QuizScreen(
prompt: String,
imageDescription: String?,
score: Int,
- sessionTime: String,
questionTime: String,
timerExpired: Boolean,
choices: List = emptyList(),
letterSlots: List = emptyList(),
letterTiles: List = emptyList(),
+ appliedHintMessages: List = emptyList(),
hintAvailable: Boolean,
unlimited: Boolean,
canSubmit: Boolean = false,
@@ -50,46 +52,46 @@ fun QuizScreen(
onLeave: () -> Unit,
modifier: Modifier = Modifier,
promptFlag: FlagUi? = null,
+ runProgress: Float? = null,
+ runProgressDescription: String = "Trivia progress",
+ overtimeMessage: String = "Time's up. Keep going for half points",
) {
- AtlasScreen(modifier) {
- Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(14.dp)) {
- BoxWithConstraints(Modifier.fillMaxWidth()) {
- if (maxWidth < 480.dp) {
- Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
- AtlasButton("Leave", onLeave, style = AtlasButtonStyle.Quiet)
- Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- QuizMetric("Score", score.toString(), Modifier.weight(1f))
- QuizMetric(if (unlimited) "Elapsed" else "Run", sessionTime, Modifier.weight(1f))
- QuizMetric("Question", questionTime, Modifier.weight(1f), warning = timerExpired)
- }
- }
- } else {
- Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- AtlasButton("Leave", onLeave, style = AtlasButtonStyle.Quiet)
- QuizMetric("Score", score.toString(), Modifier.weight(1f))
- QuizMetric(if (unlimited) "Elapsed" else "Run", sessionTime, Modifier.weight(1f))
- QuizMetric("Question", questionTime, Modifier.weight(1f), warning = timerExpired)
- }
- }
+ AtlasScreen(modifier, contentPadding = PaddingValues(0.dp)) {
+ Column(Modifier.fillMaxWidth()) {
+ runProgress?.let { SubtleRunProgress(it, runProgressDescription) }
+ Column(
+ Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Row(Modifier.fillMaxWidth().heightIn(min = 56.dp), verticalAlignment = Alignment.CenterVertically) {
+ QuizMetric("Score", score.toString(), Modifier.weight(1f))
+ QuizMetric("Question", questionTime, Modifier.weight(1f), warning = timerExpired)
+ AtlasExitButton(onLeave)
}
if (timerExpired) {
- AtlasText("Overtime · you can still answer for half points", color = AtlasTheme.colors.error, style = AtlasTheme.typography.label)
+ AtlasText(overtimeMessage, Modifier.fillMaxWidth(), color = AtlasTheme.colors.error, style = AtlasTheme.typography.label, textAlign = TextAlign.Center)
}
if (promptFlag != null || imageDescription != null) {
- AtlasCard(contentDescription = imageDescription ?: "Flag of ${promptFlag?.countryName}") {
- Box(Modifier.fillMaxWidth().heightIn(min = 116.dp), contentAlignment = Alignment.Center) {
+ Box(
+ Modifier.fillMaxWidth().heightIn(min = 148.dp)
+ .semantics { contentDescription = imageDescription ?: "Flag to identify" },
+ contentAlignment = Alignment.Center,
+ ) {
+ Box(Modifier.width(230.dp).padding(8.dp), contentAlignment = Alignment.Center) {
if (promptFlag != null) {
- FlagImage(promptFlag, Modifier.fillMaxWidth().heightIn(min = 116.dp))
+ FlagImage(promptFlag, Modifier.fillMaxWidth(), accessible = false, showFallbackLabel = false)
} else {
AtlasText("⚑", style = AtlasTheme.typography.display, color = AtlasTheme.colors.atlasBlue)
}
}
- imageDescription?.let {
- AtlasText(it, Modifier.fillMaxWidth(), style = AtlasTheme.typography.detail, color = AtlasTheme.colors.mutedPlum, textAlign = TextAlign.Center)
- }
}
}
- AtlasHeading(prompt)
+ AtlasHeading(prompt, Modifier.fillMaxWidth(), centered = true)
+ appliedHintMessages.forEach { message ->
+ AtlasCard(contentDescription = "Applied hint: $message") {
+ AtlasText("Hint: $message", color = AtlasTheme.colors.atlasBlue)
+ }
+ }
if (choices.isNotEmpty()) ResponsiveChoices(choices, onChoice)
if (letterSlots.isNotEmpty() || letterTiles.isNotEmpty()) {
LetterBank(letterSlots, letterTiles, onLetter, onRemoveLetter)
@@ -99,21 +101,42 @@ fun QuizScreen(
}
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
- AtlasButton("Hint", onHint, Modifier.weight(1f), AtlasButtonStyle.Secondary, enabled = hintAvailable)
+ AtlasButton("Get a hint", onHint, Modifier.weight(1f), AtlasButtonStyle.Secondary, enabled = hintAvailable)
if (unlimited) AtlasButton("Finish", onFinish, Modifier.weight(1f), AtlasButtonStyle.Quiet)
}
+ }
}
}
}
@Composable
private fun QuizMetric(label: String, value: String, modifier: Modifier, warning: Boolean = false) {
- Column(modifier.semantics(mergeDescendants = true) { contentDescription = "$label: $value" }, horizontalAlignment = Alignment.CenterHorizontally) {
- AtlasText(value, style = AtlasTheme.typography.timer, color = if (warning) AtlasTheme.colors.error else AtlasTheme.colors.plum, textAlign = TextAlign.Center)
+ Column(
+ modifier.semantics(mergeDescendants = true) { contentDescription = "$label: $value" },
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ AtlasText(
+ value,
+ style = AtlasTheme.typography.timer,
+ color = if (warning) AtlasTheme.colors.error else AtlasTheme.colors.plum,
+ textAlign = TextAlign.Center,
+ )
AtlasText(label, style = AtlasTheme.typography.detail, color = AtlasTheme.colors.mutedPlum, textAlign = TextAlign.Center)
}
}
+@Composable
+private fun SubtleRunProgress(progress: Float, description: String) {
+ val value = progress.coerceIn(0f, 1f)
+ Box(
+ Modifier.fillMaxWidth().heightIn(min = 4.dp)
+ .background(AtlasTheme.colors.outline.copy(alpha = .14f), RoundedCornerShape(4.dp))
+ .semantics { contentDescription = description; stateDescription = "${(value * 100).toInt()} percent" },
+ ) {
+ Box(Modifier.fillMaxWidth(value).heightIn(min = 4.dp).background(AtlasTheme.colors.coral, RoundedCornerShape(4.dp)))
+ }
+}
+
@Composable
private fun ResponsiveChoices(choices: List, onChoice: (String) -> Unit) {
BoxWithConstraints(Modifier.fillMaxWidth()) {
@@ -144,27 +167,51 @@ private fun LetterBank(
val slot = slots[index]
val label = slot.character ?: "blank"
Box(
- Modifier.size(52.dp).background(AtlasTheme.colors.paper, RoundedCornerShape(12.dp))
- .border(2.dp, AtlasTheme.colors.atlasBlue, RoundedCornerShape(12.dp))
+ Modifier.size(48.dp)
.semantics { contentDescription = "Answer position ${slot.position + 1}: $label" }
.clickable(enabled = slot.tileId != null && !slot.fixed) { slot.tileId?.let(onRemoveLetter) },
contentAlignment = Alignment.Center,
- ) { AtlasText(slot.character ?: "_", style = AtlasTheme.typography.heading) }
+ ) {
+ Box(
+ Modifier.size(38.dp)
+ .background(AtlasTheme.colors.paper, RoundedCornerShape(10.dp))
+ .border(2.dp, AtlasTheme.colors.atlasBlue, RoundedCornerShape(10.dp)),
+ contentAlignment = Alignment.Center,
+ ) {
+ AtlasText(slot.character ?: "_", style = AtlasTheme.typography.heading)
+ }
+ }
}
WrappingCells(tiles.size) { index ->
val tile = tiles[index]
val available = tile.enabled && tile.placedSlot == null
Box(
- Modifier.size(52.dp).background(if (available) AtlasTheme.colors.mustard.copy(alpha = .3f) else AtlasTheme.colors.outline.copy(alpha = .12f), RoundedCornerShape(12.dp))
- .border(1.dp, AtlasTheme.colors.outline, RoundedCornerShape(12.dp))
+ Modifier.size(48.dp)
.semantics {
role = Role.Button
contentDescription = "Letter ${tile.letter}"
- stateDescription = if (available) "Available" else "Placed"
+ stateDescription = when {
+ available -> "Available"
+ !tile.enabled -> "Removed"
+ else -> "Placed"
+ }
}
.clickable(enabled = available) { onLetter(tile.id) },
contentAlignment = Alignment.Center,
- ) { AtlasText(tile.letter, style = AtlasTheme.typography.label) }
+ ) {
+ Box(
+ Modifier.size(44.dp)
+ .background(
+ if (available) AtlasTheme.colors.mustard.copy(alpha = .3f)
+ else AtlasTheme.colors.outline.copy(alpha = .12f),
+ RoundedCornerShape(11.dp),
+ )
+ .border(1.dp, AtlasTheme.colors.outline, RoundedCornerShape(11.dp)),
+ contentAlignment = Alignment.Center,
+ ) {
+ AtlasText(tile.letter, style = AtlasTheme.typography.label)
+ }
+ }
}
}
}
@@ -172,7 +219,7 @@ private fun LetterBank(
@Composable
private fun WrappingCells(count: Int, content: @Composable (Int) -> Unit) {
BoxWithConstraints(Modifier.fillMaxWidth()) {
- val columns = (maxWidth / 60.dp).toInt().coerceAtLeast(1)
+ val columns = (maxWidth / 56.dp).toInt().coerceAtLeast(1)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
(0 until count).chunked(columns).forEach { indices ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
@@ -190,15 +237,17 @@ fun HintChooserScreen(
onChooseHint: (String) -> Unit,
onCancel: () -> Unit,
modifier: Modifier = Modifier,
+ introText: String = "Choose the clue that sounds most useful.",
+ chargedHintLabel: String = "−25 points",
) {
AtlasScreen(modifier) {
- Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(14.dp), horizontalAlignment = Alignment.CenterHorizontally) {
+ Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
ScreenTopBar("Choose a hint", onCancel)
- BertIllustration(BertPose.Hint)
+ BertIllustration(BertPose.Hint, Modifier.size(150.dp))
AtlasHeading("A little nudge?", Modifier.fillMaxWidth(), centered = true)
- AtlasText("Every hint costs up to 25 points. Your score can never drop below zero.", textAlign = TextAlign.Center)
- AtlasCard(contentDescription = "Current score $score. Score after hint ${maxOf(0, score - 25)}") {
- StatRow(listOf(StatUi("Now", score.toString()), StatUi("After hint", maxOf(0, score - 25).toString())))
+ AtlasText(introText, textAlign = TextAlign.Center, color = AtlasTheme.colors.mutedPlum)
+ if (score > 0) {
+ AtlasText(chargedHintLabel, style = AtlasTheme.typography.label, color = AtlasTheme.colors.outline, textAlign = TextAlign.Center)
}
hints.forEach { AtlasChoice(it, onChooseHint) }
}
@@ -217,12 +266,14 @@ fun FeedbackScreen(
autoAdvanceMessage: String?,
onContinue: () -> Unit,
modifier: Modifier = Modifier,
+ continueLabel: String = "Continue",
) {
val pose = when { overtime && correct -> BertPose.Overtime; correct -> BertPose.Correct; else -> BertPose.Incorrect }
val status = when { overtime && correct -> "Overtime correct"; correct -> "Correct"; else -> "Incorrect" }
AtlasScreen(modifier) {
- Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(14.dp)) {
- BertIllustration(pose, contentDescription = "Bert reacts: $status")
+ Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Spacer(Modifier.heightIn(min = 8.dp))
+ BertIllustration(pose, Modifier.size(190.dp), contentDescription = "Bert reacts: $status")
AtlasHeading(heading, Modifier.fillMaxWidth(), centered = true)
AtlasText(
if (correct) "+$pointsAwarded points" else "No points this time",
@@ -235,7 +286,7 @@ fun FeedbackScreen(
explanation?.let { AtlasText(it) }
}
if (overtime && correct) AtlasText("The clock gave up. You didn't. Half points.", textAlign = TextAlign.Center)
- AtlasButton("Continue", onContinue, Modifier.fillMaxWidth())
+ AtlasButton(continueLabel, onContinue, Modifier.fillMaxWidth())
autoAdvanceMessage?.let { AtlasText(it, style = AtlasTheme.typography.detail, color = AtlasTheme.colors.mutedPlum, textAlign = TextAlign.Center) }
}
}
@@ -249,14 +300,39 @@ fun FunFactScreen(
artDescription: String? = null,
onContinue: () -> Unit,
modifier: Modifier = Modifier,
+ kicker: String = "Fun fact about",
+ continueLabel: String = "Continue",
) {
AtlasScreen(modifier) {
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) {
BertIllustration(BertPose.Fact, contentDescription = "Bert has found an oddly specific fact")
- AtlasText("FIELD NOTE · $relatedTo", style = AtlasTheme.typography.label, color = AtlasTheme.colors.atlasBlue)
+ AtlasText("${kicker} $relatedTo", style = AtlasTheme.typography.label, color = AtlasTheme.colors.atlasBlue)
AtlasHeading(heading, Modifier.fillMaxWidth(), centered = true)
AtlasCard(contentDescription = artDescription) { AtlasText(fact, style = AtlasTheme.typography.heading, textAlign = TextAlign.Center) }
- AtlasButton("Back to the atlas", onContinue, Modifier.fillMaxWidth())
+ AtlasButton(continueLabel, onContinue, Modifier.fillMaxWidth())
+ }
+ }
+}
+
+@Composable
+fun LeaveQuizConfirmationScreen(
+ onCancel: () -> Unit,
+ onLeave: () -> Unit,
+ modifier: Modifier = Modifier,
+ title: String = "Leave this trivia run?",
+ body: String = "Your answered questions will stay in your practice progress. Ending now won't count toward your personal best or daily streak.",
+) {
+ AtlasScreen(modifier, scrollable = false) {
+ Column(
+ Modifier.fillMaxWidth().align(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ BertIllustration(BertPose.Hint, Modifier.size(160.dp), contentDescription = "Bert keeps your practice progress safe")
+ AtlasHeading(title, Modifier.fillMaxWidth(), centered = true)
+ AtlasText(body, Modifier.fillMaxWidth(), color = AtlasTheme.colors.mutedPlum, textAlign = TextAlign.Center)
+ AtlasButton("Keep playing", onCancel, Modifier.fillMaxWidth())
+ AtlasButton("Leave trivia", onLeave, Modifier.fillMaxWidth(), AtlasButtonStyle.Destructive)
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/ResultsAndProgressScreens.kt b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/ResultsAndProgressScreens.kt
index 44367c8..13818b6 100644
--- a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/ResultsAndProgressScreens.kt
+++ b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/ResultsAndProgressScreens.kt
@@ -31,11 +31,28 @@ fun ResultsScreen(
onPlayAgain: () -> Unit,
onHome: () -> Unit,
modifier: Modifier = Modifier,
+ abandoned: Boolean = false,
) {
AtlasScreen(modifier) {
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) {
- BertIllustration(BertPose.Results, contentDescription = "Bert celebrates a completed atlas run")
- AtlasHeading(if (newPersonalBest) "New personal best" else "Run complete", Modifier.fillMaxWidth(), centered = true)
+ BertIllustration(if (abandoned) BertPose.Hint else BertPose.Results, contentDescription = if (abandoned) "Bert keeps your practice progress" else "Bert celebrates completed trivia")
+ AtlasHeading(
+ when {
+ abandoned -> "Trivia ended"
+ newPersonalBest -> "New personal best"
+ else -> "Trivia complete"
+ },
+ Modifier.fillMaxWidth(),
+ centered = true,
+ )
+ if (abandoned) {
+ AtlasText(
+ "Your answers still shaped what you'll practise next. This trivia run didn't count toward your personal best or daily streak.",
+ Modifier.fillMaxWidth(),
+ color = AtlasTheme.colors.mutedPlum,
+ textAlign = TextAlign.Center,
+ )
+ }
AtlasText(score.toString(), style = AtlasTheme.typography.display, color = AtlasTheme.colors.atlasBlue)
AtlasText("points", color = AtlasTheme.colors.mutedPlum)
StatRow(
@@ -46,7 +63,7 @@ fun ResultsScreen(
),
)
AtlasCard {
- AtlasText("Run notes", style = AtlasTheme.typography.heading)
+ AtlasText("Highlights", style = AtlasTheme.typography.heading)
AtlasText("Average response · $averageResponseTime")
AtlasText("New or improved knowledge · $masteryImproved")
}
@@ -56,7 +73,7 @@ fun ResultsScreen(
AtlasText(revisitCountries.joinToString(" · "), color = AtlasTheme.colors.mutedPlum)
}
}
- AtlasButton("Play again", onPlayAgain, Modifier.fillMaxWidth())
+ AtlasButton(if (abandoned) "Start new trivia" else "Play again", onPlayAgain, Modifier.fillMaxWidth())
AtlasButton("Home", onHome, Modifier.fillMaxWidth(), AtlasButtonStyle.Secondary)
}
}
diff --git a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/SettingsScreens.kt b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/SettingsScreens.kt
index 12f18db..844c6dd 100644
--- a/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/SettingsScreens.kt
+++ b/composeApp/src/commonMain/kotlin/app/bertsatlas/ui/SettingsScreens.kt
@@ -33,12 +33,15 @@ fun SettingsScreen(
AtlasScreen(modifier) {
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
ScreenTopBar("Settings", onBack)
+ SectionGap()
AtlasCard {
settings.forEach { AtlasToggle(it, onToggle) }
}
- AtlasButton("Privacy", onPrivacy, Modifier.fillMaxWidth(), AtlasButtonStyle.Secondary)
- AtlasButton("Credits and licenses", onCredits, Modifier.fillMaxWidth(), AtlasButtonStyle.Secondary)
- AtlasButton("Reset local data", onReset, Modifier.fillMaxWidth(), AtlasButtonStyle.Destructive)
+ AtlasCard {
+ AtlasLinkRow("Privacy", onPrivacy)
+ AtlasLinkRow("Credits and licenses", onCredits)
+ AtlasLinkRow("Reset local data", onReset, destructive = true)
+ }
AtlasText(
"Bert's Atlas $version",
Modifier.fillMaxWidth()
@@ -84,7 +87,7 @@ fun PrivacyScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
title = "Privacy",
paragraphs = listOf(
"Bert's Atlas works entirely offline. It does not ask for an account or personal information.",
- "Your quiz history, progress, streaks, stamps, and preferences stay on this device. The app has no advertising, analytics, remote configuration, or product telemetry.",
+ "Your trivia history, progress, streaks, stamps, and preferences stay on this device. The app has no advertising, analytics, remote configuration, or product telemetry.",
"Clearing app data or deleting the app may permanently remove your progress. Operating-system and store diagnostics are controlled by your platform settings.",
),
onBack = onBack,
diff --git a/content/assets-and-sources.md b/content/assets-and-sources.md
index 8e7a674..0d4bfb7 100644
--- a/content/assets-and-sources.md
+++ b/content/assets-and-sources.md
@@ -16,6 +16,10 @@ The initial draft was normalized from datasets/country-codes at commit `6a595f1a
The aggregate commit is a reproducibility aid, not an editorial signoff. Capital arrangements with ambiguity or recent change are explicitly marked and removed from one-answer question eligibility.
+Land-border draft facts use the REST Countries V4 data pinned at commit `43f6260db10c54df9972e6e554ee14c2e7c85ef5`. These are concise fallback copy, not inferred lessons about UN regions. Editors must check disputed boundaries and whether overseas territories affect a claim before approval.
+
+The early-play pool has twelve more varied draft facts sourced directly from public institutions: Geoscience Australia, Brazil's IBGE, the governments of Canada, India, Japan, Mexico, South Africa, New Zealand, Nepal and the Netherlands, Royal Museums Greenwich, and the archived CIA World Factbook. Their exact page URLs and retrieval snapshots are recorded in the catalog. A source link proves provenance; it does not constitute fact or copy approval.
+
Primary review locations:
- United Nations member states: https://www.un.org/en/about-us/member-states
diff --git a/content/brand/berts-atlas-mark.svg b/content/brand/berts-atlas-mark.svg
new file mode 100644
index 0000000..08af621
--- /dev/null
+++ b/content/brand/berts-atlas-mark.svg
@@ -0,0 +1,14 @@
+
diff --git a/content/brand/berts-atlas-splash.svg b/content/brand/berts-atlas-splash.svg
new file mode 100644
index 0000000..e31cb2b
--- /dev/null
+++ b/content/brand/berts-atlas-splash.svg
@@ -0,0 +1,7 @@
+
diff --git a/content/voice-and-feedback-recommendations.md b/content/voice-and-feedback-recommendations.md
new file mode 100644
index 0000000..65c3691
--- /dev/null
+++ b/content/voice-and-feedback-recommendations.md
@@ -0,0 +1,24 @@
+# Feedback voice recommendations
+
+Duolingo's public product writing consistently puts the answer first, keeps the immediate interaction small, offers explanation as a second layer, and makes the end of a session encouraging while still naming what needs practice. That pattern is useful here, but Bert's Atlas should keep an original travel-and-curiosity voice rather than imitate Duolingo's phrases.
+
+Research basis:
+
+- Duolingo, “Explain My Answer is now free for all learners”: https://blog.duolingo.com/explain-my-answer-now-free/
+- Duolingo, “Introducing Duolingo Flashcards”: https://blog.duolingo.com/duolingo-flashcards/
+- Duolingo 101: https://blog.duolingo.com/duolingo-101-how-to-learn-a-language-on-duolingo/
+- Duolingo, “How we improved the streak”: https://blog.duolingo.com/improving-the-streak/
+
+Recommended original patterns:
+
+- Correct: “Flag filed. Nice work.”
+- Incorrect: “That was {answer}. Look for {short distinction}.”
+- Overtime: “Time's up. You still found it.”
+- Continent hint: “Map clue: {continent}.”
+- Choice hint: “Two decoys are leaving.”
+- Fact hint: “Bert's clue: {answer-safe fact}.”
+- Results: “Atlas packed: {score} points · {accuracy}%.”
+- Review: “Worth another look: {countries}.”
+- Streak: “{days} curious days in a row.”
+
+Keep feedback to one idea per line. Name the correct answer after a miss, but never include it in a pre-answer clue. Avoid shame, exaggerated celebration, and claims that a streak proves mastery.
diff --git a/domain/src/commonMain/kotlin/app/bertsatlas/domain/Contracts.kt b/domain/src/commonMain/kotlin/app/bertsatlas/domain/Contracts.kt
index 3707f46..1e5e662 100644
--- a/domain/src/commonMain/kotlin/app/bertsatlas/domain/Contracts.kt
+++ b/domain/src/commonMain/kotlin/app/bertsatlas/domain/Contracts.kt
@@ -55,8 +55,8 @@ data class PlayerSettings(
)
interface LaunchPreferences {
- suspend fun hasLaunchedBefore(): Boolean
- suspend fun markLaunched()
+ suspend fun hasStartedTrivia(): Boolean
+ suspend fun markTriviaStarted()
}
interface QuizConfigurationPreferences {
diff --git a/domain/src/commonMain/kotlin/app/bertsatlas/domain/QuestionSelector.kt b/domain/src/commonMain/kotlin/app/bertsatlas/domain/QuestionSelector.kt
index 145c6eb..6f48ca6 100644
--- a/domain/src/commonMain/kotlin/app/bertsatlas/domain/QuestionSelector.kt
+++ b/domain/src/commonMain/kotlin/app/bertsatlas/domain/QuestionSelector.kt
@@ -27,10 +27,22 @@ class QuestionSelector(
val topicQuestions = questions.filter { matchesTopic(it, topic) }
require(topicQuestions.isNotEmpty()) { "No questions are available for $topic" }
- val withoutRecentFive = topicQuestions.filter { it.id !in recentQuestionIds.takeLast(5) }
- val withoutImmediateRepeat = topicQuestions.filter { it.id != recentQuestionIds.lastOrNull() }
+ val priorQuestionIds = recentQuestionIds.takeLast(RECENT_QUESTION_WINDOW).toSet()
+ val priorQuestions = questions.filter { it.id in priorQuestionIds }
+ val immediateQuestion = questions.firstOrNull { it.id == recentQuestionIds.lastOrNull() }
+ val withoutRecentConcepts = topicQuestions.filter { candidate ->
+ candidate.id !in priorQuestionIds && priorQuestions.none { recent -> candidate.repeatsConceptFrom(recent) }
+ }
+ val withoutImmediateConcept = topicQuestions.filter { candidate ->
+ candidate.id != immediateQuestion?.id &&
+ (immediateQuestion == null || !candidate.repeatsConceptFrom(immediateQuestion))
+ }
+ val withoutPriorQuestions = topicQuestions.filter { it.id !in priorQuestionIds }
+ val withoutImmediateRepeat = topicQuestions.filter { it.id != immediateQuestion?.id }
val candidates = when {
- withoutRecentFive.isNotEmpty() -> withoutRecentFive
+ withoutRecentConcepts.isNotEmpty() -> withoutRecentConcepts
+ withoutImmediateConcept.isNotEmpty() -> withoutImmediateConcept
+ withoutPriorQuestions.isNotEmpty() -> withoutPriorQuestions
withoutImmediateRepeat.isNotEmpty() -> withoutImmediateRepeat
else -> topicQuestions
}
@@ -41,6 +53,24 @@ class QuestionSelector(
return SelectedQuestion(weighted[random.value], random.nextState)
}
+ private fun Question.repeatsConceptFrom(other: Question): Boolean {
+ if (countryIds.none { it in other.countryIds }) return false
+ val family = skill.selectionFamily()
+ return family != SelectionFamily.Mixed && family == other.skill.selectionFamily()
+ }
+
+ private fun Skill.selectionFamily(): SelectionFamily = when (this) {
+ Skill.FlagRecognition,
+ Skill.CountryToFlag,
+ -> SelectionFamily.Flag
+ Skill.CapitalRecall,
+ Skill.CapitalRecognition,
+ -> SelectionFamily.Capital
+ Skill.MixedGeography,
+ Skill.FlagSymbolism,
+ -> SelectionFamily.Mixed
+ }
+
private fun matchesTopic(question: Question, topic: QuizTopic): Boolean = when (topic) {
QuizTopic.MostlyFlags -> true
QuizTopic.Capitals -> question.skill == Skill.CapitalRecall ||
@@ -71,14 +101,14 @@ class QuestionSelector(
}
val topicWeight = when (topic) {
QuizTopic.MostlyFlags -> when (question.skill) {
- Skill.FlagRecognition -> 4
- Skill.CountryToFlag -> 2
+ Skill.FlagRecognition -> 8
+ Skill.CountryToFlag -> 6
Skill.CapitalRecall,
Skill.CapitalRecognition,
- -> 8
+ -> 1
Skill.MixedGeography,
Skill.FlagSymbolism,
- -> 8
+ -> 2
}
QuizTopic.Capitals -> 1
QuizTopic.MixedGeography -> when (question.skill) {
@@ -96,3 +126,11 @@ class QuestionSelector(
return (difficultyWeight + reviewWeight) * topicWeight
}
}
+
+private const val RECENT_QUESTION_WINDOW = 5
+
+private enum class SelectionFamily {
+ Flag,
+ Capital,
+ Mixed,
+}
diff --git a/domain/src/commonMain/kotlin/app/bertsatlas/domain/QuizEngine.kt b/domain/src/commonMain/kotlin/app/bertsatlas/domain/QuizEngine.kt
index b093f80..68f1764 100644
--- a/domain/src/commonMain/kotlin/app/bertsatlas/domain/QuizEngine.kt
+++ b/domain/src/commonMain/kotlin/app/bertsatlas/domain/QuizEngine.kt
@@ -107,13 +107,20 @@ class QuizEngine(
require(state.sessionRemainingMillis == null || state.sessionRemainingMillis >= 0) {
"A snapshot has an invalid session timer"
}
+ val completionPending = state.runCompletionPending ||
+ state.sessionExpired ||
+ (state.configuration.duration != QuizDuration.Unlimited && state.sessionRemainingMillis == 0L)
if (state.phase == RunPhase.Paused) {
require(state.pausedFromPhase in PAUSABLE_PHASES) { "A paused snapshot has no resumable phase" }
- return state.copy(tickingSinceMillis = null)
+ return state.copy(
+ runCompletionPending = completionPending,
+ tickingSinceMillis = null,
+ )
}
return state.copy(
phase = RunPhase.Paused,
pausedFromPhase = state.phase,
+ runCompletionPending = completionPending,
tickingSinceMillis = null,
)
}
@@ -130,7 +137,7 @@ class QuizEngine(
} else {
0
}
- val hinted = timed.activeQuestion.hint != null
+ val hinted = timed.activeQuestion.appliedHints.isNotEmpty()
val overtime = evaluation.correct && timed.activeQuestion.questionRemainingMillis == 0L
val attempt = Attempt(
id = "${timed.runId}:${timed.attempts.size + 1}",
@@ -147,10 +154,11 @@ class QuizEngine(
pointsAwarded = points,
answeredAtEpochMillis = wallClock.nowEpochMillis(),
localEpochDay = localDateProvider.currentEpochDay(),
- hintType = timed.activeQuestion.hint?.type,
+ hintType = timed.activeQuestion.appliedHints.lastOrNull()?.type,
hintPointsDeducted = timed.activeQuestion.hintPointsDeducted,
questionWindowMillis = timed.activeQuestion.questionWindowMillis,
questionRemainingMillis = timed.activeQuestion.questionRemainingMillis,
+ hintTypes = timed.activeQuestion.usedHintTypes,
)
val correctStreak = if (evaluation.correct) timed.currentCorrectStreak + 1 else 0
return timed.copy(
@@ -180,8 +188,9 @@ class QuizEngine(
fun openHintChooser(state: QuizRunState): QuizRunState {
requireAnswering(state)
- require(state.activeQuestion.hint == null) { "A hint has already been used for this question" }
- require(state.activeQuestion.question.supportedHints.isNotEmpty()) { "This question does not offer hints" }
+ require(state.activeQuestion.question.supportedHints.any { it !in state.activeQuestion.usedHintTypes }) {
+ "Every available hint has already been used for this question"
+ }
return freezeClocks(state).copy(phase = RunPhase.HintChooser)
}
@@ -189,11 +198,14 @@ class QuizEngine(
requireActive(state)
require(state.phase == RunPhase.HintChooser) { "Hints can only be chosen from the hint chooser" }
require(type in state.activeQuestion.question.supportedHints) { "This question does not support $type" }
- require(state.activeQuestion.hint == null) { "A hint has already been used for this question" }
+ require(type !in state.activeQuestion.usedHintTypes) { "$type has already been used for this question" }
val scoreAfterHint = Scoring.deductHint(state.score)
- val hintedQuestion = applyHint(state.activeQuestion, type).copy(
- hintPointsDeducted = state.score - scoreAfterHint,
+ val appliedQuestion = applyHint(state.activeQuestion, type)
+ val chosenHint = requireNotNull(appliedQuestion.hint)
+ val hintedQuestion = appliedQuestion.copy(
+ usedHints = state.activeQuestion.appliedHints + chosenHint,
+ hintPointsDeducted = state.activeQuestion.hintPointsDeducted + state.score - scoreAfterHint,
)
return state.copy(
phase = RunPhase.Answering,
@@ -207,8 +219,9 @@ class QuizEngine(
requireActive(state)
require(state.phase == RunPhase.Feedback) { "Feedback is not visible" }
if (state.runAbandonmentPending) return abandonRun(state)
- if (state.runCompletionPending || shouldShowScheduledFact(state)) {
- return showRelatedFact(state, finalFact = state.runCompletionPending, mastery = mastery)
+ val timedRunComplete = state.runCompletionPending || state.sessionExpired
+ if (timedRunComplete || shouldShowScheduledFact(state)) {
+ return showRelatedFact(state, finalFact = timedRunComplete, mastery = mastery)
}
return moveToNextQuestion(state, mastery)
}
@@ -292,6 +305,23 @@ class QuizEngine(
}
}
+ fun finishExpiredRun(state: QuizRunState): QuizRunState {
+ requireActive(state)
+ require(state.configuration.duration != QuizDuration.Unlimited) { "Unlimited trivia does not expire" }
+ val frozen = freezeClocks(state)
+ require(frozen.sessionExpired || frozen.sessionRemainingMillis == 0L) { "Trivia time has not expired" }
+ return frozen.copy(
+ phase = RunPhase.Completed,
+ status = RunStatus.Completed,
+ feedback = null,
+ visibleFact = null,
+ finalFactPending = false,
+ runCompletionPending = false,
+ runAbandonmentPending = false,
+ tickingSinceMillis = null,
+ )
+ }
+
fun abandonRun(state: QuizRunState): QuizRunState {
requireActive(state)
return freezeClocks(state).copy(
@@ -329,7 +359,11 @@ class QuizEngine(
requireAnswering(state)
val timed = advanceClocks(state)
require(timed.activeQuestion.question is Question.LetterBank) { "The active question is not a letter bank" }
- require(timed.activeQuestion.placements.any { it.slotIndex == slotIndex }) { "The answer slot is empty" }
+ val placement = timed.activeQuestion.placements.firstOrNull { it.slotIndex == slotIndex }
+ ?: error("The answer slot is empty")
+ require(placement.tileId !in timed.activeQuestion.revealedTileIds()) {
+ "A revealed letter cannot be removed"
+ }
return timed.copy(
activeQuestion = timed.activeQuestion.copy(
placements = timed.activeQuestion.placements.filterNot { it.slotIndex == slotIndex },
@@ -341,7 +375,12 @@ class QuizEngine(
requireAnswering(state)
val timed = advanceClocks(state)
require(timed.activeQuestion.question is Question.LetterBank) { "The active question is not a letter bank" }
- return timed.copy(activeQuestion = timed.activeQuestion.copy(placements = emptyList()))
+ val revealedTileIds = timed.activeQuestion.revealedTileIds()
+ return timed.copy(
+ activeQuestion = timed.activeQuestion.copy(
+ placements = timed.activeQuestion.placements.filter { it.tileId in revealedTileIds },
+ ),
+ )
}
fun submitLetters(state: QuizRunState): QuizRunState {
@@ -356,6 +395,15 @@ class QuizEngine(
fun runResult(state: QuizRunState, improvedMasteryItems: Int): RunResult {
require(state.status == RunStatus.Completed) { "Only a completed run has results" }
+ return buildRunSummary(state, improvedMasteryItems)
+ }
+
+ fun abandonedRunSummary(state: QuizRunState, improvedMasteryItems: Int): RunResult {
+ require(state.status == RunStatus.Abandoned) { "Only an abandoned run has an abandoned summary" }
+ return buildRunSummary(state, improvedMasteryItems)
+ }
+
+ private fun buildRunSummary(state: QuizRunState, improvedMasteryItems: Int): RunResult {
val correct = state.attempts.count { it.correct }
val averageResponse = if (state.attempts.isEmpty()) 0 else {
state.attempts.sumOf { it.responseMillis } / state.attempts.size
@@ -374,6 +422,7 @@ class QuizEngine(
.flatMap { it.countryIds }
.distinct(),
completedAtEpochMillis = wallClock.nowEpochMillis(),
+ status = state.status,
)
}
@@ -540,6 +589,9 @@ class QuizEngine(
}
}
+ private fun ActiveQuestion.revealedTileIds(): Set =
+ appliedHints.mapNotNullTo(linkedSetOf()) { it.revealedTileId }
+
private fun correctAnswer(question: Question): String = when (question) {
is Question.MultipleChoice -> question.choices.first { it.id == question.correctChoiceId }.label
is Question.LetterBank -> question.displayAnswer
diff --git a/domain/src/commonMain/kotlin/app/bertsatlas/domain/Scoring.kt b/domain/src/commonMain/kotlin/app/bertsatlas/domain/Scoring.kt
index f200450..a19b2c0 100644
--- a/domain/src/commonMain/kotlin/app/bertsatlas/domain/Scoring.kt
+++ b/domain/src/commonMain/kotlin/app/bertsatlas/domain/Scoring.kt
@@ -1,8 +1,6 @@
package app.bertsatlas.domain
object Scoring {
- const val HINT_COST = 25
-
fun pointsForCorrectAnswer(remainingMillis: Long, responseWindowMillis: Long): Int {
require(responseWindowMillis > 0)
if (remainingMillis <= 0) return 50
@@ -10,5 +8,7 @@ object Scoring {
return (50 + speedPoints).coerceAtMost(100)
}
- fun deductHint(score: Int): Int = (score - HINT_COST).coerceAtLeast(0)
+ fun hintCost(score: Int): Int = score.coerceAtLeast(0) / 2
+
+ fun deductHint(score: Int): Int = score.coerceAtLeast(0) - hintCost(score)
}
diff --git a/domain/src/commonMain/kotlin/app/bertsatlas/domain/model/Progress.kt b/domain/src/commonMain/kotlin/app/bertsatlas/domain/model/Progress.kt
index 93ae206..aabc059 100644
--- a/domain/src/commonMain/kotlin/app/bertsatlas/domain/model/Progress.kt
+++ b/domain/src/commonMain/kotlin/app/bertsatlas/domain/model/Progress.kt
@@ -22,6 +22,7 @@ data class Attempt(
val hintPointsDeducted: Int = 0,
val questionWindowMillis: Long = 0,
val questionRemainingMillis: Long = 0,
+ val hintTypes: Set = emptySet(),
)
@Serializable
@@ -88,6 +89,7 @@ data class RunResult(
val improvedMasteryItems: Int,
val revisitCountryIds: List,
val completedAtEpochMillis: Long,
+ val status: RunStatus = RunStatus.Completed,
) {
val accuracyPercent: Int
get() = if (answeredQuestions == 0) 0 else (correctAnswers * 100) / answeredQuestions
diff --git a/domain/src/commonMain/kotlin/app/bertsatlas/domain/model/Run.kt b/domain/src/commonMain/kotlin/app/bertsatlas/domain/model/Run.kt
index b8e0f68..3c747ec 100644
--- a/domain/src/commonMain/kotlin/app/bertsatlas/domain/model/Run.kt
+++ b/domain/src/commonMain/kotlin/app/bertsatlas/domain/model/Run.kt
@@ -52,10 +52,17 @@ data class ActiveQuestion(
val questionRemainingMillis: Long,
val questionElapsedMillis: Long = 0,
val hint: HintPresentation? = null,
+ val usedHints: List = emptyList(),
val hintPointsDeducted: Int = 0,
val removedChoiceIds: Set = emptySet(),
val removedTileIds: Set = emptySet(),
-)
+) {
+ val appliedHints: List
+ get() = if (usedHints.isEmpty()) listOfNotNull(hint) else usedHints
+
+ val usedHintTypes: Set
+ get() = appliedHints.mapTo(linkedSetOf()) { it.type }
+}
@Serializable
data class QuizRunState(
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
index 95196b6..589f6ab 100644
--- a/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
09FED4B446BD58DF150C455A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = C6D2DD7FE2E0BF5DB2A96B4B /* PrivacyInfo.xcprivacy */; };
+ 22EA1CB33A17DCC6FC82406D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F543015C899423D4B01CEB2E /* Assets.xcassets */; };
307C2654CD5972A25FB7FD87 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668CECA83ACC03A83C85A623 /* iOSApp.swift */; };
8799EE5CA40A935F0C1BF0B6 /* ComposeApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE64B79D6154E4181215B83C /* ComposeApp.framework */; };
/* End PBXBuildFile section */
@@ -18,6 +19,7 @@
C117AD187833E2513BD2F553 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
C6D2DD7FE2E0BF5DB2A96B4B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
CE64B79D6154E4181215B83C /* ComposeApp.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ComposeApp.framework; path = ../composeApp/build/bin/iosSimulatorArm64/debugFramework/ComposeApp.framework; sourceTree = ""; };
+ F543015C899423D4B01CEB2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -35,6 +37,7 @@
1DB4B685CAEE8BEFEBBAEF94 /* iosApp */ = {
isa = PBXGroup;
children = (
+ F543015C899423D4B01CEB2E /* Assets.xcassets */,
C117AD187833E2513BD2F553 /* Info.plist */,
668CECA83ACC03A83C85A623 /* iOSApp.swift */,
C6D2DD7FE2E0BF5DB2A96B4B /* PrivacyInfo.xcprivacy */,
@@ -125,6 +128,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 22EA1CB33A17DCC6FC82406D /* Assets.xcassets in Resources */,
09FED4B446BD58DF150C455A /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png
new file mode 100644
index 0000000..ffb11eb
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..f22e10c
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images" : [
+ {
+ "filename" : "AppIcon-1024.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/LaunchBackground.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/LaunchBackground.colorset/Contents.json
new file mode 100644
index 0000000..032af09
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/LaunchBackground.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0xE1",
+ "green" : "0xE9",
+ "red" : "0xF6"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/Contents.json
new file mode 100644
index 0000000..65b63ff
--- /dev/null
+++ b/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "LaunchLogo.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "LaunchLogo@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "LaunchLogo@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/LaunchLogo.png b/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/LaunchLogo.png
new file mode 100644
index 0000000..2b46023
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/LaunchLogo.png differ
diff --git a/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/LaunchLogo@2x.png b/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/LaunchLogo@2x.png
new file mode 100644
index 0000000..7f1f39b
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/LaunchLogo@2x.png differ
diff --git a/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/LaunchLogo@3x.png b/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/LaunchLogo@3x.png
new file mode 100644
index 0000000..3d69c27
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/LaunchLogo.imageset/LaunchLogo@3x.png differ
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
index c7a1805..a13b28b 100644
--- a/iosApp/iosApp/Info.plist
+++ b/iosApp/iosApp/Info.plist
@@ -25,7 +25,14 @@
LSRequiresIPhoneOS
UILaunchScreen
-
+
+ UIColorName
+ LaunchBackground
+ UIImageName
+ LaunchLogo
+ UIImageRespectsSafeAreaInsets
+
+
UIApplicationSupportsIndirectInputEvents
UISupportedInterfaceOrientations
diff --git a/spec.md b/spec.md
index 688c837..04cd1df 100644
--- a/spec.md
+++ b/spec.md
@@ -102,25 +102,25 @@ The first screen contains:
`Start` is the only interactive choice on the first-launch screen. System accessibility and silent-mode preferences are honored without requiring setup.
-Tapping `Start` immediately begins a quiz with these defaults:
+Tapping `Start` immediately begins a trivia run with these defaults:
- Duration: 5 minutes
- Geography: worldwide
- Question mix: flags-heavy mixed geography
- Difficulty: adaptive
-No configuration questionnaire or modal tutorial appears before the first quiz. The interface teaches mechanics contextually without delaying the first question.
+No configuration questionnaire or modal tutorial appears before the first trivia run. The interface teaches mechanics contextually without delaying the first question.
### 6.2 Returning launch
From the second app launch onward, the home screen contains:
-- `Start`, which immediately uses the most recently saved quiz configuration
+- `Start`, which immediately uses the most recently saved trivia configuration
- `Options`, a secondary action
- Current streak and a compact recent-progress summary
- Settings
-The app must distinguish first launch from later launches using local state. Completing a quiz is not required for the returning layout to appear on the next launch.
+The minimal first-use Home remains until the player starts their first trivia run. Merely opening and closing the app does not reveal returning-player controls. Completing the run is not required.
### 6.3 Options flow
@@ -138,13 +138,13 @@ The initial V1 sequence is:
- Capitals
- Mixed geography
-The final answer is saved and immediately starts the quiz. All answers are saved as one configuration. Abandoning the flow leaves the previously saved configuration unchanged.
+The final answer is saved and immediately starts the trivia. All answers are saved as one configuration. Abandoning the flow leaves the previously saved configuration unchanged.
Difficulty is always adaptive in V1 and has no player-facing configuration.
V1 does not ask the player to choose a question count or continent. These controls may be added later only if evidence shows they improve play without adding friction.
-### 6.4 Quiz run
+### 6.4 Trivia run
A run repeatedly moves through:
@@ -258,14 +258,14 @@ Examples for either question format are 100 points at the initial instant, 75 po
### 8.3 Hints
-Each question permits at most one hint. Opening the hint chooser pauses the question timer. The player chooses among the hint types supported by that question:
+Each supported hint type may be used once per question. Opening the hint chooser pauses the question timer. Used hints remain visible, disabled, and marked with a check. The player may return for another unused hint among the types supported by that question:
- Reveal one correct letter
- Remove decoy letters or incorrect choices
- Reveal the continent
- Show a funny factual clue
-All hint types use the same debit rule. When a hint is chosen, the app first deducts up to 25 points from the score already earned in the current run. The score can never become negative: a zero balance makes the hint free, while a balance below 25 is reduced to zero. A subsequent correct answer then awards points using the normal speed or overtime formula. An incorrect hinted answer awards zero.
+All hint types use the same debit rule. Choosing a hint deducts half of the currently available score using whole points. The chooser displays the exact deduction as a compact negative-points label and hides it at a zero balance. A subsequent correct answer then awards points using the normal speed or overtime formula. An incorrect hinted answer awards zero.
A hinted correct answer is recorded as correct for accuracy and keeps the item in `learning`, but it does not satisfy the unhinted thresholds for `familiar` or `mastered`. Using a hint never blocks progression or triggers negative copy.
@@ -561,7 +561,7 @@ V1 is ready only when:
- A fresh install can start and finish every quiz configuration in airplane mode on Android and iOS.
- First launch shows only the immediate-start experience; later launches show `Start` and `Options`.
- `Start` correctly reuses the most recently completed options configuration.
-- Each timed duration ends after the current question, and unlimited mode ends only by player choice.
+- Each timed duration moves directly to Results when the overall trivia timer expires, and unlimited mode ends only by player choice.
- Question and session clocks pause in every specified non-answering state.
- Abandoned and completed runs apply mastery, results, personal-best, and streak rules correctly.
- Correct-before-timeout, correct-after-timeout, incorrect, and hinted scoring follow the specified rules.