Spaces:
Running
Running
Commit ·
05345c6
1
Parent(s): e2da85d
Add week03 branching mystery template
Browse files
classroom_data/course.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{"course_id":"2026Fall","title":"2026 Fall 進階專題","demo_class":"2026Fall進階專題","weeks":[
|
| 2 |
{"id":"week01","week":1,"title":"任務管理器開發挑戰","project":"能力診斷","output":"文字版任務管理器","template":true},
|
| 3 |
{"id":"week02","week":2,"title":"消失的校徽","project":"校園謎案調查局","output":"探索地點與收集線索的文字案件","template":true},
|
| 4 |
-
{"id":"week03","week":3,"title":"線索會改變世界","project":"校園謎案調查局","output":"條件解鎖與多
|
| 5 |
{"id":"week04","week":4,"title":"把文字案件變成可點的現場","project":"校園謎案調查局","output":"Pygame 互動現場","template":false},
|
| 6 |
{"id":"week05","week":5,"title":"案件發布日","project":"校園謎案調查局","output":"原創短案件與線上展示","template":false},
|
| 7 |
{"id":"week06","week":6,"title":"領養一顆神祕蛋","project":"電子寵物進化實驗室","output":"可餵食、玩耍、休息的寵物原型","template":false},
|
|
|
|
| 1 |
{"course_id":"2026Fall","title":"2026 Fall 進階專題","demo_class":"2026Fall進階專題","weeks":[
|
| 2 |
{"id":"week01","week":1,"title":"任務管理器開發挑戰","project":"能力診斷","output":"文字版任務管理器","template":true},
|
| 3 |
{"id":"week02","week":2,"title":"消失的校徽","project":"校園謎案調查局","output":"探索地點與收集線索的文字案件","template":true},
|
| 4 |
+
{"id":"week03","week":3,"title":"線索會改變世界","project":"校園謎案調查局","output":"條件解鎖、變化證詞與多結局案件","template":true},
|
| 5 |
{"id":"week04","week":4,"title":"把文字案件變成可點的現場","project":"校園謎案調查局","output":"Pygame 互動現場","template":false},
|
| 6 |
{"id":"week05","week":5,"title":"案件發布日","project":"校園謎案調查局","output":"原創短案件與線上展示","template":false},
|
| 7 |
{"id":"week06","week":6,"title":"領養一顆神祕蛋","project":"電子寵物進化實驗室","output":"可餵食、玩耍、休息的寵物原型","template":false},
|
classroom_data/templates/week03/case_data.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"title": "午夜廣播室",
|
| 3 |
+
"intro": "放學後,校內廣播突然播放一段求救訊息,卻沒有人承認進過廣播室。",
|
| 4 |
+
"actions": {
|
| 5 |
+
"1": {
|
| 6 |
+
"name": "搜索導師桌",
|
| 7 |
+
"before": "抽屜裡有一張被塗改的廣播值日表。",
|
| 8 |
+
"after": "抽屜已經檢查過了。",
|
| 9 |
+
"changes_after": "duty_list",
|
| 10 |
+
"gives": {"id": "duty_list", "name": "被塗改的廣播值日表"}
|
| 11 |
+
},
|
| 12 |
+
"2": {
|
| 13 |
+
"name": "詢問工友",
|
| 14 |
+
"before": "工友搖搖頭:我只看到有人往舊校舍走。",
|
| 15 |
+
"after": "看到值日表後,工友改口:我撿到一張紙條,上面寫著門禁碼 0426。",
|
| 16 |
+
"changes_after": "duty_list",
|
| 17 |
+
"gives_requires": ["duty_list"],
|
| 18 |
+
"gives": {"id": "door_code", "name": "廣播室門禁碼 0426"}
|
| 19 |
+
},
|
| 20 |
+
"3": {
|
| 21 |
+
"name": "進入廣播室",
|
| 22 |
+
"requires": ["door_code"],
|
| 23 |
+
"locked_text": "門鎖需要四位數密碼。也許有人知道密碼的下落。",
|
| 24 |
+
"before": "控制台旁有一台仍在播放的錄音機。",
|
| 25 |
+
"after": "錄音機已經檢查過了。",
|
| 26 |
+
"changes_after": "recording",
|
| 27 |
+
"gives": {"id": "recording", "name": "剪接過的求救錄音"}
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"endings": {
|
| 31 |
+
"1": {
|
| 32 |
+
"label": "指控值日生惡作劇",
|
| 33 |
+
"requires": ["recording"],
|
| 34 |
+
"text": "【誤判結局】錄音雖然是剪接的,但值日表被塗改,不能證明值日生真的進過廣播室。"
|
| 35 |
+
},
|
| 36 |
+
"2": {
|
| 37 |
+
"label": "判斷這是預演錄音誤播",
|
| 38 |
+
"requires": ["duty_list", "door_code", "recording"],
|
| 39 |
+
"text": "【真相結局】求救聲是校慶戲劇預演錄音;工友誤觸了排程播放,怕被責怪才隱瞞紙條。案件破解!"
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
}
|
classroom_data/templates/week03/main.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Week 3:線索會改變世界 Starter。
|
| 2 |
+
|
| 3 |
+
先完成 TODO 1~4。你會讓線索解鎖行動、改變證詞,並產生不同結局。
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
CASE_FILE = Path(__file__).with_name("case_data.json")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def load_case():
|
| 13 |
+
with CASE_FILE.open("r", encoding="utf-8") as file:
|
| 14 |
+
return json.load(file)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def requirements_met(required_ids, clues):
|
| 18 |
+
"""所有 required_ids 都已取得時回傳 True。"""
|
| 19 |
+
# TODO 1:可使用 all(required_id in clues for required_id in required_ids)
|
| 20 |
+
return True
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def choose_text(action, clues):
|
| 24 |
+
"""依目前線索,選擇行動前或行動後的文字。"""
|
| 25 |
+
# TODO 2:若 changes_after 已在 clues,回傳 after;否則回傳 before。
|
| 26 |
+
return action["before"]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def perform_action(action, clues):
|
| 30 |
+
"""檢查前置條件、顯示內容並加入新線索。"""
|
| 31 |
+
required_ids = action.get("requires", [])
|
| 32 |
+
# TODO 3:條件不足時顯示 locked_text 並直接 return。
|
| 33 |
+
|
| 34 |
+
print(f"\n{choose_text(action, clues)}")
|
| 35 |
+
clue = action.get("gives")
|
| 36 |
+
can_give = requirements_met(action.get("gives_requires", []), clues)
|
| 37 |
+
# TODO 4:clue 存在、can_give 且尚未取得時,加入 clue["id"] 並顯示名稱。
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def show_clues(clues, case):
|
| 41 |
+
print("\n【調查狀態】")
|
| 42 |
+
if not clues:
|
| 43 |
+
print("尚未取得任何線索。")
|
| 44 |
+
return
|
| 45 |
+
names = {
|
| 46 |
+
action["gives"]["id"]: action["gives"]["name"]
|
| 47 |
+
for action in case["actions"].values()
|
| 48 |
+
if action.get("gives")
|
| 49 |
+
}
|
| 50 |
+
for number, clue_id in enumerate(clues, start=1):
|
| 51 |
+
print(f"{number}. {names.get(clue_id, clue_id)}")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def choose_ending(case, clues):
|
| 55 |
+
print("\n【結案判斷】")
|
| 56 |
+
for ending_id, ending in case["endings"].items():
|
| 57 |
+
print(f"{ending_id}. {ending['label']}")
|
| 58 |
+
choice = input("你的判斷:").strip()
|
| 59 |
+
ending = case["endings"].get(choice)
|
| 60 |
+
if ending is None:
|
| 61 |
+
print("沒有這個結局選項。")
|
| 62 |
+
return False
|
| 63 |
+
if not requirements_met(ending.get("requires", []), clues):
|
| 64 |
+
print("目前的線索還不足以支持這個判斷。")
|
| 65 |
+
return False
|
| 66 |
+
print(ending["text"])
|
| 67 |
+
return True
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def main():
|
| 71 |
+
case = load_case()
|
| 72 |
+
clues = []
|
| 73 |
+
print(f"=== {case['title']} ===")
|
| 74 |
+
print(case["intro"])
|
| 75 |
+
while True:
|
| 76 |
+
print("\n你要做什麼?")
|
| 77 |
+
for action_id, action in case["actions"].items():
|
| 78 |
+
print(f"{action_id}. {action['name']}")
|
| 79 |
+
print("4. 查看調查狀態")
|
| 80 |
+
print("5. 提出結案判斷")
|
| 81 |
+
print("6. 暫停案件")
|
| 82 |
+
choice = input("請選擇:").strip()
|
| 83 |
+
if choice in case["actions"]:
|
| 84 |
+
perform_action(case["actions"][choice], clues)
|
| 85 |
+
elif choice == "4":
|
| 86 |
+
show_clues(clues, case)
|
| 87 |
+
elif choice == "5":
|
| 88 |
+
if choose_ending(case, clues):
|
| 89 |
+
break
|
| 90 |
+
elif choice == "6":
|
| 91 |
+
print("案件已暫停。")
|
| 92 |
+
break
|
| 93 |
+
else:
|
| 94 |
+
print("請輸入畫面上的選項。")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
main()
|
tests/test_classroom.py
CHANGED
|
@@ -64,6 +64,7 @@ class ClassroomServiceTests(unittest.TestCase):
|
|
| 64 |
course = json.loads((classroom.DATA / "course.json").read_text(encoding="utf-8"))
|
| 65 |
self.assertEqual(len(course["weeks"]), 18)
|
| 66 |
self.assertEqual(set(classroom.template_files("week02")), {"main.py", "case_data.json"})
|
|
|
|
| 67 |
|
| 68 |
def test_catalog_lists_courses_and_existing_students(self):
|
| 69 |
state = classroom.empty("L6A", "student-1", "Alex", "week02")
|
|
|
|
| 64 |
course = json.loads((classroom.DATA / "course.json").read_text(encoding="utf-8"))
|
| 65 |
self.assertEqual(len(course["weeks"]), 18)
|
| 66 |
self.assertEqual(set(classroom.template_files("week02")), {"main.py", "case_data.json"})
|
| 67 |
+
self.assertEqual(set(classroom.template_files("week03")), {"main.py", "case_data.json"})
|
| 68 |
|
| 69 |
def test_catalog_lists_courses_and_existing_students(self):
|
| 70 |
state = classroom.empty("L6A", "student-1", "Alex", "week02")
|