lazy-bob 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.
lazy_bob-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anant Dhavale
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,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: lazy-bob
3
+ Version: 0.1.0
4
+ Summary: A tiny terminal runner where Lazy Bob avoids life's hurdles with one reluctant jump.
5
+ Author: Anant
6
+ License: MIT
7
+ Keywords: game,ascii,terminal,runner,pypi
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console :: Curses
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Games/Entertainment :: Arcade
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # Lazy Bob
24
+
25
+ `Lazy Bob` is a tiny terminal game for people who appreciate low-effort heroics.
26
+
27
+ Bob is too laid back to run. He only jumps when absolutely necessary.
28
+ Your job is to hit `space` just in time so he avoids the next hurdle.
29
+
30
+ But you see, Bob is a philosopher too. Do not forget to read his takes on life ( known as Bob-isms) as he lazilty jumps through the hurdles of life !
31
+
32
+ Everything is intentionally bare-bones:
33
+
34
+ - terminal only
35
+ - ASCII only
36
+ - one button that matters
37
+ - no external dependencies
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install lazy-bob
43
+ ```
44
+
45
+ For local development:
46
+
47
+ ```bash
48
+ pip install -e .
49
+ ```
50
+
51
+ ## Run
52
+
53
+ ```bash
54
+ lazy-bob
55
+ ```
56
+
57
+ ## Controls
58
+
59
+ - `space`: jump
60
+ - `r`: restart after a crash
61
+ - `q`: quit
62
+
63
+ ## Notes
64
+
65
+ - The game uses Python's built-in `curses` module, so it works best on macOS and Linux terminals.
66
+ - Bob's best score is saved locally in `~/.lazy_bob_score`.
67
+
68
+ Drop me a line if you like Lazy Bob at anantdhavale@gmail.com.
69
+
70
+ Copyright for the Lazy Bob character and Bob-isms © Anant Dhavale.
@@ -0,0 +1,48 @@
1
+ # Lazy Bob
2
+
3
+ `Lazy Bob` is a tiny terminal game for people who appreciate low-effort heroics.
4
+
5
+ Bob is too laid back to run. He only jumps when absolutely necessary.
6
+ Your job is to hit `space` just in time so he avoids the next hurdle.
7
+
8
+ But you see, Bob is a philosopher too. Do not forget to read his takes on life ( known as Bob-isms) as he lazilty jumps through the hurdles of life !
9
+
10
+ Everything is intentionally bare-bones:
11
+
12
+ - terminal only
13
+ - ASCII only
14
+ - one button that matters
15
+ - no external dependencies
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install lazy-bob
21
+ ```
22
+
23
+ For local development:
24
+
25
+ ```bash
26
+ pip install -e .
27
+ ```
28
+
29
+ ## Run
30
+
31
+ ```bash
32
+ lazy-bob
33
+ ```
34
+
35
+ ## Controls
36
+
37
+ - `space`: jump
38
+ - `r`: restart after a crash
39
+ - `q`: quit
40
+
41
+ ## Notes
42
+
43
+ - The game uses Python's built-in `curses` module, so it works best on macOS and Linux terminals.
44
+ - Bob's best score is saved locally in `~/.lazy_bob_score`.
45
+
46
+ Drop me a line if you like Lazy Bob at anantdhavale@gmail.com.
47
+
48
+ Copyright for the Lazy Bob character and Bob-isms © Anant Dhavale.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "lazy-bob"
7
+ version = "0.1.0"
8
+ description = "A tiny terminal runner where Lazy Bob avoids life's hurdles with one reluctant jump."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Anant" }
14
+ ]
15
+ keywords = ["game", "ascii", "terminal", "runner", "pypi"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console :: Curses",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Games/Entertainment :: Arcade"
27
+ ]
28
+
29
+ [project.scripts]
30
+ lazy-bob = "lazy_bob:main"
31
+
32
+ [tool.setuptools]
33
+ package-dir = { "" = "src" }
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .game import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,5 @@
1
+ from .game import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,462 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ import random
6
+ import time
7
+
8
+
9
+ FRAME_TIME = 0.05
10
+ GRAVITY = 0.42
11
+ JUMP_VELOCITY = -2.7
12
+ BASE_SPEED = 1.0
13
+ MAX_SPEED = 2.8
14
+ SCORE_FILE = Path.home() / ".lazy_bob_score"
15
+ MIN_WIDTH = 44
16
+ MIN_HEIGHT = 16
17
+ JUMP_EXCLAMATION_TICKS = 10
18
+ BOBISM_TICKS = 26
19
+
20
+ BOB_SPRITES: dict[str, list[str]] = {
21
+ "idle_a": [
22
+ " o ",
23
+ "/|_",
24
+ "/ \\",
25
+ ],
26
+ "idle_b": [
27
+ " o ",
28
+ "/|_",
29
+ "/| ",
30
+ ],
31
+ "crouch": [
32
+ " o ",
33
+ "/|_",
34
+ "_/ ",
35
+ ],
36
+ "jump": [
37
+ " o ",
38
+ "_|\\",
39
+ " /\\",
40
+ ],
41
+ "land": [
42
+ " o ",
43
+ "/|_",
44
+ "_\\\\",
45
+ ],
46
+ "bonk": [
47
+ " x ",
48
+ "_|_",
49
+ "/ \\",
50
+ ],
51
+ }
52
+
53
+ OBSTACLE_SHAPES: list[list[str]] = [
54
+ ["|"],
55
+ ["#", "#"],
56
+ ["##", "##"],
57
+ ["/|", "##"],
58
+ ["[]"],
59
+ ["|/", "##"],
60
+ ["&&"],
61
+ ["&&", "&&"],
62
+ ["&&&"],
63
+ ["& &"],
64
+ ]
65
+
66
+ JUMP_EXCLAMATIONS_CASUAL = [
67
+ "drab",
68
+ "yawn",
69
+ "hmmm",
70
+ "meh",
71
+ ]
72
+
73
+ JUMP_EXCLAMATIONS_URGENT = [
74
+ "oof",
75
+ "ugh",
76
+ "hrmph",
77
+ "barely",
78
+ ]
79
+
80
+ JUMP_EXCLAMATIONS_FAST = [
81
+ "whoa",
82
+ "still fine",
83
+ "too much",
84
+ "why run",
85
+ ]
86
+
87
+ BOBISMS = [
88
+ "Lazies shall inherit the Earth.",
89
+ "Don't try too hard.",
90
+ "Don't sweat. Catch some z's.",
91
+ "Quit making others rich by working too hard.",
92
+ "Living is also chilling.",
93
+ "See, I got through.",
94
+ "Thinkers don't do nothing.",
95
+ "Your job is not your life.",
96
+ "Breathe. Live.",
97
+ "Don't forget livin.",
98
+ "Ease up, Fred!",
99
+ "By doing nothing, you save energy.",
100
+ ]
101
+
102
+
103
+ @dataclass
104
+ class Obstacle:
105
+ x: float
106
+ shape: list[str] = field(default_factory=lambda: ["|"])
107
+ passed: bool = False
108
+ previous_x: float | None = None
109
+
110
+ @property
111
+ def height(self) -> int:
112
+ return len(self.shape)
113
+
114
+ @property
115
+ def width(self) -> int:
116
+ return max((len(row) for row in self.shape), default=1)
117
+
118
+
119
+ @dataclass
120
+ class GameState:
121
+ width: int
122
+ height: int
123
+ bob_x: int
124
+ ground_y: int
125
+ bob_y: float
126
+ bob_velocity: float = 0.0
127
+ score: int = 0
128
+ best_score: int = 0
129
+ ticks: int = 0
130
+ speed: float = BASE_SPEED
131
+ spawn_cooldown: int = 0
132
+ alive: bool = True
133
+ obstacles: list[Obstacle] = field(default_factory=list)
134
+ crouch_ticks: int = 0
135
+ landing_ticks: int = 0
136
+ bonk_ticks: int = 0
137
+ jump_exclamation: str = ""
138
+ jump_exclamation_ticks: int = 0
139
+ bobism: str = ""
140
+ bobism_ticks: int = 0
141
+
142
+ @property
143
+ def bob_row(self) -> int:
144
+ return round(self.bob_y)
145
+
146
+ @property
147
+ def on_ground(self) -> bool:
148
+ return self.bob_row >= self.ground_y
149
+
150
+
151
+ def load_best_score() -> int:
152
+ try:
153
+ return int(SCORE_FILE.read_text(encoding="utf-8").strip() or "0")
154
+ except (FileNotFoundError, ValueError, OSError):
155
+ return 0
156
+
157
+
158
+ def save_best_score(score: int) -> None:
159
+ try:
160
+ SCORE_FILE.write_text(str(score), encoding="utf-8")
161
+ except OSError:
162
+ pass
163
+
164
+
165
+ def create_state(width: int, height: int, best_score: int | None = None) -> GameState:
166
+ best = load_best_score() if best_score is None else best_score
167
+ ground_y = height - 2
168
+ bob_x = max(4, width // 6)
169
+ return GameState(
170
+ width=width,
171
+ height=height,
172
+ bob_x=bob_x,
173
+ ground_y=ground_y,
174
+ bob_y=float(ground_y),
175
+ best_score=best,
176
+ )
177
+
178
+
179
+ def nearest_obstacle_distance(state: GameState) -> float | None:
180
+ ahead_distances = [
181
+ obstacle.x - (state.bob_x + 2)
182
+ for obstacle in state.obstacles
183
+ if obstacle.x + obstacle.width - 1 >= state.bob_x
184
+ ]
185
+ return min(ahead_distances) if ahead_distances else None
186
+
187
+
188
+ def pick_jump_exclamation(state: GameState, rng: random.Random) -> str:
189
+ nearest = nearest_obstacle_distance(state)
190
+ if state.speed >= 2.1:
191
+ pool = JUMP_EXCLAMATIONS_FAST
192
+ elif nearest is not None and nearest < 8:
193
+ pool = JUMP_EXCLAMATIONS_URGENT
194
+ else:
195
+ pool = JUMP_EXCLAMATIONS_CASUAL
196
+ return rng.choice(pool)
197
+
198
+
199
+ def set_bobism(state: GameState, rng: random.Random) -> None:
200
+ state.bobism = rng.choice(BOBISMS)
201
+ state.bobism_ticks = BOBISM_TICKS
202
+
203
+
204
+ def maybe_set_bobism(state: GameState, rng: random.Random) -> None:
205
+ should_crack = state.score > 0 and (state.score % 5 == 0 or rng.random() < 0.18)
206
+ if should_crack:
207
+ set_bobism(state, rng)
208
+
209
+
210
+ def jump(state: GameState, rng: random.Random | None = None) -> None:
211
+ if state.on_ground and state.alive:
212
+ state.crouch_ticks = 2
213
+ state.landing_ticks = 0
214
+ state.bob_velocity = JUMP_VELOCITY
215
+ picker = rng if rng is not None else random
216
+ state.jump_exclamation = pick_jump_exclamation(state, picker)
217
+ state.jump_exclamation_ticks = JUMP_EXCLAMATION_TICKS
218
+ set_bobism(state, picker)
219
+
220
+
221
+ def choose_obstacle_shape(rng: random.Random, score: int) -> list[str]:
222
+ shape_limit = min(len(OBSTACLE_SHAPES), 3 + score // 3)
223
+ return list(rng.choice(OBSTACLE_SHAPES[:shape_limit]))
224
+
225
+
226
+ def maybe_spawn_obstacle(state: GameState, rng: random.Random) -> None:
227
+ if state.spawn_cooldown > 0:
228
+ state.spawn_cooldown -= 1
229
+ return
230
+
231
+ spawn_chance = min(0.1 + state.speed * 0.02, 0.22)
232
+ if rng.random() < spawn_chance:
233
+ shape = choose_obstacle_shape(rng, state.score)
234
+ state.obstacles.append(Obstacle(x=float(state.width - 2), shape=shape))
235
+ state.spawn_cooldown = rng.randint(10, 18)
236
+
237
+
238
+ def move_bob(state: GameState) -> None:
239
+ was_on_ground = state.on_ground
240
+ state.bob_velocity += GRAVITY
241
+ state.bob_y += state.bob_velocity
242
+ if state.bob_y >= state.ground_y:
243
+ state.bob_y = float(state.ground_y)
244
+ if not was_on_ground:
245
+ state.landing_ticks = 2
246
+ state.bob_velocity = 0.0
247
+
248
+ if state.crouch_ticks > 0:
249
+ state.crouch_ticks -= 1
250
+ if state.landing_ticks > 0 and state.on_ground:
251
+ state.landing_ticks -= 1
252
+ if state.bonk_ticks > 0:
253
+ state.bonk_ticks -= 1
254
+ if state.jump_exclamation_ticks > 0:
255
+ state.jump_exclamation_ticks -= 1
256
+ elif state.jump_exclamation:
257
+ state.jump_exclamation = ""
258
+ if state.bobism_ticks > 0:
259
+ state.bobism_ticks -= 1
260
+ elif state.bobism:
261
+ state.bobism = ""
262
+
263
+
264
+ def move_obstacles(state: GameState, rng: random.Random) -> None:
265
+ survivors: list[Obstacle] = []
266
+ for obstacle in state.obstacles:
267
+ obstacle.previous_x = obstacle.x
268
+ obstacle.x -= state.speed
269
+ if not obstacle.passed and obstacle.x + obstacle.width - 1 < state.bob_x:
270
+ obstacle.passed = True
271
+ state.score += 1
272
+ if state.score > state.best_score:
273
+ state.best_score = state.score
274
+ save_best_score(state.best_score)
275
+ maybe_set_bobism(state, rng)
276
+ if obstacle.x + obstacle.width > -1:
277
+ survivors.append(obstacle)
278
+ state.obstacles = survivors
279
+
280
+
281
+ def detect_collision(state: GameState) -> bool:
282
+ bob_left = state.bob_x
283
+ bob_right = state.bob_x + 2
284
+ bob_top = state.bob_row - 2
285
+ bob_bottom = state.bob_row
286
+
287
+ for obstacle in state.obstacles:
288
+ previous_x = obstacle.previous_x if obstacle.previous_x is not None else obstacle.x
289
+ left_edge = min(previous_x, obstacle.x)
290
+ right_edge = max(previous_x, obstacle.x) + obstacle.width - 1
291
+ obstacle_top = state.ground_y - obstacle.height + 1
292
+ obstacle_bottom = state.ground_y
293
+
294
+ x_overlap = not (right_edge < bob_left or left_edge > bob_right)
295
+ y_overlap = not (obstacle_bottom < bob_top or obstacle_top > bob_bottom)
296
+ if not x_overlap or not y_overlap:
297
+ continue
298
+ return True
299
+ return False
300
+
301
+
302
+ def step(state: GameState, rng: random.Random) -> None:
303
+ if not state.alive:
304
+ return
305
+
306
+ state.ticks += 1
307
+ state.speed = min(BASE_SPEED + state.score * 0.05, MAX_SPEED)
308
+ maybe_spawn_obstacle(state, rng)
309
+ move_bob(state)
310
+ move_obstacles(state, rng)
311
+ if detect_collision(state):
312
+ state.alive = False
313
+ state.bonk_ticks = 4
314
+
315
+
316
+ def current_bob_sprite(state: GameState) -> list[str]:
317
+ if not state.alive:
318
+ return BOB_SPRITES["bonk"]
319
+ if state.landing_ticks > 0 and state.on_ground:
320
+ return BOB_SPRITES["land"]
321
+ if not state.on_ground:
322
+ return BOB_SPRITES["jump"]
323
+ if state.crouch_ticks > 0:
324
+ return BOB_SPRITES["crouch"]
325
+ if (state.ticks // 6) % 2 == 0:
326
+ return BOB_SPRITES["idle_a"]
327
+ return BOB_SPRITES["idle_b"]
328
+
329
+
330
+ def draw_text(canvas: list[list[str]], row: int, col: int, text: str) -> None:
331
+ if row < 0 or row >= len(canvas):
332
+ return
333
+ for offset, char in enumerate(text):
334
+ x = col + offset
335
+ if 0 <= x < len(canvas[row]):
336
+ canvas[row][x] = char
337
+
338
+
339
+ def render_lines(state: GameState) -> list[str]:
340
+ canvas = [[" " for _ in range(state.width)] for _ in range(state.height)]
341
+
342
+ for x in range(state.width):
343
+ canvas[state.ground_y + 1][x] = "_"
344
+
345
+ bob_row = max(0, min(state.height - 1, state.bob_row))
346
+ bob_sprite = current_bob_sprite(state)
347
+ bob_top = bob_row - (len(bob_sprite) - 1)
348
+ for sprite_row, sprite_text in enumerate(bob_sprite):
349
+ draw_text(canvas, bob_top + sprite_row, state.bob_x, sprite_text)
350
+
351
+ if state.alive and state.on_ground and state.crouch_ticks == 0 and state.landing_ticks == 0:
352
+ if (state.ticks // 14) % 3 == 0:
353
+ draw_text(canvas, max(0, bob_top), state.bob_x + 4, "z")
354
+ if state.jump_exclamation_ticks > 0 and state.jump_exclamation:
355
+ exclamation_row = max(0, bob_top - 1)
356
+ exclamation_col = min(state.width - len(state.jump_exclamation), state.bob_x + 4)
357
+ draw_text(canvas, exclamation_row, max(0, exclamation_col), state.jump_exclamation)
358
+
359
+ for obstacle in state.obstacles:
360
+ obstacle_x = round(obstacle.x)
361
+ obstacle_top = state.ground_y - obstacle.height + 1
362
+ for row_offset, row_text in enumerate(obstacle.shape):
363
+ draw_text(canvas, obstacle_top + row_offset, obstacle_x, row_text)
364
+
365
+ title = " Lazy Bob "
366
+ controls = "space jump q quit"
367
+ score = f"score {state.score} best {state.best_score} speed {state.speed:.1f}"
368
+ if state.width > len(title):
369
+ start = max(0, (state.width - len(title)) // 2)
370
+ for index, char in enumerate(title):
371
+ canvas[0][start + index] = char
372
+
373
+ for row, text in ((1, controls), (2, score)):
374
+ if row < state.height:
375
+ for index, char in enumerate(text[: state.width]):
376
+ canvas[row][index] = char
377
+
378
+ if state.bobism_ticks > 0 and state.bobism:
379
+ bobism_row = max(4, bob_top - 2)
380
+ bobism_col = min(state.width - len(state.bobism), max(0, state.bob_x - 1))
381
+ draw_text(canvas, bobism_row, bobism_col, state.bobism)
382
+
383
+ if not state.alive:
384
+ message = " bonk. press r to retry or q to quit "
385
+ if state.width > len(message):
386
+ start = (state.width - len(message)) // 2
387
+ for index, char in enumerate(message):
388
+ canvas[state.height // 2][start + index] = char
389
+
390
+ return ["".join(row).rstrip() for row in canvas]
391
+
392
+
393
+ def handle_input(key: int, state: GameState, best_score: int) -> GameState | None:
394
+ if key in (-1,):
395
+ return state
396
+ if key in (ord("q"), ord("Q")):
397
+ return None
398
+ if key in (ord(" "),):
399
+ if state.alive:
400
+ jump(state)
401
+ return state
402
+ return create_state(state.width, state.height, best_score=best_score)
403
+ if key in (ord("r"), ord("R")) and not state.alive:
404
+ return create_state(state.width, state.height, best_score=best_score)
405
+ return state
406
+
407
+
408
+ def run_curses(stdscr) -> None:
409
+ import curses
410
+
411
+ curses.curs_set(0)
412
+ stdscr.nodelay(True)
413
+ stdscr.timeout(0)
414
+
415
+ rng = random.Random()
416
+
417
+ while True:
418
+ height, width = stdscr.getmaxyx()
419
+ if width < MIN_WIDTH or height < MIN_HEIGHT:
420
+ stdscr.erase()
421
+ stdscr.addstr(0, 0, f"Need at least {MIN_WIDTH}x{MIN_HEIGHT}. Current: {width}x{height}")
422
+ stdscr.addstr(1, 0, "Resize the terminal, then press q to leave.")
423
+ stdscr.refresh()
424
+ key = stdscr.getch()
425
+ if key in (ord("q"), ord("Q")):
426
+ return
427
+ time.sleep(0.1)
428
+ continue
429
+
430
+ state = create_state(width, height)
431
+ while True:
432
+ height, width = stdscr.getmaxyx()
433
+ if width != state.width or height != state.height:
434
+ break
435
+
436
+ loop_start = time.monotonic()
437
+ next_state = handle_input(stdscr.getch(), state, state.best_score)
438
+ if next_state is None:
439
+ return
440
+ state = next_state
441
+ step(state, rng)
442
+
443
+ stdscr.erase()
444
+ for row_index, line in enumerate(render_lines(state)):
445
+ try:
446
+ stdscr.addstr(row_index, 0, line)
447
+ except curses.error:
448
+ pass
449
+ stdscr.refresh()
450
+
451
+ remaining = FRAME_TIME - (time.monotonic() - loop_start)
452
+ if remaining > 0:
453
+ time.sleep(remaining)
454
+
455
+
456
+ def main() -> None:
457
+ try:
458
+ import curses
459
+ except ImportError as exc:
460
+ raise SystemExit("Lazy Bob needs the standard curses module to run in the terminal.") from exc
461
+
462
+ curses.wrapper(run_curses)
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: lazy-bob
3
+ Version: 0.1.0
4
+ Summary: A tiny terminal runner where Lazy Bob avoids life's hurdles with one reluctant jump.
5
+ Author: Anant
6
+ License: MIT
7
+ Keywords: game,ascii,terminal,runner,pypi
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console :: Curses
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Games/Entertainment :: Arcade
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # Lazy Bob
24
+
25
+ `Lazy Bob` is a tiny terminal game for people who appreciate low-effort heroics.
26
+
27
+ Bob is too laid back to run. He only jumps when absolutely necessary.
28
+ Your job is to hit `space` just in time so he avoids the next hurdle.
29
+
30
+ But you see, Bob is a philosopher too. Do not forget to read his takes on life ( known as Bob-isms) as he lazilty jumps through the hurdles of life !
31
+
32
+ Everything is intentionally bare-bones:
33
+
34
+ - terminal only
35
+ - ASCII only
36
+ - one button that matters
37
+ - no external dependencies
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install lazy-bob
43
+ ```
44
+
45
+ For local development:
46
+
47
+ ```bash
48
+ pip install -e .
49
+ ```
50
+
51
+ ## Run
52
+
53
+ ```bash
54
+ lazy-bob
55
+ ```
56
+
57
+ ## Controls
58
+
59
+ - `space`: jump
60
+ - `r`: restart after a crash
61
+ - `q`: quit
62
+
63
+ ## Notes
64
+
65
+ - The game uses Python's built-in `curses` module, so it works best on macOS and Linux terminals.
66
+ - Bob's best score is saved locally in `~/.lazy_bob_score`.
67
+
68
+ Drop me a line if you like Lazy Bob at anantdhavale@gmail.com.
69
+
70
+ Copyright for the Lazy Bob character and Bob-isms © Anant Dhavale.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/lazy_bob/__init__.py
5
+ src/lazy_bob/__main__.py
6
+ src/lazy_bob/game.py
7
+ src/lazy_bob.egg-info/PKG-INFO
8
+ src/lazy_bob.egg-info/SOURCES.txt
9
+ src/lazy_bob.egg-info/dependency_links.txt
10
+ src/lazy_bob.egg-info/entry_points.txt
11
+ src/lazy_bob.egg-info/top_level.txt
12
+ tests/test_game.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lazy-bob = lazy_bob:main
@@ -0,0 +1 @@
1
+ lazy_bob
@@ -0,0 +1,82 @@
1
+ import random
2
+ import unittest
3
+
4
+ from lazy_bob.game import (
5
+ BOB_SPRITES,
6
+ JUMP_EXCLAMATIONS_CASUAL,
7
+ JUMP_EXCLAMATIONS_FAST,
8
+ Obstacle,
9
+ create_state,
10
+ current_bob_sprite,
11
+ detect_collision,
12
+ jump,
13
+ step,
14
+ )
15
+
16
+
17
+ class LazyBobTests(unittest.TestCase):
18
+ def test_jump_only_when_grounded(self) -> None:
19
+ state = create_state(48, 16, best_score=0)
20
+ jump(state)
21
+ self.assertLess(state.bob_velocity, 0)
22
+
23
+ airborne = create_state(48, 16, best_score=0)
24
+ airborne.bob_y -= 2
25
+ jump(airborne)
26
+ self.assertEqual(airborne.bob_velocity, 0)
27
+
28
+ def test_collision_detected_on_ground(self) -> None:
29
+ state = create_state(48, 16, best_score=0)
30
+ state.obstacles.append(Obstacle(x=float(state.bob_x)))
31
+ self.assertTrue(detect_collision(state))
32
+
33
+ def test_collision_detected_when_obstacle_skips_past_bob(self) -> None:
34
+ state = create_state(48, 16, best_score=0)
35
+ state.obstacles.append(
36
+ Obstacle(x=float(state.bob_x) - 1.2, previous_x=float(state.bob_x) + 1.4)
37
+ )
38
+ self.assertTrue(detect_collision(state))
39
+
40
+ def test_step_awards_score_after_passing(self) -> None:
41
+ state = create_state(48, 16, best_score=0)
42
+ state.obstacles.append(Obstacle(x=float(state.bob_x) - 0.1))
43
+ step(state, random.Random(1))
44
+ self.assertEqual(state.score, 1)
45
+ self.assertEqual(state.best_score, 1)
46
+
47
+ def test_jump_switches_bob_into_air_pose(self) -> None:
48
+ state = create_state(48, 16, best_score=0)
49
+ jump(state, random.Random(1))
50
+ step(state, random.Random(1))
51
+ self.assertEqual(current_bob_sprite(state), BOB_SPRITES["jump"])
52
+
53
+ def test_wide_obstacle_counts_for_collision(self) -> None:
54
+ state = create_state(48, 16, best_score=0)
55
+ state.obstacles.append(Obstacle(x=float(state.bob_x + 1), shape=["[]"]))
56
+ self.assertTrue(detect_collision(state))
57
+
58
+ def test_jump_sets_a_lazy_exclamation(self) -> None:
59
+ state = create_state(48, 16, best_score=0)
60
+ jump(state, random.Random(1))
61
+ self.assertTrue(state.jump_exclamation)
62
+ self.assertGreater(state.jump_exclamation_ticks, 0)
63
+ self.assertTrue(state.bobism)
64
+ self.assertGreater(state.bobism_ticks, 0)
65
+
66
+ def test_fast_jump_uses_faster_exclamation_pool(self) -> None:
67
+ state = create_state(48, 16, best_score=0)
68
+ state.speed = 2.2
69
+ jump(state, random.Random(1))
70
+ self.assertIn(state.jump_exclamation, JUMP_EXCLAMATIONS_FAST)
71
+
72
+ def test_scoring_can_trigger_bobism(self) -> None:
73
+ state = create_state(48, 16, best_score=0)
74
+ state.score = 4
75
+ state.obstacles.append(Obstacle(x=float(state.bob_x) - 0.1))
76
+ step(state, random.Random(1))
77
+ self.assertEqual(state.score, 5)
78
+ self.assertTrue(state.bobism)
79
+
80
+
81
+ if __name__ == "__main__":
82
+ unittest.main()