Skip to content

Commit 73753d7

Browse files
author
William Yang
committed
feat: initial course-cs50 with hello, mario-less, cash stages
0 parents  commit 73753d7

File tree

18 files changed

+823
-0
lines changed

18 files changed

+823
-0
lines changed

.github/workflows/sync.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# .github/workflows/sync.yml
2+
#
3+
# 课程同步到 BootCS API
4+
# 触发条件:推送到 main 分支 或 手动触发
5+
6+
name: Sync Course
7+
8+
on:
9+
push:
10+
branches: [main]
11+
workflow_dispatch:
12+
13+
jobs:
14+
sync:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Setup Node.js
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: "20"
24+
25+
- name: Install dependencies
26+
run: npm install js-yaml
27+
28+
- name: Parse and Sync
29+
env:
30+
BOOTCS_API_URL: ${{ secrets.BOOTCS_API_URL }}
31+
SYNC_SECRET: ${{ secrets.SYNC_SECRET }}
32+
run: node scripts/sync-to-api.js

.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Build output
5+
dist/
6+
7+
# IDE
8+
.idea/
9+
.vscode/
10+
*.swp
11+
12+
# OS
13+
.DS_Store
14+
Thumbs.db
15+
16+
# Compiled C files
17+
*.o
18+
*.exe
19+
*.out

