1 if __name__ == '__main__':
7 from collections.abc import Iterable, Iterator
8 from typing import Any, Optional, Union
17 SchemeValue = Any # TODO
20 class Locked(SpecialForm):
21 def __init__(self, name: Symbol):
22 def no_access(_vals, _env):
23 raise SchemeError(f"You don't have access to '{name}' yet!")
24 super().__init__(name, no_access)
27 return '#[locked ' + self.name + ']'
30 def unlock(name: SchemeValue, quiet: bool = False):
31 check_type(name, scheme_symbolp, 0, '#unlock')
32 if isinstance(TheLockingFrame.bindings.get(name, None), Locked):
33 del TheLockingFrame.bindings[name]
36 raise SchemeError(f"'{name}' is not locked")
38 def do_undef(vals: SchemeList, env: Frame):
39 check_form(vals, 1, 1)
41 if not scheme_symbolp(name):
42 raise SchemeError("can only undefine symbols")
43 if name in env.bindings:
44 del env.bindings[name]
46 raise SchemeError(f"'{name}' not found in current frame (can't undef from parents)")
48 text_wrapper = textwrap.TextWrapper()
51 def show_problem(which: SchemeValue):
52 check_type(which, scheme_stringp, 0, '#solve')
54 problem = Problems.get(which, None)
56 raise SchemeError(f"{which} is not a valid problem number")
60 text_wrapper.width = shutil.get_terminal_size().columns
61 for paragraph in problem.prompt().splitlines():
62 if not paragraph.startswith(" "):
63 paragraph = text_wrapper.fill(paragraph)
69 print("Welcome to SICPelago!")
71 print(f"Your goal is to solve problems and collect the {len(MacGuffins)} sacred relics.")
72 print("At the start of the game, you only have access to `lambda` and `quote`.")
73 print("Use the commands below to guide you on your way:")
75 print(" (#help) - show this message")
76 print(" (#progress) - show how many relics you have collected")
77 print(" (#inventory) - list all abilities you have access to")
78 print(" (#list) - list all problems")
79 print(' (#show "1.1") - print the prompt for a specific problem')
80 print(' (#solve "1.1" (lambda (x) x)) - attempt to solve a problem')
81 print("\tAll problem solutions take the form of a single function.")
82 print(" (#unlock 'cons) - unlock an ability directly (i.e. cheat)")
87 @primitive("#progress")
89 print(f"Relics collected: {macguffinsCollected}/{len(MacGuffins)}")
92 def show_all_problems():
94 bullet = "•" if ProblemRewards.get(k, None) else "✔︎"
95 print(f"{bullet} {Problems[k]}")
97 @primitive("#inventory")
99 unlocked = set(TheGlobalFrame.bindings.keys()) - TheLockingFrame.bindings.keys()
100 for name in sorted(unlocked):
101 if name.startswith('#'):
110 print(f"\tYOU COLLECTED THE {len(MacGuffins)} SACRED RELICS!")
111 print("\tYour time was: idk I didn't set this up yet")
112 print("\tThank you for playing SICPelago!")
113 print("\tYou may now (exit)")
117 def receive_reward(reward):
118 if scheme_symbolp(reward):
119 return unlock(reward, quiet=True)
120 if isinstance(reward, MacGuffin):
121 global macguffinsCollected
122 macguffinsCollected += 1
123 if macguffinsCollected == len(MacGuffins):
127 problem_solved_hook = lambda problem: False
130 def solve(which: SchemeValue, solution: SchemeValue):
131 check_type(which, scheme_stringp, 0, '#solve')
132 check_type(solution, scheme_procedurep, 1, '#solve')
135 if which not in ProblemRewards:
136 raise SchemeError(f"{which} is not a valid problem number")
137 rewards = ProblemRewards[which]
139 raise SchemeError(f"already solved problem {which}")
141 if solution is TheGlobalFrame.bindings['#win']:
142 pass # For testing purposes
144 test_env = TheGlobalFrame.make_call_frame(Pair("f", nil), Pair(solution, nil))
145 for (input, expected) in Problems[which].data['tests'].items():
146 expected = scheme_eval(read_line(expected), env=test_env)
147 actual = scheme_eval(read_line(input), env=test_env)
148 if expected != actual:
149 raise SchemeError(f"incorrect output for {input}: {actual}")
151 ProblemRewards[which] = None
152 if problem_solved_hook(Problems[which]):
155 print('You unlocked:')
156 for reward in rewards:
158 receive_reward(reward)
162 def __init__(self, name: str):
167 return repr(self.name)
170 'Icosahedron of Alyssa, Sage of Pairs',
171 'Torus of Louis, Sage of Symbols',
172 'Cube of Ben, Sage of Booleans',
173 'Octahedron of Lem, Sage of Numbers',
174 'Pyramid of Cy, Sage of Promises',
175 'Dodecahedron of Eva, Sage of Procedures',
176 'Sphere of the Lost Sage of Nil',
179 macguffinsCollected = 0
182 def __init__(self, data: dict[str, Any]):
186 return self.data['name']
189 return data['name'].partition(' ')[0]
192 return self.data['prompt']
194 def shadow_all_forms_except(frame: Frame, *exceptions: Symbol):
195 for name in frame.global_frame().bindings:
196 if name not in exceptions and not name.startswith("#"):
197 frame.define(name, Locked(name))
199 def load_problems(problems: dict[str, Problem], yaml_path: str):
200 with open(yaml_path, 'r') as input:
201 for problem_data in yaml.safe_load(input):
202 problems[Problem.label_for(problem_data)] = Problem(problem_data)
204 def generate_problem_rewards_except(problems: Iterable[str], frame_of_locks: Frame, *exceptions: Symbol) -> dict[str, list[Union[Symbol, MacGuffin]]]:
205 rewards: dict[str, list[Union[Symbol, MacGuffin]]] = {}
206 problems = list(problems)
207 for problem in problems:
208 rewards[problem] = []
210 # Distribute MacGuffins across different problems, randomly.
211 positions: Iterator[str] = itertools.cycle(random.sample(problems, len(problems)))
212 for reward in MacGuffins:
213 rewards[next(positions)].append(MacGuffin(reward))
215 for name in frame_of_locks.bindings:
216 if name not in exceptions:
217 rewards[next(positions)].insert(0, name)
222 TheGlobalFrame = create_global_frame()
223 TheLockingFrame = Frame(TheGlobalFrame)
224 ThePlayerFrame = Frame(TheLockingFrame)
226 Problems: dict[str, Problem] = {}
227 ProblemRewards: dict[str, Optional[list[Union[Symbol, MacGuffin]]]] = {}
230 TheGlobalFrame.define('#undef', SpecialForm('#undef', do_undef))
232 game_dir = os.path.dirname(os.path.realpath(__file__))
233 prelude_path = os.path.join(game_dir, "game_prelude.scm")
234 scheme_load(prelude_path, True, TheGlobalFrame)
236 shadow_all_forms_except(TheLockingFrame, 'lambda', 'quote', 'display', 'print', 'newline', 'error')
237 load_problems(Problems, os.path.join(game_dir, 'locations.yml'))
238 global ProblemRewards
239 ProblemRewards = generate_problem_rewards_except(Problems.keys(), TheLockingFrame, 'eval', 'exit', 'load')
242 def run_game(*argv: str):
243 if '--list-all-items' in argv:
244 for name in sorted(TheGlobalFrame.bindings):
249 if '--dump-items' in argv:
250 for reward in itertools.chain.from_iterable(ProblemRewards.values()):
251 if isinstance(reward, MacGuffin):
252 item_id = MacGuffins.index(reward.name) + 1
254 # The last six characters of our operations happen to be unique
255 # and 48 bits is within the allowed ID range for Archipelago.
256 item_id = int.from_bytes(reward.encode('utf-8')[-6:], byteorder='big')
257 print(f'- name: "{reward}"')
258 print(f' id: {item_id:#x}')
260 if '--spoiler-log' in argv:
261 for name in ProblemRewards:
262 print(f'{name}: {ProblemRewards[name]}')
265 read_eval_print_loop(buffer_input, ThePlayerFrame, startup=True,