import json
import os
import sys
sys.path.insert(
0, os.path.join(os.path.dirname(__file__), "..", "app", "src", "main", "python")
)
import runner
APP_ASSETS = os.path.join(
os.path.dirname(__file__), "..", "app", "src", "main", "assets"
)
LESSON_FILES = ["lessons_basic.json", "lessons_mid.json", "lessons_adv.json", "lessons_pro.json"]
SOLUTIONS = {
"l01": "hacker_name = 'V'\nonline = True\nprint(hacker_name, online)",
"l02": "energy = 9000\nshield = 62.5\n\ntotal = energy + shield\nprint(total)",
"l03": "base = 'CYBER'\n\nfull_id = base + '-' + base[2:]\nprint(full_id)",
"l04": (
"def damage(attack, defense):\n"
" return int(attack ** 0.5) - defense // 2\n"
"\n"
"print(damage(25, 6))\n"
"print(damage(100, 10))"
),
"l05": (
"def echo_band(band):\n"
" return 'BAND:' + band.upper()\n"
"\n"
"print(echo_band('neon'))"
),
"l06": (
"def access(level):\n"
" if level >= 100:\n"
" return 'ROOT'\n"
" elif level >= 50:\n"
" return 'OPERATOR'\n"
" else:\n"
" return 'GUEST'\n"
"\n"
"print(access(120))\n"
"print(access(66))\n"
"print(access(3))"
),
"l07": (
"def harmonic(limit):\n"
" total = 0\n"
" for n in range(2, limit + 1, 2):\n"
" total += n\n"
" return total\n"
"\n"
"print(harmonic(10))"
),
"l08": (
"def peak(values):\n"
" if not values:\n"
" return None\n"
" best = values[0]\n"
" for v in values:\n"
" if v > best:\n"
" best = v\n"
" return best\n"
"\n"
"print(peak([88, 92, 79]))\n"
"print(peak([]))"
),
"l09": (
"def freq(text):\n"
" counts = {}\n"
" for ch in text:\n"
" counts[ch] = counts.get(ch, 0) + 1\n"
" return counts\n"
"\n"
"print(freq('aab'))"
),
"l10": (
"def street_credit(stats):\n"
" total = sum(stats.values())\n"
" if total >= 100:\n"
" return 'LEGEND'\n"
" elif total >= 60:\n"
" return 'PRO'\n"
" else:\n"
" return 'ROOKIE'\n"
"\n"
"s = {'atk': 40, 'def': 35, 'hack': 30}\n"
"print(street_credit(s))"
),
"l11": (
"def slugify(title):\n"
" return title.strip().lower().replace(' ', '-')\n"
"\n"
"print(slugify('Hello Neon City'))\n"
"print(slugify(' PY '))"
),
"l12": (
"def dedupe(seq):\n"
" result = []\n"
" seen = set()\n"
" for item in seq:\n"
" if item not in seen:\n"
" seen.add(item)\n"
" result.append(item)\n"
" return result\n"
"\n"
"print(dedupe([3, 1, 3, 2, 1]))"
),
"l13": (
"def flex_total(*nums, start=0):\n"
" return sum(nums) + start\n"
"\n"
"print(flex_total(1, 2, 3))\n"
"print(flex_total(1, 2, start=10))"
),
"l14": (
"def even_squares(n):\n"
" return [i * i for i in range(1, n + 1) if i % 2 == 0]\n"
"\n"
"print(even_squares(5))\n"
"print(even_squares(1))"
),
"l15": (
"def safe_div(a, b):\n"
" try:\n"
" return a / b\n"
" except ZeroDivisionError:\n"
" return None\n"
"\n"
"def parse_age(s):\n"
" try:\n"
" return int(s)\n"
" except ValueError:\n"
" return -1\n"
"\n"
"print(safe_div(6, 3), safe_div(1, 0))\n"
"print(parse_age('20'), parse_age('abc'))"
),
"l16": (
"import json\n"
"\n"
"def restore_agent(json_str):\n"
" data = json.loads(json_str)\n"
" data.setdefault('level', 1)\n"
" return data\n"
"\n"
'print(restore_agent(\'{"name": "V"}\'))\n'
'print(restore_agent(\'{"name": "J", "level": 5}\'))'
),
"l17": (
"import random\n"
"\n"
"def roll(sides, times):\n"
" random.seed(7)\n"
" return [random.randint(1, sides) for _ in range(times)]\n"
"\n"
"print(roll(6, 4))"
),
"l18": (
"class NeonBot:\n"
" def __init__(self, name, hp=100):\n"
" self.name = name\n"
" self.hp = hp\n"
"\n"
" def hit(self, dmg):\n"
" self.hp = max(0, self.hp - dmg)\n"
"\n"
" def is_alive(self):\n"
" return self.hp > 0\n"
"\n"
"b = NeonBot('K', 50)\n"
"b.hit(30)\n"
"print(b.hp, b.is_alive())\n"
"b.hit(999)\n"
"print(b.is_alive())"
),
"l19": (
"class NeonBot:\n"
" def __init__(self, name, hp=100):\n"
" self.name = name\n"
" self.hp = hp\n"
"\n"
" def hit(self, dmg):\n"
" self.hp = max(0, self.hp - dmg)\n"
"\n"
"class Ninja(NeonBot):\n"
" def __init__(self, name, hp=100, weapon='刀'):\n"
" super().__init__(name, hp)\n"
" self.weapon = weapon\n"
"\n"
" def hit(self, dmg):\n"
" super().hit(dmg // 2)\n"
"\n"
" def __str__(self):\n"
" return f'Ninja:{self.name}'\n"
"\n"
"k = Ninja('K', weapon='振动刀')\n"
"k.hit(30)\n"
"print(k.hp, k.weapon)\n"
"print(str(k))"
),
"l20": (
"class Wallet:\n"
" def __init__(self, balance=0):\n"
" self.balance = balance\n"
"\n"
" def deposit(self, amount):\n"
" self.balance += amount\n"
" return self.balance\n"
"\n"
" def withdraw(self, amount):\n"
" if amount > self.balance:\n"
" raise ValueError('insufficient')\n"
" self.balance -= amount\n"
" return self.balance\n"
"\n"
"w = Wallet(100)\n"
"print(w.withdraw(30))\n"
"try:\n"
" w.withdraw(999)\n"
"except ValueError as e:\n"
" print('拦截:', e)"
),
"l21": (
"def fibonacci(k):\n"
" a, b = 0, 1\n"
" for _ in range(k):\n"
" yield a\n"
" a, b = b, a + b\n"
"\n"
"print(list(fibonacci(6)))"
),
"l22": (
"def make_multiplier(factor):\n"
" def apply(x):\n"
" return x * factor\n"
" return apply\n"
"\n"
"def double_result(fn):\n"
" def inner(a, b):\n"
" return fn(a, b) * 2\n"
" return inner\n"
"\n"
"@double_result\n"
"def add(a, b):\n"
" return a + b\n"
"\n"
"twice = make_multiplier(2)\n"
"print(twice(5), add(1, 2))"
),
"l23": (
"def process(nums, mode):\n"
" ops = {\n"
" 'sq': lambda x: x * x,\n"
" 'dbl': lambda x: x * 2\n"
" }\n"
" return list(map(ops[mode], nums))\n"
"\n"
"print(process([1, 2, 3], 'sq'))\n"
"print(process([1, 2, 3], 'dbl'))"
),
"l24": (
"from collections import Counter\n"
"\n"
"def top_word(sentence):\n"
" words = sentence.lower().split()\n"
" return Counter(words).most_common(1)[0][0]\n"
"\n"
"print(top_word('Neon city NEON rain'))"
),
"l25": (
"from datetime import date\n"
"import random\n"
"\n"
"def weekday_of(y, m, d):\n"
" return date(y, m, d).strftime('%a')\n"
"\n"
"def shuffle_copy(lst, seed):\n"
" copy = list(lst)\n"
" random.Random(seed).shuffle(copy)\n"
" return copy\n"
"\n"
"print(weekday_of(2026, 8, 26))\n"
"print(shuffle_copy([1, 2, 3, 4, 5], 99))"
),
"l26": (
"def analyze(lines):\n"
" result = {}\n"
" for line in lines:\n"
" tag = line[1:line.index(']')]\n"
" result[tag] = result.get(tag, 0) + 1\n"
" result['total'] = len(lines)\n"
" return result\n"
"\n"
"sample = ['[ERROR] a', '[INFO] b', '[ERROR] c']\n"
"print(analyze(sample))"
),
"l27": (
"class Gate:\n"
" def __init__(self):\n"
" self.inside = False\n"
" def __enter__(self):\n"
" self.inside = True\n"
" return self\n"
" def __exit__(self, exc_type, exc, tb):\n"
" self.inside = False\n"
"\n"
"gate = Gate()\n"
"with gate as g:\n"
" print('进入:', g.inside)"
),
"bonus-27": (
"def safe_len(x):\n"
" return len(x) if hasattr(x, '__len__') else None\n"
"\n"
"print(safe_len('neon'))\n"
"print(safe_len(42))"
),
"l28": (
"def write_note(path, text):\n"
" with open(path, 'w', encoding='utf-8') as f:\n"
" f.write(text)\n"
" return len(text)\n"
"\n"
"print(write_note('note.txt', 'hello'))\n"
"print(open('note.txt', encoding='utf-8').read())"
),
"l29": (
"class HackError(Exception):\n"
" def __init__(self, msg='非法入侵'):\n"
" super().__init__(msg)\n"
"\n"
"def guard(n):\n"
" if n < 0:\n"
" raise HackError()\n"
" return n * 2\n"
"\n"
"print(guard(5))\n"
"try:\n"
" guard(-1)\n"
"except HackError as e:\n"
" print('已拦截:', e)"
),
"l30": (
"import math\n"
"\n"
"def area_circle(r):\n"
" return round(math.pi * r * r, 2)\n"
"\n"
"print(area_circle(1))\n"
"print(area_circle(2))"
),
"l31": (
"import re\n"
"\n"
"def is_phone(s):\n"
" return bool(re.fullmatch(r'1[3-9]\\d{9}', s))\n"
"\n"
"print(is_phone('13800138000'))\n"
"print(is_phone('12345'))"
),
"l32": (
"from dataclasses import dataclass\n"
"\n"
"@dataclass\n"
"class Student:\n"
" name: str\n"
" score: int\n"
"\n"
" def level(self) -> str:\n"
" if self.score >= 90:\n"
" return '优秀'\n"
" elif self.score >= 60:\n"
" return '及格'\n"
" else:\n"
" return '加油'\n"
"\n"
"s = Student('小明', 92)\n"
"print(s, s.level())"
),
"l33": (
"from functools import partial\n"
"from operator import mul\n"
"\n"
"double = partial(mul, 2)\n"
"\n"
"print(double(10))\n"
"print(double(-3))"
),
"l34": (
"import threading\n"
"\n"
"results = []\n"
"\n"
"def collect(n):\n"
" results.append(n)\n"
"\n"
"threads = [threading.Thread(target=collect, args=(i,)) for i in range(2)]\n"
"for t in threads:\n"
" t.start()\n"
"for t in threads:\n"
" t.join()\n"
"print(sorted(results))"
),
"l35": (
"import asyncio\n"
"\n"
"done = []\n"
"\n"
"async def greet(name: str, delay: float):\n"
" await asyncio.sleep(delay)\n"
" done.append(name)\n"
" print(f'你好,{name}')\n"
"\n"
"async def main():\n"
" await asyncio.gather(greet('A', 0.05), greet('B', 0.05))\n"
"\n"
"asyncio.run(main())"
),
"l36": (
"def parse_config(args):\n"
" cfg = {}\n"
" i = 0\n"
" while i < len(args):\n"
" key = args[i]\n"
" if key == '--name' and i + 1 < len(args):\n"
" cfg['name'] = args[i + 1]\n"
" i += 2\n"
" elif key == '--times' and i + 1 < len(args):\n"
" cfg['times'] = int(args[i + 1])\n"
" i += 2\n"
" else:\n"
" i += 1\n"
" return cfg\n"
"\n"
"print(parse_config(['--name', 'Python', '--times', '3']))"
),
"l37": (
"import unittest\n"
"\n"
"def max3(a, b, c):\n"
" return max(a, b, c)\n"
"\n"
"class TestMax3(unittest.TestCase):\n"
" def test_max_basic(self):\n"
" self.assertEqual(max3(1, 9, 5), 9)\n"
"\n"
" def test_max_all_equal(self):\n"
" self.assertEqual(max3(4, 4, 4), 4)\n"
"\n"
"suite = unittest.TestLoader().loadTestsFromTestCase(TestMax3)\n"
"result = unittest.TextTestRunner(verbosity=0).run(suite)\n"
"print('通过', result.testsRun, '个,失败', len(result.failures) + len(result.errors))"
),
"l38": (
"def parse_requirements(text):\n"
" result = []\n"
" for ln in text.strip().splitlines():\n"
" ln = ln.strip()\n"
" if not ln or ln.startswith('#'):\n"
" continue\n"
" for sep in ('==', '>='):\n"
" if sep in ln:\n"
" name, ver = ln.split(sep, 1)\n"
" result.append((name.strip(), sep + ver.strip()))\n"
" break\n"
" else:\n"
" result.append((ln, ''))\n"
" return result\n"
"\n"
"print(parse_requirements('requests==2.32.3\\nflask>=3.0\\npytest'))"
),
"c01": (
"def neon_fb(n):\n"
" result = []\n"
" for i in range(1, n + 1):\n"
" if i % 15 == 0:\n"
" result.append('NEONCITY')\n"
" elif i % 3 == 0:\n"
" result.append('NEON')\n"
" elif i % 5 == 0:\n"
" result.append('CITY')\n"
" else:\n"
" result.append(str(i))\n"
" return result\n"
"\n"
"print(neon_fb(5))"
),
"c02": (
"def is_palindrome(s):\n"
" clean = ''.join(ch.lower() for ch in s if ch.isalnum())\n"
" return clean == clean[::-1]\n"
"\n"
"print(is_palindrome('No lemon, no melon'))\n"
"print(is_palindrome('cyber'))"
),
"c03": (
"def strength(pw):\n"
" score = 0\n"
" if len(pw) >= 8:\n"
" score += 1\n"
" if any(ch.isupper() for ch in pw):\n"
" score += 1\n"
" if any(ch.islower() for ch in pw):\n"
" score += 1\n"
" if any(ch.isdigit() for ch in pw):\n"
" score += 1\n"
" if any((not ch.isalnum()) and (not ch.isspace()) for ch in pw):\n"
" score += 1\n"
" if score >= 4:\n"
" return 'STRONG'\n"
" elif score == 3:\n"
" return 'MEDIUM'\n"
" else:\n"
" return 'WEAK'\n"
"\n"
"print(strength('Abc123!@'))\n"
"print(strength('abc'))"
),
"c04": (
"def is_balanced(s):\n"
" pairs = {')': '(', ']': '[', '}': '{'}\n"
" stack = []\n"
" for ch in s:\n"
" if ch in '([{':\n"
" stack.append(ch)\n"
" elif ch in pairs:\n"
" if not stack or stack.pop() != pairs[ch]:\n"
" return False\n"
" return not stack\n"
"\n"
"print(is_balanced('{[()]}'))\n"
"print(is_balanced('([)]'))"
),
"c05": (
"def encode_rle(s):\n"
" if not s:\n"
" return ''\n"
" result = []\n"
" current = s[0]\n"
" count = 1\n"
" for ch in s[1:]:\n"
" if ch == current:\n"
" count += 1\n"
" else:\n"
" result.append(current + str(count))\n"
" current = ch\n"
" count = 1\n"
" result.append(current + str(count))\n"
" return ''.join(result)\n"
"\n"
"print(encode_rle('AAABBC'))"
),
"c06": (
"class Inventory:\n"
" def __init__(self):\n"
" self.goods = {}\n"
"\n"
" def add(self, item, n):\n"
" if not isinstance(n, int) or n <= 0 or isinstance(n, bool):\n"
" raise ValueError('数量必须为正整数')\n"
" self.goods[item] = self.goods.get(item, 0) + n\n"
"\n"
" def remove(self, item, n):\n"
" if self.stock(item) < n:\n"
" raise ValueError('库存不足')\n"
" self.goods[item] -= n\n"
"\n"
" def stock(self, item):\n"
" return self.goods.get(item, 0)\n"
"\n"
" def total(self):\n"
" return sum(self.goods.values())\n"
"\n"
"inv = Inventory()\n"
"inv.add('chip', 5)\n"
"inv.add('cell', 3)\n"
"inv.remove('chip', 2)\n"
"print(inv.stock('chip'), inv.total())\n"
"try:\n"
" inv.remove('chip', 99)\n"
"except ValueError as e:\n"
" print('拦截:', e)"
),
}
def structural_checks(lessons):
problems = []
for lesson in lessons:
lid = lesson["id"]
has_runnable = any(
b.get("type") == "code" and b.get("runnable") for b in lesson["blocks"]
)
if not has_runnable:
problems.append(f"{lid}: 缺少可运行代码块")
ex = lesson.get("exercise")
if not ex:
problems.append(f"{lid}: 缺少练习")
elif lid not in SOLUTIONS and not ex.get("solution"):
problems.append(f"{lid}: 缺少参考答案")
for b in lesson["blocks"]:
if b.get("type") == "quiz":
if not (0 <= b["answer"] < len(b["options"])):
problems.append(f"{lid}: quiz answer 越界")
if b.get("type") == "table":
width = len(b["headers"])
for row in b["rows"]:
if len(row) != width:
problems.append(f"{lid}: 表格列数不一致")
return problems
SOLUTIONS.update(
{
"dsa01": "def sum_to_n(n):\n return n * (n + 1) // 2\n\nprint(sum_to_n(5))\nprint(sum_to_n(100))",
"dsa02": (
"def two_sum_sorted(nums, target):\n"
" i, j = 0, len(nums) - 1\n"
" while i < j:\n"
" s = nums[i] + nums[j]\n"
" if s == target:\n"
" return (i, j)\n"
" elif s < target:\n"
" i += 1\n"
" else:\n"
" j -= 1\n"
" return (-1, -1)\n"
"\n"
"print(two_sum_sorted([2, 7, 11, 15], 9))"
),
"dsa03": (
"def first_unique(s):\n"
" counts = {}\n"
" for ch in s:\n"
" counts[ch] = counts.get(ch, 0) + 1\n"
" for i, ch in enumerate(s):\n"
" if counts[ch] == 1:\n"
" return i\n"
" return -1\n"
"\n"
"print(first_unique('pyneon'))\n"
"print(first_unique('aabb'))"
),
"dsa04": (
"def bracket_depth(s):\n"
" depth = 0\n"
" best = 0\n"
" for ch in s:\n"
" if ch == '(':\n"
" depth += 1\n"
" best = max(best, depth)\n"
" else:\n"
" depth -= 1\n"
" if depth < 0:\n"
" return -1\n"
" return -1 if depth != 0 else best\n"
"\n"
"print(bracket_depth('(()(()))'))\n"
"print(bracket_depth('(()'))"
),
"dsa05": (
"def fib_memo(n, memo=None):\n"
" if memo is None:\n"
" memo = {}\n"
" if n in memo:\n"
" return memo[n]\n"
" if n < 2:\n"
" return n\n"
" memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)\n"
" return memo[n]\n"
"\n"
"print(fib_memo(10))\n"
"print(fib_memo(30))"
),
"dsa06": (
"def choose_sort(nums):\n"
" data = list(nums)\n"
" for i in range(len(data)):\n"
" mi = i\n"
" for j in range(i + 1, len(data)):\n"
" if data[j] < data[mi]:\n"
" mi = j\n"
" data[i], data[mi] = data[mi], data[i]\n"
" return data\n"
"\n"
"print(choose_sort([3, 1, 2]))"
),
"dsa07": (
"def binary_search(nums, target):\n"
" lo, hi = 0, len(nums) - 1\n"
" while lo <= hi:\n"
" mid = (lo + hi) // 2\n"
" if nums[mid] == target:\n"
" return mid\n"
" elif nums[mid] < target:\n"
" lo = mid + 1\n"
" else:\n"
" hi = mid - 1\n"
" return -1\n"
"\n"
"print(binary_search([1, 2, 3, 4, 5], 3))\n"
"print(binary_search([1, 2, 3, 4, 5], 9))"
),
"dsa08": (
"def min_coins(amount):\n"
" count = 0\n"
" for c in [25, 10, 5, 1]:\n"
" count += amount // c\n"
" amount %= c\n"
" return count\n"
"\n"
"print(min_coins(41))\n"
"print(min_coins(30))"
),
"dsa09": (
"class Node:\n"
" def __init__(self, value):\n"
" self.value = value\n"
" self.next = None\n"
"\n"
"def ll_length(head):\n"
" cnt = 0\n"
" cur = head\n"
" while cur is not None:\n"
" cnt += 1\n"
" cur = cur.next\n"
" return cnt\n"
"\n"
"n1 = Node(1); n2 = Node(2); n3 = Node(3)\n"
"n1.next = n2; n2.next = n3\n"
"print(ll_length(n1))\n"
"print(ll_length(None))"
),
"dsa10": (
"def max_subarray(nums):\n"
" best = float('-inf')\n"
" cur = 0\n"
" for x in nums:\n"
" cur = max(x, cur + x)\n"
" best = max(best, cur)\n"
" return best\n"
"\n"
"print(max_subarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]))\n"
"print(max_subarray([-1, -2]))"
),
}
)
def main():
lessons = []
for name in LESSON_FILES:
with open(os.path.join(APP_ASSETS, name), encoding="utf-8") as f:
lessons.extend(json.load(f))
pack_path = os.path.join(
os.path.dirname(__file__), "..", "content_packs", "bonus-builtin.json"
)
with open(pack_path, encoding="utf-8") as f:
lessons.extend(json.load(f))
for pack in ("dsa-basics.json", "dsa-mastery.json", "js-basics.json", "lua-basics.json", "sql-basics.json"):
pack_path = os.path.join(os.path.dirname(__file__), "..", "content_packs", pack)
with open(pack_path, encoding="utf-8") as f:
lessons.extend(json.load(f))
with open(os.path.join(APP_ASSETS, "challenges.json"), encoding="utf-8") as f:
challenges = json.load(f)
problems = structural_checks(lessons)
if not os.path.exists(
os.path.join(os.path.dirname(__file__), "..", "catalog.json")
):
problems.append("catalog.json 缺失(内容中心远端目录)")
js_lessons = [l for l in lessons if l.get("lang", "python") == "javascript"]
lua_lessons = [l for l in lessons if l.get("lang", "python") == "lua"]
sql_lessons = [l for l in lessons if l.get("lang", "python") == "sql"]
py_lessons = [
l for l in lessons if l.get("lang", "python") not in ("javascript", "lua", "sql")
]
items = []
for lesson in sorted(py_lessons, key=lambda x: x["order"]):
ex = lesson.get("exercise")
if ex:
items.append(
(
lesson["id"],
ex["tests"],
ex.get("stdin") or [],
SOLUTIONS[lesson["id"]],
)
)
for ch in challenges:
if ch.get("lang", "python") == "javascript":
continue
items.append((ch["id"], ch["tests"], [], SOLUTIONS[ch["id"]]))
py_challenges = [c for c in challenges if c.get("lang", "python") != "javascript"]
failures = []
checked = 0
total_cases = 0
for key, tests, stdin_lines, solution in items:
result = runner.check_exercise(solution, tests, stdin_lines)
checked += 1
total_cases += len(tests)
status = "PASS" if result["passed"] else "FAIL"
print(f"[{status}] {key} ({len(tests)} cases)")
if not result["passed"]:
failures.append((key, result["error"], result["stdout"]))
elif not result["ok"]:
failures.append(
(key, {"type": "crash", "message": str(result["error"])}, "")
)
print(
f"\n课程数={len(lessons)}(Python {len(py_lessons)} + JavaScript {len(js_lessons)}"
f" + Lua {len(lua_lessons)} + SQL {len(sql_lessons)};"
f"JS / Lua / SQL 判题分别由 test_js_lessons / LuaCoursePacksTest / test_sql_lessons 执行)"
f" 挑战数={len(challenges)}(JS {len(challenges) - len(py_challenges)} 道)"
f" 判题项={checked} 用例总数={total_cases}"
)
print(f"结构问题={len(problems)} 判题失败={len(failures)}")
for p in problems:
print("STRUCT:", p)
for key, err, out in failures:
print("---", key, err, "\nstdout:", out)
sys.exit(1 if (failures or problems) else 0)
if __name__ == "__main__":
main()