CONTRIBUTING.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# course-cs50
2+
3+
CS50 课程包,用于 BootCS 平台。
4+
5+
## 结构
6+
7+
```
8+
course-cs50/
9+
├── course.yml # 课程元信息
10+
├── README.md # 课程详细介绍
11+
├── stages/ # 阶段定义
12+
│ ├── hello.yml + .md
13+
│ ├── mario-less.yml + .md
14+
│ └── cash.yml + .md
15+
├── checks/ # 评测脚本 (bootcs-cli 格式)
16+
│ ├── hello/
17+
│ ├── mario-less/
18+
│ └── cash/
19+
├── starters/ # 起始代码模板
20+
│ ├── hello/
21+
│ ├── mario-less/
22+
│ └── cash/
23+
└── .github/workflows/
24+
└── sync.yml # 同步到 bootcs-api
25+
```
26+
27+
## 本地测试
28+
29+
```bash
30+
# 安装 bootcs-cli
31+
npm install -g bootcs
32+
33+
# 测试 hello 阶段
34+
cd starters/hello
35+
bootcs check ../checks/hello
36+
```
37+
38+
## 添加新阶段
39+
40+
1.`stages/` 创建 `{slug}.yml``{slug}.md`
41+
2.`checks/{slug}/` 创建 `checks.ts`
42+
3.`starters/{slug}/` 创建起始代码
43+
4.`course.yml``stage_order` 中添加 slug
44+
5. 提交并推送到 main 分支
45+
46+
## 同步配置
47+
48+
需要在仓库 Secrets 中配置:
49+
50+
- `BOOTCS_API_URL`: API 地址
51+
- `SYNC_SECRET`: 同步密钥

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# CS50: Introduction to Computer Science
2+
3+
## 简介
4+
5+
CS50 是哈佛大学的计算机科学入门课程,由 David J. Malan 教授主讲。这门课程涵盖了计算机科学的核心概念,适合零基础学员。
6+
7+
## 学习内容
8+
9+
- **Week 1**: C 语言基础
10+
- **Week 2**: 数组与字符串
11+
- **Week 3**: 算法
12+
- **Week 4**: 内存管理
13+
- **Week 5**: 数据结构
14+
15+
## 先修要求
16+
17+
无需编程经验。
18+
19+
## 参考资料
20+
21+
- [CS50 官方网站](https://cs50.harvard.edu/)
22+
- [CS50 Manual](https://manual.cs50.io/)

checks/cash/checks.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Cash 评测检查
3+
*
4+
* 迁移自 CS50 problems/cash/__init__.py
5+
*/
6+
import { Checks, Check, exists, run } from "bootcs";
7+
import { compile } from "bootcs/c";
8+
9+
// 生成硬币数量的正则表达式(确保不与其他数字混淆)
10+
function coins(num: number): string {
11+
return `(?<!\\d)${num}(?!\\d)`;
12+
}
13+
14+
export default class CashChecks extends Checks {
15+
@Check({ description: "cash.c exists" })
16+
async exists() {
17+
exists("cash.c");
18+
}
19+
20+
@Check({ description: "cash.c compiles", dependency: "exists" })
21+
async compiles() {
22+
await compile("cash.c", { lcs50: true });
23+
}
24+
25+
@Check({ description: "input of 41 yields output of 4", dependency: "compiles" })
26+
async test041() {
27+
await run("./cash").stdin("41").stdout(coins(4), "4\n").exit(0);
28+
}
29+
30+
@Check({ description: "input of 1 yields output of 1", dependency: "compiles" })
31+
async test001() {
32+
await run("./cash").stdin("1").stdout(coins(1), "1\n").exit(0);
33+
}
34+
35+
@Check({ description: "input of 15 yields output of 2", dependency: "compiles" })
36+
async test015() {
37+
await run("./cash").stdin("15").stdout(coins(2), "2\n").exit(0);
38+
}
39+
40+
@Check({ description: "input of 160 yields output of 7", dependency: "compiles" })
41+
async test160() {
42+
await run("./cash").stdin("160").stdout(coins(7), "7\n").exit(0);
43+
}
44+
45+
@Check({ description: "input of 2300 yields output of 92", dependency: "compiles" })
46+
async test230() {
47+
await run("./cash").stdin("2300").stdout(coins(92), "92\n").exit(0);
48+
}
49+
50+
@Check({ description: "rejects a negative input like -1", dependency: "compiles" })
51+
async test_reject_negative() {
52+
await run("./cash").stdin("-1").reject();
53+
}
54+
55+
@Check({ description: 'rejects a non-numeric input of "foo"', dependency: "compiles" })
56+
async test_reject_foo() {
57+
await run("./cash").stdin("foo").reject();
58+
}
59+
60+
@Check({ description: 'rejects a non-numeric input of ""', dependency: "compiles" })
61+
async test_reject_empty() {
62+
await run("./cash").stdin("").reject();
63+
}
64+
}

checks/hello/checks.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Hello 评测检查
3+
*
4+
* 迁移自 CS50 problems/hello/__init__.py
5+
*/
6+
import { Checks, Check, exists, run } from "bootcs";
7+
import { compile } from "bootcs/c";
8+
9+
export default class HelloChecks extends Checks {
10+
@Check({ description: "hello.c exists" })
11+
async exists() {
12+
exists("hello.c");
13+
}
14+
15+
@Check({ description: "hello.c compiles", dependency: "exists" })
16+
async compiles() {
17+
await compile("hello.c", { lcs50: true });
18+
}
19+
20+
@Check({ description: "responds to name Emma", dependency: "compiles" })
21+
async emma() {
22+
await run("./hello").stdin("Emma").stdout("Emma").exit();
23+
}
24+
25+
@Check({ description: "responds to name Rodrigo", dependency: "compiles" })
26+
async rodrigo() {
27+
await run("./hello").stdin("Rodrigo").stdout("Rodrigo").exit();
28+
}
29+
}

checks/mario-less/checks.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Mario (Less) 评测检查
3+
*
4+
* 迁移自 CS50 problems/mario/less/__init__.py
5+
*/
6+
import { Checks, Check, exists, include, run, Failure } from "bootcs";
7+
import { compile } from "bootcs/c";
8+
import * as fs from "fs";
9+
10+
// 期望输出
11+
const PYRAMIDS: Record<number, string> = {
12+
1: "#\n",
13+
2: " #\n##\n",
14+
8: " #\n ##\n ###\n ####\n #####\n ######\n #######\n########\n",
15+
};
16+
17+
function checkPyramid(output: string, expected: string): void {
18+
if (output === expected) {
19+
return;
20+
}
21+
22+
const outputLines = output.split("\n").filter((line) => line !== "");
23+
const expectedLines = expected.split("\n").filter((line) => line !== "");
24+
25+
let help: string | undefined;
26+
if (outputLines.length === expectedLines.length) {
27+
if (
28+
outputLines.every(
29+
(ol, i) => ol.trimEnd() === expectedLines[i]
30+
)
31+
) {
32+
help = "did you add too much trailing whitespace to the end of your pyramid?";
33+
} else if (
34+
outputLines.every((ol, i) => ol.slice(1) === expectedLines[i])
35+
) {
36+
help = "are you printing an additional character at the beginning of each line?";
37+
}
38+
}
39+
40+
throw new Failure(`Expected pyramid does not match output`, { help });
41+
}
42+
43+
export default class MarioLessChecks extends Checks {
44+
@Check({ description: "mario.c exists" })
45+
async exists() {
46+
exists("mario.c");
47+
}
48+
49+
@Check({ description: "mario.c compiles", dependency: "exists" })
50+
async compiles() {
51+
await compile("mario.c", { lcs50: true });
52+
}
53+
54+
@Check({ description: "rejects a height of -1", dependency: "compiles" })
55+
async test_reject_negative() {
56+
await run("./mario").stdin("-1").reject();
57+
}
58+
59+
@Check({ description: "rejects a height of 0", dependency: "compiles" })
60+
async test0() {
61+
await run("./mario").stdin("0").reject();
62+
}
63+
64+
@Check({ description: "handles a height of 1 correctly", dependency: "compiles" })
65+
async test1() {
66+
const result = await run("./mario").stdin("1").stdout();
67+
checkPyramid(result, PYRAMIDS[1]);
68+
}
69+
70+
@Check({ description: "handles a height of 2 correctly", dependency: "compiles" })
71+
async test2() {
72+
const result = await run("./mario").stdin("2").stdout();
73+
checkPyramid(result, PYRAMIDS[2]);
74+
}
75+
76+
@Check({ description: "handles a height of 8 correctly", dependency: "compiles" })
77+
async test8() {
78+
const result = await run("./mario").stdin("8").stdout();
79+
checkPyramid(result, PYRAMIDS[8]);
80+
}
81+
82+
@Check({
83+
description: 'rejects a height of -1, then accepts height of 2',
84+
dependency: "compiles",
85+
})
86+
async test9() {
87+
const result = await run("./mario").stdin("-1").reject().stdin("2").stdout();
88+
checkPyramid(result, PYRAMIDS[2]);
89+
}
90+
91+
@Check({ description: 'rejects a non-numeric height of "foo"', dependency: "compiles" })
92+
async test_reject_foo() {
93+
await run("./mario").stdin("foo").reject();
94+
}
95+
96+
@Check({ description: 'rejects a non-numeric height of ""', dependency: "compiles" })
97+
async test_reject_empty() {
98+
await run("./mario").stdin("").reject();
99+
}
100+
}

course.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# course.yml
2+
version: "1.0"
3+
4+
# 课程基本信息
5+
slug: cs50
6+
name: "CS50: Introduction to Computer Science"
7+
description: README.md
8+
summary: |
9+
哈佛大学计算机科学入门课程,涵盖 C、Python、SQL 和 Web 开发。
10+
11+
icon: https://cdn.bootcs.cn/icons/cs50.svg
12+
difficulty: medium
13+
status: beta
14+
15+
# 评测配置
16+
evaluation:
17+
image: ghcr.io/bootcs-cn/bootcs-cli:cs50
18+
timeout: 120
19+
20+
# 标签
21+
tags:
22+
- c
23+
- python
24+
- algorithms
25+
- data-structures
26+
27+
# 先修课程
28+
prerequisites: []
29+
30+
# 阶段顺序(决定 position)
31+
stage_order:
32+
- hello
33+
- mario-less
34+
- cash

0 commit comments

Comments
 (0)