cege-gopher 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. cege_gopher-0.1.0/LICENSE +21 -0
  2. cege_gopher-0.1.0/PKG-INFO +120 -0
  3. cege_gopher-0.1.0/README.md +83 -0
  4. cege_gopher-0.1.0/pyproject.toml +25 -0
  5. cege_gopher-0.1.0/setup.cfg +4 -0
  6. cege_gopher-0.1.0/src/cege_gopher/__init__.py +611 -0
  7. cege_gopher-0.1.0/src/cege_gopher/sprites/E.png +0 -0
  8. cege_gopher-0.1.0/src/cege_gopher/sprites/N.png +0 -0
  9. cege_gopher-0.1.0/src/cege_gopher/sprites/S.png +0 -0
  10. cege_gopher-0.1.0/src/cege_gopher/sprites/W.png +0 -0
  11. cege_gopher-0.1.0/src/cege_gopher/worlds/around1.wld +5 -0
  12. cege_gopher-0.1.0/src/cege_gopher/worlds/around2.wld +14 -0
  13. cege_gopher-0.1.0/src/cege_gopher/worlds/around3.wld +15 -0
  14. cege_gopher-0.1.0/src/cege_gopher/worlds/harvest1.wld +42 -0
  15. cege_gopher-0.1.0/src/cege_gopher/worlds/harvest2.wld +36 -0
  16. cege_gopher-0.1.0/src/cege_gopher/worlds/harvest3.wld +36 -0
  17. cege_gopher-0.1.0/src/cege_gopher/worlds/harvest5.wld +42 -0
  18. cege_gopher-0.1.0/src/cege_gopher/worlds/hurdles1.wld +12 -0
  19. cege_gopher-0.1.0/src/cege_gopher/worlds/hurdles2.wld +12 -0
  20. cege_gopher-0.1.0/src/cege_gopher/worlds/hurdles3.wld +15 -0
  21. cege_gopher-0.1.0/src/cege_gopher/worlds/hurdles4.wld +18 -0
  22. cege_gopher-0.1.0/src/cege_gopher/worlds/maze1.wld +32 -0
  23. cege_gopher-0.1.0/src/cege_gopher/worlds/newspaper.wld +18 -0
  24. cege_gopher-0.1.0/src/cege_gopher.egg-info/PKG-INFO +120 -0
  25. cege_gopher-0.1.0/src/cege_gopher.egg-info/SOURCES.txt +26 -0
  26. cege_gopher-0.1.0/src/cege_gopher.egg-info/dependency_links.txt +1 -0
  27. cege_gopher-0.1.0/src/cege_gopher.egg-info/requires.txt +1 -0
  28. cege_gopher-0.1.0/src/cege_gopher.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Seongjin Choi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: cege-gopher
