Kunal Pai commited on
Commit
aa64af8
·
1 Parent(s): a3a158e

Globle Tool

Browse files
.gitignore CHANGED
@@ -1,4 +1,5 @@
1
  *venv/
2
  __pycache__/
3
  *.pyc
4
- .env
 
 
1
  *venv/
2
  __pycache__/
3
  *.pyc
4
+ .env
5
+ .gradio
tools/globle_tool.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+ import random
3
+ import math
4
+ import difflib
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ __all__ = ["GlobleDistanceTool"]
10
+
11
+
12
+ class GlobleDistanceTool:
13
+ """Globle‑style geography guessing game.
14
+
15
+ *Persists* game state to a JSON file in the user’s home directory (`~/.Globle_distance_state.json`).
16
+ This survives process restarts, ensuring the hidden country stays the same
17
+ until the player guesses correctly, gives up, or passes `reset=True`.
18
+ """
19
+
20
+ # ------------------------------------------------------------------
21
+ # Metadata for orchestration
22
+ # ------------------------------------------------------------------
23
+ dependencies = [
24
+ "geopandas==1.0.1",
25
+ "shapely==2.1.0",
26
+ ]
27
+
28
+ inputSchema = {
29
+ "name": "GlobleDistanceTool",
30
+ "description": (
31
+ "Guess the hidden country. Tool replies with distance to the target's centroid, "
32
+ "or notifies if they share a border. Use '/GIVEUP' to surrender."
33
+ ),
34
+ "parameters": {
35
+ "type": "object",
36
+ "properties": {
37
+ "geo_path": {
38
+ "type": "string",
39
+ "description": "Path to GeoJSON/Shapefile with country polygons. Default './tools/util/countries.geojson'.",
40
+ },
41
+ "guess": {
42
+ "type": "string",
43
+ "description": "The country you are guessing (case‑insensitive).",
44
+ },
45
+ "reset": {
46
+ "type": "boolean",
47
+ "description": "Force a new game and discard stored state.",
48
+ },
49
+ },
50
+ },
51
+ }
52
+
53
+ # ------------------------------------------------------------------
54
+ # Constants / state file location
55
+ # ------------------------------------------------------------------
56
+ _STATE_PATH = Path.home() / ".Globle_distance_state.json"
57
+
58
+ # Runtime caches (populated lazily)
59
+ _countries_cache = {} # geo_path → (countries, geoms)
60
+
61
+ # --------------------------- public entry ---------------------------
62
+ def run(self, **kwargs):
63
+ geo_path = kwargs.get("geo_path", "./tools/util/countries.geojson")
64
+ guess = kwargs.get("guess")
65
+ reset_flag = bool(kwargs.get("reset", False))
66
+
67
+ # -----------------------------------
68
+ # Load or reset persistent state file
69
+ # -----------------------------------
70
+ if reset_flag and self._STATE_PATH.exists():
71
+ self._STATE_PATH.unlink(missing_ok=True)
72
+
73
+ state = self._load_state()
74
+ if state is None: # no current game → start one
75
+ init_res = self._start_new_game(geo_path)
76
+ if init_res["status"] != "success":
77
+ return init_res
78
+ state = self._load_state() # reload the freshly written state
79
+
80
+ # ------------------
81
+ # Ensure guess given
82
+ # ------------------
83
+ if not guess:
84
+ return {
85
+ "status": "error",
86
+ "message": "Provide a 'guess' parameter, or '/GIVEUP' to give up.",
87
+ "output": None,
88
+ }
89
+
90
+ # --------------------------------------------------------------
91
+ # Play round
92
+ # --------------------------------------------------------------
93
+ countries, geoms = self._get_country_data(state["geo_path"])
94
+ target = state["target"]
95
+ tlat, tlon = countries[target]
96
+
97
+ raw_guess = guess.strip()
98
+
99
+ # Handle give‑up
100
+ if raw_guess.upper() == "/GIVEUP":
101
+ self._STATE_PATH.unlink(missing_ok=True)
102
+ return {
103
+ "status": "success",
104
+ "message": "Gave up.",
105
+ "output": {"result": "gave_up", "answer": target},
106
+ }
107
+
108
+ # Fuzzy match unknown country
109
+ if raw_guess not in countries:
110
+ match = difflib.get_close_matches(raw_guess, countries.keys(), n=1, cutoff=0.6)
111
+ if not match:
112
+ return {
113
+ "status": "error",
114
+ "message": f"Unknown country '{raw_guess}'.",
115
+ "output": None,
116
+ }
117
+ guess_name = match[0]
118
+ else:
119
+ guess_name = raw_guess
120
+
121
+ # Correct?
122
+ if guess_name == target:
123
+ self._STATE_PATH.unlink(missing_ok=True) # clear state for next game
124
+ return {
125
+ "status": "success",
126
+ "message": "Correct!",
127
+ "output": {"result": "correct", "country": target},
128
+ }
129
+
130
+ # Shared border?
131
+ if geoms[guess_name].touches(geoms[target]):
132
+ return {
133
+ "status": "success",
134
+ "message": "Bordering country.",
135
+ "output": {"result": "border", "country": guess_name},
136
+ }
137
+
138
+ # Distance feedback
139
+ glat, glon = countries[guess_name]
140
+ dist_km = self._haversine(glat, glon, tlat, tlon)
141
+ return {
142
+ "status": "success",
143
+ "message": "Distance computed.",
144
+ "output": {"result": "distance", "country": guess_name, "km": round(dist_km)},
145
+ }
146
+
147
+ # ------------------------------------------------------------------
148
+ # Helper: start new game and persist state
149
+ # ------------------------------------------------------------------
150
+ def _start_new_game(self, geo_path):
151
+ try:
152
+ countries, _ = self._get_country_data(geo_path)
153
+ except Exception as e:
154
+ return {
155
+ "status": "error",
156
+ "message": f"Failed to load '{geo_path}': {e}",
157
+ "output": None,
158
+ }
159
+ target = random.choice(list(countries))
160
+ state = {"geo_path": geo_path, "target": target}
161
+ try:
162
+ self._STATE_PATH.write_text(json.dumps(state))
163
+ except Exception as e:
164
+ return {
165
+ "status": "error",
166
+ "message": f"Cannot write state file: {e}",
167
+ "output": None,
168
+ }
169
+ return {"status": "success", "message": "New game started", "output": None}
170
+
171
+ # ------------------------------------------------------------------
172
+ # Helper: load state file (or None if missing)
173
+ # ------------------------------------------------------------------
174
+ def _load_state(self):
175
+ if not self._STATE_PATH.exists():
176
+ return None
177
+ try:
178
+ return json.loads(self._STATE_PATH.read_text())
179
+ except Exception:
180
+ # Corrupt state – delete and start fresh next time
181
+ self._STATE_PATH.unlink(missing_ok=True)
182
+ return None
183
+
184
+ # ------------------------------------------------------------------
185
+ # Country data cache (per geo_path) to avoid re‑reading file
186
+ # ------------------------------------------------------------------
187
+ def _get_country_data(self, geo_path):
188
+ if geo_path in self._countries_cache:
189
+ return self._countries_cache[geo_path]
190
+ countries, geoms = self._load_countries(geo_path)
191
+ self._countries_cache[geo_path] = (countries, geoms)
192
+ return countries, geoms
193
+
194
+ # ------------------------------------------------------------------
195
+ # Geometry / file utility
196
+ # ------------------------------------------------------------------
197
+ @staticmethod
198
+ def _haversine(lat1, lon1, lat2, lon2):
199
+ R = 6371.0
200
+ φ1, φ2 = math.radians(lat1), math.radians(lat2)
201
+ dφ = math.radians(lat2 - lat1)
202
+ dλ = math.radians(lon2 - lon1)
203
+ a = math.sin(dφ / 2) ** 2 + math.cos(φ1) * math.cos(φ2) * math.sin(dλ / 2) ** 2
204
+ return 2 * R * math.asin(math.sqrt(a))
205
+
206
+ @staticmethod
207
+ def _load_countries(geo_path):
208
+ gpd = importlib.import_module("geopandas")
209
+ Point = importlib.import_module("shapely.geometry").Point
210
+ gdf = gpd.read_file(geo_path)
211
+
212
+ name_field = next((c for c in ["ADMIN", "NAME", "NAME_EN", "NAME_LONG", "SOVEREIGN", "COUNTRY"] if c in gdf.columns), None)
213
+ if not name_field:
214
+ non_geom = [c for c in gdf.columns if c.lower() != "geometry"]
215
+ if not non_geom:
216
+ raise ValueError("No suitable name column in geo file")
217
+ name_field = non_geom[0]
218
+
219
+ centroids, geoms = {}, {}
220
+ for _, row in gdf.iterrows():
221
+ geom = row.geometry
222
+ if not geom or geom.is_empty:
223
+ continue
224
+ c = geom.centroid # type: Point
225
+ name = row[name_field]
226
+ centroids[name] = (c.y, c.x)
227
+ geoms[name] = geom
228
+ return centroids, geoms
tools/{WordleTool.py → wordle_tool.py} RENAMED
File without changes