3
+ Version: 0.1.0
4
+ Summary: A Karel-style grid robot for teaching Python, themed as Goldy Gopher. Records and replays, so it works in Colab.
5
+ Author-email: Seongjin Choi <chois@umn.edu>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Seongjin Choi
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Keywords: education,karel,robot,colab,cege3101
29
+ Classifier: Intended Audience :: Education
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Topic :: Education
32
+ Requires-Python: >=3.9
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: matplotlib>=3.5
36
+ Dynamic: license-file
37
+
38
+ # cege_gopher
39
+
40
+ A Karel-style grid robot for teaching introductory Python, themed as Goldy
41
+ Gopher. Written for **CEGE 3101 — Computer Applications I** at the University of
42
+ Minnesota.
43
+
44
+ Ported from the MATLAB robot used in earlier offerings of the course, which was
45
+ itself a port of the KAIST CS101 HUBO exercise
46
+ ([otfried/cs101](https://github.com/otfried/cs101)) — Karel the Robot, at the
47
+ root of it.
48
+
49
+ ## Why this exists
50
+
51
+ The original libraries open a live window and animate each command as it runs.
52
+ **Google Colab cannot host one.**
53
+
54
+ So this version **records, then replays**. Every command appends a frame to a
55
+ buffer; nothing is drawn until `show()`, which renders the whole run as an
56
+ inline animation:
57
+
58
+ > write the whole program → run the cell → watch it play back
59
+
60
+ Students never drive the robot interactively; every program is pre-coded, which
61
+ is also how the homework is graded.
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ pip install cege-gopher
67
+ ```
68
+
69
+ Or, from the course repository:
70
+
71
+ ```bash
72
+ pip install "git+https://github.com/benchoi93/CEGE3101_Computer_Applications.git#subdirectory=packages/cege-gopher"
73
+ ```
74
+
75
+ The 13 world files ship **inside the package**, so nothing needs uploading —
76
+ `load_world("harvest1.wld")` just works.
77
+
78
+ ## Use
79
+
80
+ ```python
81
+ from cege_gopher import *
82
+
83
+ load_world("harvest1.wld") # or: create_world(10, 10)
84
+ goldy = Gopher()
85
+ goldy.set_trace("maroon")
86
+
87
+ while goldy.front_is_clear():
88
+ if goldy.on_carrot():
89
+ goldy.pick_carrot()
90
+ goldy.move()
91
+
92
+ show() # the animation renders here
93
+ ```
94
+
95
+ `list_worlds()` returns the bundled world names.
96
+
97
+ ## API
98
+
99
+ | | |
100
+ |---|---|
101
+ | Module | `create_world(avenues, streets)`, `load_world(name)`, `list_worlds()`, `show(fps=)`, `show_final()`, `get_world()` |
102
+ | Move | `move()`, `turn_left()`, `turn_right()` |
103
+ | Carrots | `pick_carrot()`, `drop_carrot()` |
104
+ | Sense | `front_is_clear()`, `left_is_clear()`, `right_is_clear()`, `facing_north()`, `on_carrot()`, `carries_carrots()` |
105
+ | Display | `set_trace(color)`, `get_pos()` |
106
+
107
+ `Robot` is an alias for `Gopher`, and `pick_beeper`/`pick_coin` for
108
+ `pick_carrot`, so code written against `cs1robots` or the MATLAB version still
109
+ runs.
110
+
111
+ ## A bug fixed in this port
112
+
113
+ The MATLAB version had `leftIsClear` and `rightIsClear` **swapped** — facing
114
+ east it checked south for "left", when `turnLeft` rotates counter-clockwise
115
+ (E→N) so left of east is north. This port uses `left = (-dy, dx)`,
116
+ `right = (dy, -dx)`.
117
+
118
+ ## License
119
+
120
+ MIT.
@@ -0,0 +1,83 @@
1
+ # cege_gopher
2
+
3
+ A Karel-style grid robot for teaching introductory Python, themed as Goldy
4
+ Gopher. Written for **CEGE 3101 — Computer Applications I** at the University of
5
+ Minnesota.
6
+
7
+ Ported from the MATLAB robot used in earlier offerings of the course, which was
8
+ itself a port of the KAIST CS101 HUBO exercise
9
+ ([otfried/cs101](https://github.com/otfried/cs101)) — Karel the Robot, at the
10
+ root of it.
11
+
12
+ ## Why this exists
13
+
14
+ The original libraries open a live window and animate each command as it runs.
15
+ **Google Colab cannot host one.**
16
+
17
+ So this version **records, then replays**. Every command appends a frame to a
18
+ buffer; nothing is drawn until `show()`, which renders the whole run as an
19
+ inline animation:
20
+
21
+ > write the whole program → run the cell → watch it play back
22
+
23
+ Students never drive the robot interactively; every program is pre-coded, which
24
+ is also how the homework is graded.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install cege-gopher
30
+ ```
31
+
32
+ Or, from the course repository:
33
+
34
+ ```bash
35
+ pip install "git+https://github.com/benchoi93/CEGE3101_Computer_Applications.git#subdirectory=packages/cege-gopher"
36
+ ```
37
+
38
+ The 13 world files ship **inside the package**, so nothing needs uploading —
39
+ `load_world("harvest1.wld")` just works.
40
+
41
+ ## Use
42
+
43
+ ```python
44
+ from cege_gopher import *
45
+
46
+ load_world("harvest1.wld") # or: create_world(10, 10)
47
+ goldy = Gopher()
48
+ goldy.set_trace("maroon")
49
+
50
+ while goldy.front_is_clear():
51
+ if goldy.on_carrot():
52
+ goldy.pick_carrot()
53
+ goldy.move()
54
+
55
+ show() # the animation renders here
56
+ ```
57
+
58
+ `list_worlds()` returns the bundled world names.
59
+
60
+ ## API
61
+
62
+ | | |
63
+ |---|---|
64
+ | Module | `create_world(avenues, streets)`, `load_world(name)`, `list_worlds()`, `show(fps=)`, `show_final()`, `get_world()` |
65
+ | Move | `move()`, `turn_left()`, `turn_right()` |
66
+ | Carrots | `pick_carrot()`, `drop_carrot()` |
67
+ | Sense | `front_is_clear()`, `left_is_clear()`, `right_is_clear()`, `facing_north()`, `on_carrot()`, `carries_carrots()` |
68
+ | Display | `set_trace(color)`, `get_pos()` |
69
+
70
+ `Robot` is an alias for `Gopher`, and `pick_beeper`/`pick_coin` for
71
+ `pick_carrot`, so code written against `cs1robots` or the MATLAB version still
72
+ runs.
73
+
74
+ ## A bug fixed in this port
75
+
76
+ The MATLAB version had `leftIsClear` and `rightIsClear` **swapped** — facing
77
+ east it checked south for "left", when `turnLeft` rotates counter-clockwise
78
+ (E→N) so left of east is north. This port uses `left = (-dy, dx)`,
79
+ `right = (dy, -dx)`.
80
+
81
+ ## License
82
+
83
+ MIT.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cege-gopher"
7
+ version = "0.1.0"
8
+ description = "A Karel-style grid robot for teaching Python, themed as Goldy Gopher. Records and replays, so it works in Colab."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Seongjin Choi", email = "chois@umn.edu" }]
13
+ keywords = ["education", "karel", "robot", "colab", "cege3101"]
14
+ classifiers = [
15
+ "Intended Audience :: Education",
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Education",
18
+ ]
19
+ dependencies = ["matplotlib>=3.5"]
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
23
+
24
+ [tool.setuptools.package-data]
25
+ cege_gopher = ["worlds/*.wld", "sprites/*.png"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,611 @@
1
+ """cege_gopher --- a Karel-style grid robot for CEGE 3101, themed as Goldy Gopher.
2
+
3
+ Ported from the MATLAB `robot_matlab` version in
4
+ `lectures/week-02/python_tutorial_with_gopher/`, which was itself a port of the
5
+ KAIST CS101 `cs1robots` library (https://github.com/otfried/cs101).
6
+
7
+ WHY THIS EXISTS / WHAT IS DIFFERENT
8
+ -----------------------------------
9
+ The original opens a live window and animates each command as it happens. That
10
+ does not work in Google Colab, which cannot host an interactive window.
11
+
12
+ So this version **records, then replays**. Every command appends a frame to a
13
+ buffer; nothing is drawn until you call `show()`, which renders the whole run as
14
+ an inline animation. The practical consequence for students:
15
+
16
+ write the whole program -> run the cell -> watch it play back
17
+
18
+ You never "drive" the gopher interactively. Everything is pre-coded, which is
19
+ also how the homework is graded.
20
+
21
+ QUICK START
22
+ -----------
23
+ from cege_gopher import *
24
+
25
+ load_world("harvest1.wld") # or: create_world(10, 10)
26
+ goldy = Gopher(carrots=0)
27
+ goldy.set_trace("maroon")
28
+
29
+ for i in range(5):
30
+ goldy.move()
31
+ if goldy.on_carrot():
32
+ goldy.pick_carrot()
33
+
34
+ show() # <- the animation appears here
35
+
36
+ API
37
+ ---
38
+ Module: create_world(avenues, streets), load_world(path), show(), show_final()
39
+ Gopher: move, turn_left, turn_right, pick_carrot, drop_carrot,
40
+ front_is_clear, left_is_clear, right_is_clear,
41
+ facing_north, carries_carrots, on_carrot, set_trace, get_pos
42
+
43
+ Aliases are provided so code written against the original libraries still runs:
44
+ `Robot` for `Gopher`; `pick_beeper`/`pick_coin` for `pick_carrot`, and so on.
45
+
46
+ FIXED IN THIS PORT
47
+ ------------------
48
+ The MATLAB version had `leftIsClear` and `rightIsClear` swapped -- facing East it
49
+ checked South for "left", when left of East is North. Verified against the
50
+ turn_left direction cycle, which is counter-clockwise (E -> N -> W -> S). This
51
+ port uses left = (-dy, dx), right = (dy, -dx).
52
+ """
53
+ from __future__ import annotations
54
+
55
+ import os
56
+ import re
57
+
58
+ import matplotlib
59
+ import matplotlib.pyplot as plt
60
+ from matplotlib import animation
61
+ from matplotlib.patches import Circle, Ellipse, Polygon, Rectangle
62
+
63
+ __all__ = [
64
+ "Gopher", "Robot", "World",
65
+ "create_world", "load_world", "show", "show_final", "get_world",
66
+ "list_worlds", "GopherError",
67
+ ]
68
+
69
+ UMN_MAROON = "#7A0019"
70
+ UMN_GOLD = "#FFCC33"
71
+ _TRACE_COLORS = {"maroon": UMN_MAROON, "gold": UMN_GOLD}
72
+
73
+ # Orientation is stored as a unit vector (dx, dy). East is the start, matching
74
+ # the `robot = (1, 1, 'E', 0)` line in the .wld files.
75
+ _DIRS = {"E": (1, 0), "N": (0, 1), "W": (-1, 0), "S": (0, -1)}
76
+ _DIR_NAMES = {v: k for k, v in _DIRS.items()}
77
+
78
+
79
+ class GopherError(Exception):
80
+ """Raised when the gopher is asked to do something impossible."""
81
+
82
+
83
+ # ==========================================================================
84
+ # World
85
+ # ==========================================================================
86
+ class World:
87
+ """A grid of avenues (columns) x streets (rows).
88
+
89
+ Two coordinate systems are in play, and mixing them up is the classic bug:
90
+
91
+ * **cell** coordinates ``(avenue, street)``, 1-based, are what the gopher
92
+ uses. Avenue is the column, street is the row.
93
+ * **doubled** coordinates ``(col, row)`` are what *walls* use, where a cell
94
+ ``(a, s)`` sits at ``(2a-1, 2s-1)``. A wall lives between two cells, so it
95
+ always has one even coordinate. This is the same convention as the
96
+ original `.wld` files, so world files are compatible unchanged.
97
+ """
98
+
99
+ def __init__(self, avenues=10, streets=10, walls=None, carrots=None):
100
+ self.avenues = int(avenues)
101
+ self.streets = int(streets)
102
+ self.walls = set(walls or ())
103
+ # {(avenue, street): count}
104
+ self.carrots = dict(carrots or {})
105
+ self._borders = self._make_borders()
106
+
107
+ # ---- geometry ----------------------------------------------------
108
+ def _make_borders(self) -> set:
109
+ b = set()
110
+ for a in range(1, self.avenues + 1):
111
+ b.add((2 * a - 1, 0)) # south wall of the world
112
+ b.add((2 * a - 1, 2 * self.streets)) # north wall
113
+ for s in range(1, self.streets + 1):
114
+ b.add((0, 2 * s - 1)) # west wall
115
+ b.add((2 * self.avenues, 2 * s - 1)) # east wall
116
+ return b
117
+
118
+ def is_clear(self, col, row) -> bool:
119
+ """True if no wall and no world border sits at this doubled coordinate."""
120
+ return (col, row) not in self.walls and (col, row) not in self._borders
121
+
122
+ def toggle_wall(self, col, row):
123
+ if (col + row) % 2 == 0:
124
+ raise GopherError(
125
+ f"A wall cannot sit at ({col}, {row}) -- walls go *between* "
126
+ "cells, so exactly one coordinate must be even.")
127
+ self.walls.symmetric_difference_update({(col, row)})
128
+
129
+ # ---- carrots -----------------------------------------------------
130
+ def count_carrots(self, avenue, street) -> int:
131
+ return self.carrots.get((avenue, street), 0)
132
+
133
+ def add_carrot(self, avenue, street):
134
+ self.carrots[(avenue, street)] = self.count_carrots(avenue, street) + 1
135
+
136
+ def remove_carrot(self, avenue, street):
137
+ n = self.count_carrots(avenue, street)
138
+ if n <= 1:
139
+ self.carrots.pop((avenue, street), None)
140
+ else:
141
+ self.carrots[(avenue, street)] = n - 1
142
+
143
+
144
+ # ==========================================================================
145
+ # World files
146
+ # ==========================================================================
147
+ def _parse_world(text: str):
148
+ """Parse a `.wld` file.
149
+
150
+ Accepts ``carrots``, ``coins`` and ``beepers`` as the item key, so both the
151
+ MATLAB-era worlds in this repo and the original CS101 worlds load unchanged.
152
+ """
153
+ def _int(key, default=10):
154
+ m = re.search(rf"{key}\s*=\s*(\d+)", text)
155
+ return int(m.group(1)) if m else default
156
+
157
+ avenues, streets = _int("avenues"), _int("streets")
158
+
159
+ walls = set()
160
+ m = re.search(r"walls\s*=\s*\[(.*?)\]", text, re.S)
161
+ if m:
162
+ for a, b in re.findall(r"\(\s*(\d+)\s*,\s*(\d+)\s*\)", m.group(1)):
163
+ walls.add((int(a), int(b)))
164
+
165
+ carrots = {}
166
+ m = re.search(r"(?:carrots|coins|beepers)\s*=\s*\{(.*?)\}", text, re.S)
167
+ if m:
168
+ for a, b, n in re.findall(
169
+ r"\(\s*(\d+)\s*,\s*(\d+)\s*\)\s*:\s*(\d+)", m.group(1)):
170
+ carrots[(int(a), int(b))] = int(n)
171
+
172
+ start = (1, 1, "E", 0)
173
+ m = re.search(r"robot\s*=\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*'(\w)'\s*,\s*(\d+)",
174
+ text)
175
+ if m:
176
+ start = (int(m.group(1)), int(m.group(2)), m.group(3).upper(),
177
+ int(m.group(4)))
178
+
179
+ return World(avenues, streets, walls, carrots), start
180
+
181
+
182
+ _world: World | None = None
183
+ _start = (1, 1, "E", 0)
184
+ _frames: list = []
185
+ _gophers: list = []
186
+ _last_anim = None # holds the most recent animation so it is not garbage-collected
187
+
188
+
189
+ def create_world(avenues=10, streets=10):
190
+ """Make an empty world and reset the recording."""
191
+ global _world, _start, _frames, _gophers
192
+ _world = World(avenues, streets)
193
+ _start = (1, 1, "E", 0)
194
+ _frames, _gophers = [], []
195
+ return _world
196
+
197
+
198
+ _HERE = os.path.dirname(os.path.abspath(__file__))
199
+ _BUNDLED = os.path.join(_HERE, "worlds")
200
+ _SPRITES = os.path.join(_HERE, "sprites")
201
+ _sprite_cache: dict = {}
202
+
203
+
204
+ def _sprite(direction):
205
+ """The gopher artwork for a facing direction, or None if unavailable.
206
+
207
+ One PNG per direction, so the sprite never has to be rotated. These are the
208
+ same images the MATLAB `visualizeWorld.m` used.
209
+ """
210
+ name = _DIR_NAMES[direction]
211
+ if name not in _sprite_cache:
212
+ path = os.path.join(_SPRITES, f"{name}.png")
213
+ try:
214
+ _sprite_cache[name] = plt.imread(path)
215
+ except Exception:
216
+ _sprite_cache[name] = None
217
+ return _sprite_cache[name]
218
+
219
+
220
+ def list_worlds():
221
+ """Names of the world files that ship with the package."""
222
+ try:
223
+ return sorted(f for f in os.listdir(_BUNDLED) if f.endswith(".wld"))
224
+ except OSError:
225
+ return []
226
+
227
+
228
+ def _resolve_world(filename):
229
+ """Find a world file.
230
+
231
+ Looks in this order, so that no upload is ever required:
232
+
233
+ 1. the path exactly as given (your own world files still work)
234
+ 2. the worlds bundled inside the installed package, by bare name --
235
+ ``load_world("harvest1.wld")`` and ``load_world("worlds/harvest1.wld")``
236
+ both resolve to the bundled copy
237
+ """
238
+ if os.path.exists(filename):
239
+ return filename
240
+ bundled = os.path.join(_BUNDLED, os.path.basename(filename))
241
+ if os.path.exists(bundled):
242
+ return bundled
243
+ raise GopherError(
244
+ f"No world file called {os.path.basename(filename)!r}.\n"
245
+ f"Worlds bundled with cege_gopher: {', '.join(list_worlds()) or '(none)'}")
246
+
247
+
248
+ def load_world(filename):
249
+ """Load a `.wld` file and reset the recording.
250
+
251
+ The name alone is enough for the worlds that ship with the package:
252
+ ``load_world("harvest1.wld")``.
253
+ """
254
+ global _world, _start, _frames, _gophers
255
+ with open(_resolve_world(filename)) as fh:
256
+ _world, _start = _parse_world(fh.read())
257
+ _frames, _gophers = [], []
258
+ return _world
259
+
260
+
261
+ def get_world() -> World:
262
+ if _world is None:
263
+ raise GopherError("No world yet -- call create_world() or load_world() first.")
264
+ return _world
265
+
266
+
267
+ # ==========================================================================
268
+ # Gopher
269
+ # ==========================================================================
270
+ class Gopher:
271
+ """Goldy, on a grid. Every command records a frame; nothing draws until show()."""
272
+
273
+ def __init__(self, orientation=None, avenue=None, street=None, carrots=None,
274
+ color=UMN_MAROON, **kw):
275
+ if _world is None:
276
+ create_world()
277
+ sa, ss, so, sc = _start
278
+ self.avenue = sa if avenue is None else int(avenue)
279
+ self.street = ss if street is None else int(street)
280
+ self.orientation = _DIRS[(orientation or so).upper()]
281
+ # accept beepers=/coins= as aliases for carrots=
282
+ if carrots is None:
283
+ carrots = kw.pop("beepers", kw.pop("coins", sc))
284
+ self.carrots = int(carrots)
285
+ self.color = color
286
+ self.trace_color = None
287
+ self.trace = []
288
+ self.steps = 0
289
+ _gophers.append(self)
290
+ self._record()
291
+
292
+ # ---- internals ---------------------------------------------------
293
+ def _record(self):
294
+ if self.trace_color:
295
+ self.trace.append((self.avenue, self.street))
296
+ _frames.append({
297
+ "avenue": self.avenue,
298
+ "street": self.street,
299
+ "dir": self.orientation,
300
+ "carried": self.carrots,
301
+ "world": dict(get_world().carrots),
302
+ "trace": list(self.trace),
303
+ "steps": self.steps,
304
+ })
305
+
306
+ def _doubled(self):
307
+ return 2 * self.avenue - 1, 2 * self.street - 1
308
+
309
+ # ---- movement ----------------------------------------------------
310
+ def move(self):
311
+ """Move one cell in the direction the gopher is facing."""
312
+ if not self.front_is_clear():
313
+ raise GopherError(
314
+ f"Ouch! There is a wall in front of me at avenue {self.avenue}, "
315
+ f"street {self.street}, facing {_DIR_NAMES[self.orientation]}.")
316
+ dx, dy = self.orientation
317
+ self.avenue += dx
318
+ self.street += dy
319
+ self.steps += 1
320
+ self._record()
321
+
322
+ def turn_left(self):
323
+ """Turn 90 degrees counter-clockwise (E -> N -> W -> S -> E)."""
324
+ dx, dy = self.orientation
325
+ self.orientation = (-dy, dx)
326
+ self._record()
327
+
328
+ def turn_right(self):
329
+ """Not in the original API -- three lefts, provided for convenience."""
330
+ for _ in range(3):
331
+ self.turn_left()
332
+
333
+ # ---- sensing -----------------------------------------------------
334
+ def front_is_clear(self) -> bool:
335
+ col, row = self._doubled()
336
+ dx, dy = self.orientation
337
+ return get_world().is_clear(col + dx, row + dy)
338
+
339
+ def left_is_clear(self) -> bool:
340
+ col, row = self._doubled()
341
+ dx, dy = self.orientation
342
+ return get_world().is_clear(col - dy, row + dx)
343
+
344
+ def right_is_clear(self) -> bool:
345
+ col, row = self._doubled()
346
+ dx, dy = self.orientation
347
+ return get_world().is_clear(col + dy, row - dx)
348
+
349
+ def facing_north(self) -> bool:
350
+ return self.orientation == _DIRS["N"]
351
+
352
+ def carries_carrots(self) -> bool:
353
+ return self.carrots > 0
354
+
355
+ def on_carrot(self) -> bool:
356
+ return get_world().count_carrots(self.avenue, self.street) > 0
357
+
358
+ def get_pos(self):
359
+ return self.avenue, self.street
360
+
361
+ # ---- carrots -----------------------------------------------------
362
+ def pick_carrot(self):
363
+ if not self.on_carrot():
364
+ raise GopherError(
365
+ f"There is no carrot at avenue {self.avenue}, street {self.street}.")
366
+ get_world().remove_carrot(self.avenue, self.street)
367
+ self.carrots += 1
368
+ self._record()
369
+
370
+ def drop_carrot(self):
371
+ if not self.carries_carrots():
372
+ raise GopherError("I am not carrying any carrots to drop.")
373
+ get_world().add_carrot(self.avenue, self.street)
374
+ self.carrots -= 1
375
+ self._record()
376
+
377
+ # ---- display -----------------------------------------------------
378
+ def set_trace(self, color="maroon"):
379
+ """Draw a line behind the gopher as it moves."""
380
+ self.trace_color = _TRACE_COLORS.get(color, color)
381
+ self.trace = [(self.avenue, self.street)]
382
+
383
+ def set_pause(self, delay=0):
384
+ """No-op. Kept so code from the original library still runs; pacing is
385
+ set by show(fps=...) instead, because playback is a recording."""
386
+ return None
387
+
388
+ # ---- compatibility aliases --------------------------------------
389
+ pick_beeper = pick_coin = pick_carrot
390
+ drop_beeper = drop_coin = drop_carrot
391
+ on_beeper = on_coin = on_carrot
392
+ carries_beepers = carries_coins = carries_carrots
393
+
394
+
395
+ Robot = Gopher # so `Robot(...)` from the original examples still works
396
+
397
+
398
+ # ==========================================================================
399
+ # Rendering
400
+ # ==========================================================================
401
+ def _draw_gopher(ax, x, y, direction, color):
402
+ """Draw the gopher facing `direction`, using the artwork if it is available."""
403
+ img = _sprite(direction)
404
+ if img is not None:
405
+ h = 0.86 # cell heights
406
+ w = h * img.shape[1] / img.shape[0] # keep the sprite's aspect
407
+ ax.imshow(img, extent=[x - w / 2, x + w / 2, y - h / 2, y + h / 2],
408
+ zorder=10, interpolation="antialiased")
409
+ return
410
+
411
+ # Fallback: a simple drawn gopher, so the library still works if the
412
+ # sprite files are missing.
413
+ dx, dy = direction
414
+ px, py = -dy, dx
415
+ for sgn in (-1, 1):
416
+ ax.add_patch(Circle((x - 0.20 * dx + 0.17 * px * sgn,
417
+ y - 0.20 * dy + 0.17 * py * sgn),
418
+ 0.11, facecolor=color, edgecolor="white",
419
+ linewidth=1.0, zorder=5))
420
+ ax.add_patch(Circle((x, y), 0.30, facecolor=color, edgecolor="white",
421
+ linewidth=1.4, zorder=6))
422
+ ax.add_patch(Ellipse((x + 0.13 * dx, y + 0.13 * dy), 0.24, 0.24,
423
+ facecolor=UMN_GOLD, edgecolor="white",
424
+ linewidth=1.0, zorder=7))
425
+ ax.add_patch(Polygon(
426
+ [(x + 0.36 * dx, y + 0.36 * dy),
427
+ (x + 0.16 * dx + 0.11 * px, y + 0.16 * dy + 0.11 * py),
428
+ (x + 0.16 * dx - 0.11 * px, y + 0.16 * dy - 0.11 * py)],
429
+ closed=True, facecolor="#2b2b2b", edgecolor="white",
430
+ linewidth=0.8, zorder=8))
431
+
432
+
433
+ def _present(html: str, controls: bool = False) -> str:
434
+ """Prepare a matplotlib jshtml animation for the classroom.
435
+
436
+ Two changes, both cosmetic but both worth having:
437
+
438
+ * **Autoplay.** `to_jshtml` always renders paused and has no autoplay option
439
+ (only `to_html5_video` autoplays). It does build a global `animNNN` object
440
+ inside a `setTimeout(..., 0)`, so we find that name and poll until it
441
+ exists, then press play.
442
+ * **Hide the controls** -- the slider, the button row and the
443
+ once/loop/reflect radios. Students are watching a replay, not scrubbing a
444
+ video, and the controls are more clutter than help.
445
+
446
+ The controls are *hidden with CSS, not removed*: the animation's JavaScript
447
+ looks the slider and the loop-select up by id at construction time, so
448
+ deleting them from the DOM breaks playback. The rule is scoped to this one
449
+ animation so it cannot affect anything else on the page.
450
+
451
+ The id is a hex uuid, not a number -- e.g. animef73a836c4794088b855e878cfca32da.
452
+ """
453
+ m = re.search(r"\banim(\w+)\s*=\s*new\s+Animation", html)
454
+ if not m: # matplotlib changed its template; leave it be
455
+ return html
456
+ ident = m.group(1)
457
+
458
+ hide = "" if controls else f"""
459
+ <style>
460
+ #gopher-anim-{ident} .anim-controls {{ display: none !important; }}
461
+ </style>"""
462
+
463
+ return f"""<div id="gopher-anim-{ident}">
464
+ {html}
465
+ </div>{hide}
466
+ <script language="javascript">
467
+ (function() {{
468
+ var tries = 0;
469
+ var timer = setInterval(function() {{
470
+ if (typeof anim{ident} !== "undefined" && anim{ident}) {{
471
+ anim{ident}.play_animation();
472
+ clearInterval(timer);
473
+ }} else if (++tries > 100) {{
474
+ clearInterval(timer); /* give up rather than poll forever */
475
+ }}
476
+ }}, 50);
477
+ }})()
478
+ </script>
479
+ """
480
+
481
+
482
+ def _auto_figsize(world):
483
+ """Size the figure from the world, the way visualizeWorld.m did.
484
+
485
+ MATLAB used 50 pixels per cell; at matplotlib's 100 dpi that is 0.5 inch.
486
+ Clamped so a 3x3 world is still readable and a 20x20 one still fits on a
487
+ screen, and the aspect always matches the grid.
488
+ """
489
+ per_cell = 0.5
490
+ lo, hi = 3.2, 11.0
491
+ w = min(max(world.avenues * per_cell, lo), hi)
492
+ h = min(max(world.streets * per_cell, lo), hi)
493
+ return (w, h)
494
+
495
+
496
+ def _draw_frame(ax, frame, world, title=None):
497
+ ax.clear()
498
+ av, st = world.avenues, world.streets
499
+ ax.set_xlim(0.3, av + 0.7)
500
+ ax.set_ylim(0.3, st + 0.7)
501
+ ax.set_aspect("equal")
502
+ ax.set_xticks(range(1, av + 1))
503
+ ax.set_yticks(range(1, st + 1))
504
+ ax.set_xlabel("avenue")
505
+ ax.set_ylabel("street")
506
+ ax.grid(True, color="0.85", linewidth=0.8, zorder=0)
507
+ for spine in ax.spines.values():
508
+ spine.set_visible(False)
509
+
510
+ # world border
511
+ ax.add_patch(Rectangle((0.5, 0.5), av, st, fill=False,
512
+ edgecolor="0.25", linewidth=2.5, zorder=3))
513
+
514
+ # Walls, in doubled coordinates, sit *between* cells.
515
+ # even col -> vertical wall east of avenue col/2, at x = col/2 + 0.5
516
+ # even row -> horizontal wall north of street row/2, at y = row/2 + 0.5
517
+ for (col, row) in world.walls:
518
+ if col % 2 == 0: # vertical
519
+ x = col / 2 + 0.5
520
+ s = (row + 1) / 2
521
+ ax.plot([x, x], [s - 0.5, s + 0.5], color="0.25", linewidth=4,
522
+ zorder=4, solid_capstyle="butt")
523
+ else: # horizontal
524
+ y = row / 2 + 0.5
525
+ a = (col + 1) / 2
526
+ ax.plot([a - 0.5, a + 0.5], [y, y], color="0.25", linewidth=4,
527
+ zorder=4, solid_capstyle="butt")
528
+
529
+ # carrots
530
+ for (a, s), n in frame["world"].items():
531
+ ax.add_patch(Circle((a, s), 0.17, color=UMN_GOLD,
532
+ ec="0.3", lw=1.0, zorder=2))
533
+ if n > 1:
534
+ ax.text(a, s, str(n), ha="center", va="center",
535
+ fontsize=8, fontweight="bold", zorder=3)
536
+
537
+ # trace
538
+ if len(frame["trace"]) > 1:
539
+ xs = [p[0] for p in frame["trace"]]
540
+ ys = [p[1] for p in frame["trace"]]
541
+ ax.plot(xs, ys, color=UMN_MAROON, linewidth=1.8, alpha=0.55, zorder=1)
542
+
543
+ _draw_gopher(ax, frame["avenue"], frame["street"], frame["dir"], UMN_MAROON)
544
+
545
+ head = title or (f"steps: {frame['steps']} carrots carried: "
546
+ f"{frame['carried']}")
547
+ ax.set_title(head, fontsize=10)
548
+
549
+
550
+ def show(fps=3, figsize=None, controls=False):
551
+ """Replay the recorded run as an inline animation, and start it playing.
552
+
553
+ The animation is **displayed as a side effect**, not returned, so it appears
554
+ wherever you call it -- including with `print()` lines after it. (Returning
555
+ it would rely on Jupyter auto-displaying the last expression of the cell,
556
+ which silently drops the animation the moment anything follows.)
557
+
558
+ The player's slider and buttons are hidden by default; pass
559
+ ``controls=True`` to get them back.
560
+
561
+ Outside a notebook there is nothing to animate into, so the final frame is
562
+ returned as a figure instead.
563
+ """
564
+ world = get_world()
565
+ if not _frames:
566
+ raise GopherError("Nothing to show -- create a Gopher and move it first.")
567
+ if figsize is None:
568
+ figsize = _auto_figsize(world)
569
+
570
+ # Decide *before* building anything: outside a notebook the animation would
571
+ # be built and thrown away, which also trips a matplotlib warning.
572
+ try:
573
+ from IPython import get_ipython
574
+ from IPython.display import HTML, display
575
+ in_notebook = get_ipython() is not None
576
+ except ImportError:
577
+ in_notebook = False
578
+ if not in_notebook:
579
+ return show_final(figsize=figsize)
580
+
581
+ plt.close("all")
582
+ fig, ax = plt.subplots(figsize=figsize)
583
+ plt.close(fig) # keep the static figure out of the output
584
+
585
+ def update(i):
586
+ _draw_frame(ax, _frames[i], world)
587
+ return ax.patches + ax.lines
588
+
589
+ anim = animation.FuncAnimation(
590
+ fig, update, frames=len(_frames),
591
+ interval=max(1000 // max(fps, 1), 1), blit=False, repeat=True)
592
+ # Keep a reference: matplotlib warns (and can garbage-collect the animation)
593
+ # if nothing holds on to it.
594
+ global _last_anim
595
+ _last_anim = anim
596
+
597
+ display(HTML(_present(anim.to_jshtml(default_mode="once"), controls)))
598
+ return None
599
+
600
+
601
+ def show_final(figsize=None):
602
+ """Draw just the last frame. Useful for a static figure in slides or a PDF."""
603
+ world = get_world()
604
+ if not _frames:
605
+ raise GopherError("Nothing to show -- create a Gopher and move it first.")
606
+ if figsize is None:
607
+ figsize = _auto_figsize(world)
608
+ plt.close("all") # do not leak a figure per call
609
+ fig, ax = plt.subplots(figsize=figsize)
610
+ _draw_frame(ax, _frames[-1], world, title="final state")
611
+ return fig
@@ -0,0 +1,5 @@
1
+ avenues = 5
2
+ streets = 5
3
+ robot = (1, 1, 'E', 1)
4
+ walls = []
5
+ coins = {}
@@ -0,0 +1,14 @@
1
+ avenues = 7
2
+ streets = 7
3
+ robot = (1, 1, 'E', 1)
4
+ walls = [
5
+ (6, 13),
6
+ (6, 11),
7
+ (6, 9),
8
+ (13, 6),
9
+ (11, 6),
10
+ (9, 6),
11
+ (7, 6),
12
+ (6, 7)
13
+ ]
14
+ coins = {}
@@ -0,0 +1,15 @@
1
+ avenues = 7
2
+ streets = 7
3
+ robot = (1, 1, 'E', 1)
4
+ walls = [
5
+ (2, 1),
6
+ (3, 2),
7
+ (5, 2),
8
+ (6, 3),
9
+ (6, 5),
10
+ (6, 7),
11
+ (6, 9),
12
+ (6, 11),
13
+ (6, 13)
14
+ ]
15
+ coins = {}
@@ -0,0 +1,42 @@
1
+ avenues = 7
2
+ streets = 7
3
+ robot = (1, 1, 'E', 0)
4
+ walls = []
5
+ coins = {
6
+ (3, 3): 1,
7
+ (3, 2): 1,
8
+ (3, 1): 1,
9
+ (5, 6): 1,
10
+ (5, 1): 1,
11
+ (3, 6): 1,
12
+ (5, 3): 1,
13
+ (5, 2): 1,
14
+ (7, 6): 1,
15
+ (7, 5): 1,
16
+ (7, 4): 1,
17
+ (7, 3): 1,
18
+ (7, 2): 1,
19
+ (7, 1): 1,
20
+ (3, 5): 1,
21
+ (3, 4): 1,
22
+ (2, 4): 1,
23
+ (2, 5): 1,
24
+ (2, 6): 1,
25
+ (2, 1): 1,
26
+ (2, 2): 1,
27
+ (2, 3): 1,
28
+ (4, 6): 1,
29
+ (4, 4): 1,
30
+ (4, 5): 1,
31
+ (4, 2): 1,
32
+ (4, 3): 1,
33
+ (4, 1): 1,
34
+ (6, 1): 1,
35
+ (6, 2): 1,
36
+ (6, 3): 1,
37
+ (6, 4): 1,
38
+ (6, 5): 1,
39
+ (6, 6): 1,
40
+ (5, 5): 1,
41
+ (5, 4): 1
42
+ }
@@ -0,0 +1,36 @@
1
+ avenues = 7
2
+ streets = 7
3
+ robot = (1, 1, 'E', 0)
4
+ walls = []
5
+ coins = {
6
+ (7, 3): 1,
7
+ (6, 6): 1,
8
+ (5, 6): 1,
9
+ (3, 2): 1,
10
+ (2, 1): 1,
11
+ (6, 2): 1,
12
+ (5, 1): 1,
13
+ (2, 5): 1,
14
+ (7, 2): 1,
15
+ (7, 6): 1,
16
+ (4, 4): 1,
17
+ (3, 6): 1,
18
+ (2, 2): 1,
19
+ (3, 5): 1,
20
+ (4, 1): 1,
21
+ (6, 4): 1,
22
+ (5, 4): 1,
23
+ (7, 1): 1,
24
+ (4, 5): 1,
25
+ (5, 5): 1,
26
+ (2, 3): 1,
27
+ (4, 2): 1,
28
+ (6, 5): 1,
29
+ (5, 3): 1,
30
+ (4, 6): 1,
31
+ (3, 4): 1,
32
+ (6, 1): 1,
33
+ (7, 4): 1,
34
+ (4, 3): 1,
35
+ (2, 4): 1
36
+ }
@@ -0,0 +1,36 @@
1
+ avenues = 7
2
+ streets = 7
3
+ robot = (1, 1, 'E', 99)
4
+ walls = []
5
+ coins = {
6
+ (7, 3): 1,
7
+ (6, 6): 1,
8
+ (5, 6): 1,
9
+ (3, 2): 1,
10
+ (2, 1): 1,
11
+ (6, 2): 1,
12
+ (5, 1): 2,
13
+ (2, 5): 1,
14
+ (7, 2): 1,
15
+ (5, 5): 1,
16
+ (7, 6): 1,
17
+ (4, 4): 1,
18
+ (3, 6): 1,
19
+ (2, 2): 2,
20
+ (3, 5): 1,
21
+ (4, 1): 1,
22
+ (6, 4): 1,
23
+ (5, 4): 1,
24
+ (7, 1): 1,
25
+ (4, 5): 1,
26
+ (2, 3): 1,
27
+ (4, 2): 1,
28
+ (6, 5): 2,
29
+ (5, 3): 2,
30
+ (4, 6): 1,
31
+ (6, 1): 1,
32
+ (7, 4): 1,
33
+ (4, 3): 1,
34
+ (3, 4): 2,
35
+ (2, 4): 1
36
+ }
@@ -0,0 +1,42 @@
1
+ avenues = 12
2
+ streets = 12
3
+ robot = (1, 1, 'E', 36)
4
+ walls = []
5
+ coins = {
6
+ (7, 3): 1,
7
+ (6, 10): 1,
8
+ (6, 6): 1,
9
+ (2, 8): 1,
10
+ (10, 6): 1,
11
+ (7, 7): 1,
12
+ (4, 6): 1,
13
+ (6, 2): 1,
14
+ (7, 11): 1,
15
+ (3, 7): 1,
16
+ (10, 8): 1,
17
+ (5, 5): 1,
18
+ (4, 4): 1,
19
+ (8, 10): 1,
20
+ (4, 8): 1,
21
+ (8, 6): 1,
22
+ (5, 3): 1,
23
+ (9, 7): 1,
24
+ (4, 10): 1,
25
+ (2, 6): 1,
26
+ (5, 11): 1,
27
+ (5, 9): 1,
28
+ (7, 5): 1,
29
+ (6, 12): 1,
30
+ (6, 4): 1,
31
+ (3, 5): 1,
32
+ (11, 7): 1,
33
+ (6, 8): 1,
34
+ (5, 7): 1,
35
+ (9, 9): 1,
36
+ (8, 8): 1,
37
+ (7, 9): 1,
38
+ (1, 7): 1,
39
+ (9, 5): 1,
40
+ (3, 9): 1,
41
+ (8, 4): 1
42
+ }
@@ -0,0 +1,12 @@
1
+ avenues = 10
2
+ streets = 10
3
+ robot = (1, 1, 'E', 0)
4
+ walls = [
5
+ (4, 1),
6
+ (8, 1),
7
+ (12, 1),
8
+ (16, 1)
9
+ ]
10
+ coins = {
11
+ (10, 1): 1
12
+ }
@@ -0,0 +1,12 @@
1
+ avenues = 10
2
+ streets = 10
3
+ robot = (1, 1, 'E', 0)
4
+ walls = [
5
+ (4, 1),
6
+ (8, 1),
7
+ (12, 1),
8
+ (16, 1)
9
+ ]
10
+ coins = {
11
+ (7, 1): 1
12
+ }
@@ -0,0 +1,15 @@
1
+ avenues = 10
2
+ streets = 10
3
+ robot = (1, 1, 'E', 0)
4
+ walls = [
5
+ (4, 1),
6
+ (8, 1),
7
+ (16, 1),
8
+ (2, 1),
9
+ (10, 1),
10
+ (18, 1),
11
+ (12, 1)
12
+ ]
13
+ coins = {
14
+ (10, 1): 1
15
+ }
@@ -0,0 +1,18 @@
1
+ avenues = 10
2
+ streets = 10
3
+ robot = (1, 1, 'E', 0)
4
+ walls = [
5
+ (4, 1),
6
+ (8, 1),
7
+ (16, 1),
8
+ (2, 1),
9
+ (10, 1),
10
+ (18, 1),
11
+ (12, 1),
12
+ (4, 3),
13
+ (10, 3),
14
+ (10, 5)
15
+ ]
16
+ coins = {
17
+ (10, 1): 1
18
+ }
@@ -0,0 +1,32 @@
1
+ avenues = 10
2
+ streets = 10
3
+ robot = (1, 1, 'E', 0)
4
+ walls = [
5
+ (10, 1),
6
+ (10, 3),
7
+ (10, 5),
8
+ (1, 10),
9
+ (3, 10),
10
+ (5, 10),
11
+ (2, 1),
12
+ (2, 3),
13
+ (1, 6),
14
+ (3, 6),
15
+ (4, 5),
16
+ (4, 3),
17
+ (5, 2),
18
+ (6, 3),
19
+ (7, 8),
20
+ (5, 8),
21
+ (2, 7),
22
+ (7, 10),
23
+ (8, 7),
24
+ (9, 6),
25
+ (8, 3),
26
+ (9, 4),
27
+ (9, 10),
28
+ (10, 9)
29
+ ]
30
+ coins = {
31
+ (6, 4): 1
32
+ }
@@ -0,0 +1,18 @@
1
+ avenues = 10
2
+ streets = 10
3
+ robot = (1, 1, 'E', 1)
4
+ walls = [
5
+ (4, 1),
6
+ (5, 2),
7
+ (7, 2),
8
+ (8, 3),
9
+ (9, 4),
10
+ (11, 4),
11
+ (12, 5),
12
+ (13, 6),
13
+ (15, 6),
14
+ (16, 7),
15
+ (17, 8),
16
+ (19, 8)
17
+ ]
18
+ coins = {}
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: cege-gopher
3
+ Version: 0.1.0
4
+ Summary: A Karel-style grid robot for teaching Python, themed as Goldy Gopher. Records and replays, so it works in Colab.
5
+ Author-email: Seongjin Choi <chois@umn.edu>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Seongjin Choi
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Keywords: education,karel,robot,colab,cege3101
29
+ Classifier: Intended Audience :: Education
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Topic :: Education
32
+ Requires-Python: >=3.9
33
+ Description-Content-Type: text/markdown
34
+ License-File: LICENSE
35
+ Requires-Dist: matplotlib>=3.5
36
+ Dynamic: license-file
37
+
38
+ # cege_gopher
39
+
40
+ A Karel-style grid robot for teaching introductory Python, themed as Goldy
41
+ Gopher. Written for **CEGE 3101 — Computer Applications I** at the University of
42
+ Minnesota.
43
+
44
+ Ported from the MATLAB robot used in earlier offerings of the course, which was
45
+ itself a port of the KAIST CS101 HUBO exercise
46
+ ([otfried/cs101](https://github.com/otfried/cs101)) — Karel the Robot, at the
47
+ root of it.
48
+
49
+ ## Why this exists
50
+
51
+ The original libraries open a live window and animate each command as it runs.
52
+ **Google Colab cannot host one.**
53
+
54
+ So this version **records, then replays**. Every command appends a frame to a
55
+ buffer; nothing is drawn until `show()`, which renders the whole run as an
56
+ inline animation:
57
+
58
+ > write the whole program → run the cell → watch it play back
59
+
60
+ Students never drive the robot interactively; every program is pre-coded, which
61
+ is also how the homework is graded.
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ pip install cege-gopher
67
+ ```
68
+
69
+ Or, from the course repository:
70
+
71
+ ```bash
72
+ pip install "git+https://github.com/benchoi93/CEGE3101_Computer_Applications.git#subdirectory=packages/cege-gopher"
73
+ ```
74
+
75
+ The 13 world files ship **inside the package**, so nothing needs uploading —
76
+ `load_world("harvest1.wld")` just works.
77
+
78
+ ## Use
79
+
80
+ ```python
81
+ from cege_gopher import *
82
+
83
+ load_world("harvest1.wld") # or: create_world(10, 10)
84
+ goldy = Gopher()
85
+ goldy.set_trace("maroon")
86
+
87
+ while goldy.front_is_clear():
88
+ if goldy.on_carrot():
89
+ goldy.pick_carrot()
90
+ goldy.move()
91
+
92
+ show() # the animation renders here
93
+ ```
94
+
95
+ `list_worlds()` returns the bundled world names.
96
+
97
+ ## API
98
+
99
+ | | |
100
+ |---|---|
101
+ | Module | `create_world(avenues, streets)`, `load_world(name)`, `list_worlds()`, `show(fps=)`, `show_final()`, `get_world()` |
102
+ | Move | `move()`, `turn_left()`, `turn_right()` |
103
+ | Carrots | `pick_carrot()`, `drop_carrot()` |
104
+ | Sense | `front_is_clear()`, `left_is_clear()`, `right_is_clear()`, `facing_north()`, `on_carrot()`, `carries_carrots()` |
105
+ | Display | `set_trace(color)`, `get_pos()` |
106
+
107
+ `Robot` is an alias for `Gopher`, and `pick_beeper`/`pick_coin` for
108
+ `pick_carrot`, so code written against `cs1robots` or the MATLAB version still
109
+ runs.
110
+
111
+ ## A bug fixed in this port
112
+
113
+ The MATLAB version had `leftIsClear` and `rightIsClear` **swapped** — facing
114
+ east it checked south for "left", when `turnLeft` rotates counter-clockwise
115
+ (E→N) so left of east is north. This port uses `left = (-dy, dx)`,
116
+ `right = (dy, -dx)`.
117
+
118
+ ## License
119
+
120
+ MIT.
@@ -0,0 +1,26 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/cege_gopher/__init__.py
5
+ src/cege_gopher.egg-info/PKG-INFO
6
+ src/cege_gopher.egg-info/SOURCES.txt
7
+ src/cege_gopher.egg-info/dependency_links.txt
8
+ src/cege_gopher.egg-info/requires.txt
9
+ src/cege_gopher.egg-info/top_level.txt
10
+ src/cege_gopher/sprites/E.png
11
+ src/cege_gopher/sprites/N.png
12
+ src/cege_gopher/sprites/S.png
13
+ src/cege_gopher/sprites/W.png
14
+ src/cege_gopher/worlds/around1.wld
15
+ src/cege_gopher/worlds/around2.wld
16
+ src/cege_gopher/worlds/around3.wld
17
+ src/cege_gopher/worlds/harvest1.wld
18
+ src/cege_gopher/worlds/harvest2.wld
19
+ src/cege_gopher/worlds/harvest3.wld
20
+ src/cege_gopher/worlds/harvest5.wld
21
+ src/cege_gopher/worlds/hurdles1.wld
22
+ src/cege_gopher/worlds/hurdles2.wld
23
+ src/cege_gopher/worlds/hurdles3.wld
24
+ src/cege_gopher/worlds/hurdles4.wld
25
+ src/cege_gopher/worlds/maze1.wld
26
+ src/cege_gopher/worlds/newspaper.wld
@@ -0,0 +1 @@
1
+ matplotlib>=3.5
@@ -0,0 +1 @@
1
+ cege_gopher