期中项目选题及要求

计算器

在现代的编程语言中,表达式求值是必须要支持的一部分,比如 Python 的命令行模式,可以作为一个增强版的计算器来使用。

![image](Untitled 2.assets/python-command-line.png.34cf22df)

本项目要求实现一个类似的表达式求值工具,接受变量赋值(比如 a = 233)和表达式求值(比如 a + 114514)两种语句,检查输入的错误,并计算表达式的值。

项目指南

2022.12.08 upd:项目指南更新,修正了一些错误与遗漏之处,欢迎大家找错误。

新版链接

2022.12.26 upd:李清扬助教对手册的讲解:点此链接

旧版请查看 NJU Box 链接 (可下载)。

评分标准

本题为客观题,由 dotOJ 黑箱测试自动评分,根据通过的测试点客观给分,OJ 得分即为你期中项目的得分。与主观题不同,你需要提交项目文档。

你可以及时看到得分,但无法得知错误的测试数据点(助教不会给,保证公平性),与平时编程练习的形式完全相同,可多次提交,取最高分,以 dotOJ 显示为准。

蔡之恒:感觉师姐去年造数据的时候有点偷懒,今年想办法加强一波(

本题将同其他作业一样参与查重,并按照 抄袭与惩罚 执行。

选做该题目,将有较大机会得到期中项目的满分。其中,正确实现所有必做部分,可得到 70% 的分数;每正确实现一个选做部分,将额外得到 10%,也就是说,实现题面中所有部分,将得到满分。

除去该题之外,其余题目均为主观题,都将在期中项目截止后人工验收,因此选做本项目的同学的得分分布将会作为其他项目评分的参考标准,我们会尽力保证期中项目所有选择的在评分时的公平性。

输入格式

输入包含若干行,每行为一个表达式或赋值语句,关于输入输出的详细规约请参考项目指南

输出格式

对于每一个输入的表达式或赋值语句,输出 Error 或相应的值。

测试样例

Input

1
1 + 2
Output

1
3

小恐龙

题目描述

出题人:李薛成

验题人:李薛成

项目概述

机房电脑上除了扫雷蜘蛛纸牌,还是要数Chrome的小恐龙最好玩了,一玩就是一节信息课()

没玩过的同学们也可以现在就打开 Chrome,在地址栏输入 chrome://dino (或者断网)就可以畅玩了!(Edge的小恐龙被换成冲浪了没法玩)

Sakiyary 在高中每节信息课都和同学比拼小恐龙赛跑,但自从上了大学,时移事易,物是人非(还记得2016年夏的守望先锋吗.jpg),Sakiyary 再也找不回当年玩小恐龙的感觉(再也没有超过高中时的最高纪录)。

现在请你来帮帮他,写一个 C 语言的小恐龙并和 Sakiyary 一起比拼,让他找回当年的感觉!


项目要求

你可以使用命令行的字符界面或图形界面来制作并运行游戏。

建议用键盘来操控小恐龙而不是鼠标。(Chrome 的小恐龙使用了空格键与上下两个方向键来操控)

你可以用命令行字符或图形界面的贴图绘出小恐龙,题目注解中提供了一种可能的字符小恐龙画法,请根据小恐龙的大小决定整个场景的大小,障碍物同理。(小恐龙之外的形象也可以,但不能是竖线、正方形、圆形等简单几何图形。下文均以“小恐龙”来指代玩家所操控的角色)

你需要至少实现两种障碍物,即需要跳跃的障碍物(如仙人掌)和需要下蹲的障碍物(如飞鸟)。

你需要让你的代码能够在别人的电脑上(按照你自己给出的环境要求与规定编译并正确运行

Sakiyary 将人工审查你的代码(与其他同学、各大开源平台上的开源代码进行比对与查重)并按照 抄袭与惩罚 执行。


你需要正确地实现游戏进程:

  1. 如何实现小恐龙向前跑动与障碍物迎面而来的效果与动画?
  2. 如何实现小恐龙跳起落下、下蹲的效果与动画?
  3. 如何实现按键操控且保证键盘输入不冲突不积压
  4. 如何判断小恐龙与障碍物的碰撞与 Game Over?
  5. 如何计算与记录小恐龙跑出的分数?
  6. 如何暂停游戏、继续游戏?
  7. 如何在不重启程序的情况下重新开始一局新的游戏?

关于上述第3点,给出一些补充:

  1. 根据正常操控逻辑,跳跃键(如空格键)需要点按生效,下蹲键(如下方向键)需要长按生效。
  2. 如果一边按毫无意义的按键(如字母键),一边按跳跃键,小恐龙能否不卡顿地正常起跳?
  3. 如果长按或快速按跳跃键,小恐龙会飞上天吗?还是只能一下一下地跳?
  4. 如果在跳跃的过程中按下蹲键,小恐龙会有什么表现?
  5. 如果按住下蹲键的同时按跳跃键,小恐龙会有什么表现?

按键操控有许多细节,大家都可以在原版小恐龙游戏中尝试,并在自己的代码中体现出来。若能完美实现,获得一定的加分,见下。


关于两种界面的实现,分别给出一定的要求:

命令行字符界面

推荐使用 Windows Terminal 来运行程序,你需要锁定整个命令行界面的大小,可以自定义命令行字体大小,自行判断小恐龙与整个场景的比例。

字符的动画可以通过清屏+重新输出全部来实现(但这种做法效率很低哦),游戏的帧率与动画速度需要自行把控。

但显然,这个游戏越流畅越好玩。思考怎样让界面的字符动画能够尽可能流畅且屏幕不闪烁。

图形界面

图形界面的难点就是你要去自学怎么写好图形界面……参见本课程 图形界面要求

但在绘制小恐龙与场景与实现动画保证流畅度的时候会比字符界面简单非常多。

故就难度而言其实与命令行字符界面差不多,甚至更加简单。但要求也会更高哦~


评分标准

Sakiyary 人工评判,实现要求基本正常即可得到 8080% 以上的分数。

使用图形界面并不会得到更高的分数 ,若图形界面不符合要求或 bug 太多,得分甚至会比命令行字符界面的实现更低。

剩余分数由相应的加分条件给出:

  • 基础功能十分完善,界面美观,动画流畅,没有 bug。(bug 太多会倒扣
  • Sakiyary 认为你的代码结构非常合理,或是对 C 语言的掌握较好。
  • 实现一些扩展功能:
    • 随得分增长小恐龙加速;
    • 更多种类的障碍物;
    • 道具与状态(飞行、无敌、冲刺);
    • 符合游戏逻辑的情况下自由发挥
  • 对于命令行字符界面,实现流畅的动画、绘制更美观且比例适当的字符画则加分。

技术要求

编写项目文档(实验报告),须放在项目中容易找到的地方。

文档写得必须简洁,超过两页倒扣分数。

可以是任意格式,推荐 Markdown 格式,清晰即可,不必拘泥于排版(也不会加任何分)。

Markdown 可以在 课程资源-教程视频-Markdown 自行学习。

文档中请务必指出:

  • 平台(操作系统)
  • C 语言环境(mingw、msvc等及其详细的版本)
  • 编译选项(CMakeLists、MakeFile、编译指令等)
  • 使用的第三方库
  • 游戏方式(你设计的键位、操作等)

可以少许地介绍项目设计思路,以及你认为你写的特别好的地方。

同时,你也可以写一些感想与意见,如果你能写出充实的感想或有价值的意见,你将获得教学团队的认可!

题目注解

  1. 小恐龙越大,整个场景就越大,字符界面动画的显示就越卡顿,碰撞判断就越困难。

    这里提供一种使用扩展ascii码的 12×812×8 的小恐龙画法(仅限 Windows mingw gcc):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <windows.h>
int main() {
SetConsoleOutputCP(437);
char dino[8][12] = {
{32, 32, 32, 32, 32, 32, 95, 95, 95, 95, 95, 32},
{32, 32, 32, 32, 206, 181, 32, 64, 32, 32, 32, 179},
{32, 179, 32, 32, 206, 181, 32, 32, 32, 200, 205, 181},
{47, 179, 32, 32, 206, 181, 32, 32, 32, 218, 196, 217},
{92, 192, 208, 208, 208, 217, 32, 32, 32, 198, 203, 187},
{32, 92, 95, 95, 95, 95, 95, 95, 47, 32, 32, 32},
{32, 32, 32, 32, 179, 32, 32, 179, 32, 32, 32, 32},
{32, 32, 32, 32, 207, 205, 32, 207, 205, 32, 32, 32}};
for (int i = 0; i < 8; i++, printf("\n"))
for (int j = 0; j < 12; j++)
printf("%c", dino[i][j]);
return 0;
}

预览效果:

![image](Untitled 2.assets/QQ图片20221121145105.png.2e50b1e5)

TODO(持续更新ing……)

提交方式

如果使用了第三方库,可以自行针对自己选择的库查阅如何生成release版本的可执行的文件(也就是打包发布的方法,而不是只是单纯在提交的压缩包里复制一个编译生成的可执行文件,这样的文件在使用第三方库的情况下多半是无法在另外一台电脑上直接运行的)并放在bin目录下;如果你没有使用第三方库(比如只是CLI界面),那么直接复制编译生成的可执行文件可能是可行的。注意要注明你的编译生成文件对应的平台,此项不作为强制要求,但是这样的操作可以让你把游戏分享给其他人进行游玩。

将源码的压缩包提交到课程系统中(打开代码编辑器->上传压缩文件),保持原始的目录结构,并在文档中说明如何你的源码如何编译生成对应的可执行文件和基本操作的方式

可以参考如下的结构

1
2
3
4
5
6
7
8
9
10
11
|--学号_姓名.zip
| |--src //(源码文件夹)
| | |--a.c
| | |--b.c
| | |--Makefile
| | |--……
| |--bin //(可执行文件文件夹,如果有)
| | |--win
| | | |--game_x64.exe
| | |--……
| |--实验报告_学号_姓名.pdf

羊了个🐏

题目描述

出题人:张哲恺

验题人:张哲恺

项目概述

没玩过羊了个羊的人只能度过一些相对美好的夜晚😄

羊了个羊是一款突然也不知道为什么就火起来了的垃圾游戏,玩家可以选择地图中随机出的卡片放到界面下方的卡槽中,卡槽中每具有三张同种类的卡片就可以消除掉它们,如果地图中的卡片被全部消除则 You Win,如果卡槽中卡片堆满了则 Game Over。

可以参考一些视频来了解具体的游戏流程。


Corax 在接触到羊了个羊之后很快就恨上了这款随机无解的垃圾游戏,在无数的夜晚向文件传输助手转发了无数的广告之后,Corax终于破防了,然而他的好胜心不允许他征服不了这款游戏,于是他决定让你帮他写一个更合理羊了个羊,以此来通关这个游戏曲线救国。


实现要求

你可以使用图形界面,也可以使用字符的命令行界面来运行游戏

游戏的逻辑需要正确实现,例如当地图上的卡片被清空或者卡槽中的卡片堆满且无法消除时游戏要能够正常结束,卡槽中同种类的卡片应该堆放在相邻的位置,每三张相同卡牌要能够正常消除等

卡牌要能够明确地分辨其边界,地图中需要有多层卡牌,并且能通过上层卡牌看到部分下层卡牌(可参考实机游戏画面,字符界面会在后面说明),下层卡牌上面的数张上层卡牌未被清空前不可选择下层的卡牌(具体要求在后面说明),你需要保证游戏有解,即不会存在剩下两张或一张某种卡牌的情况

你还需要实现一些附加的游戏道具功能,如洗牌(即重新打乱地图中卡牌的位置,但你仍然需要保证游戏有解),撤销(即将最近一张放入卡槽的卡牌放回其原有的位置),移出卡牌(即将卡槽中现有的卡牌移出,并放置于地图中的随机位置)

游戏中要能够暂停、继续、退出游戏,在游戏结束后要能够重新开始,而不是关掉程序重新打开


图形界面附加要求

首先请仔细阅读课程网站上的图形界面要求

如果你选择使用图形界面运行游戏,那么需要通过鼠标点击卡牌进行操作,通过设置图片的饱和度或给图片蒙一层半透明蒙版等来区分可选择的上层卡牌和暂时不可选择,仅可见的下层卡牌。

字符界面附加要求

如果你选择使用字符的命令行界面运行游戏,那么需要通过键盘进行操作,为了区分卡牌的边界和能看见下层的卡牌,你需要给每一张卡牌画上边界,可参考下图

![img](Untitled 2.assets/image.png.0ab1f489)

示例中用+-绘制了地图边界,用扩展ascii码中的 191192217218 以及 -| 绘制了卡牌的边框,为了在你的代码中使用扩展ascii码(关于什么是扩展ascii码以及更多其中的符号请自行STFW),你可以尝试以下代码:

1
2
3
4
5
#include<windows.h>

int main(){
SetConsoleOutputCP(437);
}

将输出的编码标准切换至允许使用扩展ascii编码的标准,你也可以使用其他的字符来绘制边框,总之你的游戏中卡牌的边框要可见。但卡槽中的卡片可以没有边框,否则会显得有些冗杂

选择卡牌时可以通过或者WASD来控制选择光标的移动,并将选中的卡牌编号或者整张卡牌的颜色切换为显眼的颜色,因此你的程序逻辑要正确地控制光标的移动,例如在上面的图片中按下右键,22号卡牌变回白色,33号卡牌变为红色,再按下右键,应该是位于顶层可被选中的11号卡牌变为红色,而非位于第二层且仍被第一层卡牌压住的55号卡牌变为红色,(如果5号卡牌上没有第一层卡牌,那么应该是它被选中)另外,如果Corax 输入一系列预期之外的字符你的程序应该无视并且仍然能够正常运行

为了改变输出的字符颜色,你可以使用以下代码:

1
2
3
4
5
6
7
8
9
#include<windows.h>

int main(){
printf("The color is white now. U'll change it to red.\n");
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 4);
printf("The color is red now. U'll change it again\n");
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 7);
printf("It turns back to white.");
}

你可以尝试改变 SetConsoleTextAttribute 函数的第二个参数来获得不同的颜色。

至于按下键盘上的哪个键将当前选中的卡牌放入卡槽可由你自己定义。

tips:以上代码均仅适用于 Windows 操作系统环境,如果是 Linux/macos 用户请自行 STFW/RTFM,应该需要用到 UTF-8 字符集。


评分标准

Corax 人工评判,实现要求基本正常即可得到 8080% 以上的分数。

使用图形界面并不会得到更高的分数,若图形界面不符合要求或 bug 太多,得分甚至会比命令行字符界面的实现更低。

剩余分数由相应的加分条件给出:

  • 基础功能十分完善,界面美观,没有 bug。(bug 太多会倒扣)
  • Corax 认为你的代码结构非常合理,或是对 C 语言的掌握较好。
  • 实现一些扩展功能:
    • 若Corax在一定时间内消除了足够数量的卡牌,即可进入狂热模式,在此期间他可以快速地消除地图上的卡牌,但你需要通过一定手段保证狂热模式结束后游戏仍然有解,并且Corax在游戏期间要能看见狂热模式的积攒条;
    • 除此之外,你还可以设计一些其他有趣的功能。
  • 对于图形界面,实现正确的图片动态移入卡槽可酌情加分。
  • 对于命令行字符界面,实现附加的选牌逻辑加分,附加的选牌逻辑如下:
    • 当输入→后如果当前卡牌的正右方没有卡牌,但右下/上方有卡牌,那么应该移动到右下/上方的卡牌,其他三个方向同理;
    • 除此之外,你需要用另外一种颜色来标记可以被选中的卡牌,区分不可被选中的卡牌和已经被选中的卡牌。

技术要求

编写项目文档(实验报告),须放在项目中容易找到的地方。

文档写得必须简洁,超过两页倒扣分数。

可以是任意格式,推荐 Markdown 格式,清晰即可,不必拘泥于排版(也不会加任何分)。

Markdown 可以在 课程资源-教程视频-Markdown 自行学习。

文档中请务必指出:

  • 平台(操作系统)
  • C 语言环境(mingw、msvc等及其详细的版本)
  • 编译选项(CMakeLists、MakeFile、编译指令等)
  • 使用的第三方库
  • 游戏方式(你设计的键位、操作等)。

可以少许地介绍项目设计思路,以及你认为你写的特别好的地方。

同时,你也可以写一些感想与意见,如果你能写出充实的感想或有价值的意见,你将获得教学团队的认可!

提交方式

如果使用了第三方库,可以自行针对自己选择的库查阅如何生成release版本的可执行的文件(也就是打包发布的方法,而不是只是单纯在提交的压缩包里复制一个编译生成的可执行文件,这样的文件在使用第三方库的情况下多半是无法在另外一台电脑上直接运行的)并放在bin目录下;如果你没有使用第三方库(比如只是CLI界面),那么直接复制编译生成的可执行文件可能是可行的。注意要注明你的编译生成文件对应的平台,此项不作为强制要求,但是这样的操作可以让你把游戏分享给其他人进行游玩。

将源码的压缩包提交到课程系统中(打开代码编辑器->上传压缩文件),保持原始的目录结构,并在文档中说明如何你的源码如何编译生成对应的可执行文件和基本操作的方式

可以参考如下的结构

1
2
3
4
5
6
7
8
9
10
11
|--学号_姓名.zip
| |--src //(源码文件夹)
| | |--a.c
| | |--b.c
| | |--Makefile
| | |--……
| |--bin //(可执行文件文件夹,如果有)
| | |--win
| | | |--game_x64.exe
| | |--……
| |--实验报告_学号_姓名.pdf

飞行棋

题目描述

出题人:浦亮

验题人:浦亮

项目概述

飞行棋是十字戏类游戏,以模拟飞机飞航为主题,游戏以飞机由机场起飞至目的地,所以称为飞行棋。飞行棋是中国参考英国十字戏发展出来的,而英国十字戏是从印度十字戏演变出来的。

在这个项目里,需要大家完成一个简易版本的飞行棋游戏,最终呈现的效果可以是命令行呈现,也可以是自己学习图形库后以GUI的形式呈现。

![image](Untitled 2.assets/QQ图片20221102224124.png.0d47332e)

项目要求

你可以使用命令行的字符界面或图形界面来制作并运行游戏。最终实现的效果既可以是通过在终端里打印飞行棋的棋盘来表现,也可以是完整的图形界面。但要求一定要对棋盘上的情况有所呈现。

shuilongzhihun 将人工审查你的代码(与其他同学、各大开源平台上的开源代码进行比对与查重)并按照 抄袭与惩罚 执行。

简化版飞行棋规则(游戏基本逻辑)

  1. 有2-4个“玩家”。
  2. 每个“玩家”操控一个颜色对应的棋子,“玩家”可以是真人控制也可以是电脑控制,“玩家”可以全是真人也可以全是电脑。真人通过与实现系统交互来进行游戏,电脑玩家自动进行游戏。
  3. 每个玩家四颗棋子,初始都在机场不能出门,按顺序投骰子(1-6点的骰子)。如果玩家骰子抛出6,则可以让自己一个在机场的棋子在起点准备出发,并且再抛一次骰子决定可以出门走几格(该次抛骰子无论抛出几都只能作为刚刚准备出发的棋子前进的步数)。
    1. 如果玩家的每个棋子都在机场或者终点处,且没有抛到6,则该玩家这个回合结束。
    2. 如果玩家抛出的点数是1-5,且有至少一个既不在机场也不在终点的棋子,则可以选择一个棋子根据点数往前移动一定步数。在简化实现中,所有的棋子都共享一条直线型的跑道!
    3. 如果玩家同时有棋子在机场和已经出发,在抛到6的情况下如何处理留给同学自行设计(可以设计成只能让棋子出发,或者也可以设计成可以选择让已经出发的前进,或其他设计)。
    4. 为了方便大家的实现,在这里做了简化处理。如果有同学想实现原版的效果,可以作为扩展功能实现。
  4. 同时要求 直线型的跑道格子数量不低于15个,也就是下图中中间的深蓝色格子不少于15个。
  5. 终点的格子数应该为6个

![image](Untitled 2.assets/r1.png.2925d469)

  1. 在终点前要进行分流,各自进入对应颜色的分流终点。然后进入终点分流阶段。

![image](Untitled 2.assets/r2.png.5e82d363)

如图,绿色的棋子如果此时是抛出了某些点数,或者是因为前面抛出了点数走到了这个位置还有几步没有走完,接下来都应该进入绿色对应的终点区域

  1. 在终点区域,到达终点的判定是正好走到最后一格。如果没有走到则等待下一轮抛投,如果走到最后一格还有剩余步数,则需要反弹剩余步数。

![image](Untitled 2.assets/r3.png.16a0cb13)

如上图,在当前位置时,如果抛出的点数是3,则正好到达终点。如果抛出的点数是5,则反弹效果如图所示。

![image](Untitled 2.assets/r4.png.23a43eea)

  1. 一颗棋子达到终点的时候,就将这个棋子移出整个棋盘,记为成功到达。玩家胜利的条件是让全部的四颗棋子都到达终点。
  2. 对于同格子情况的处理:有不同的规则版本,在此处统一:
    1. 对于途径的其他棋子无视,不论那个途径的格子里有几个棋子。
    2. 如果本次移动最终落点处是己方棋子,则这两个棋子可以同时存在这个格子里(但是不采取某些可以叠子的规则,后续再进行移动的时候同一格的棋子仍然视为多个分开的个体前进)
    3. 如果本次移动最终落点处有敌方棋子,则己方棋子占据这个格子,所有在这个格子的敌方棋子不论数量都返回机场(也就是需要抛到6才能再出发的状态)

电脑操作

对于上述提到的电脑玩家操作,应当保证电脑的表现正常,即不存在非法操作(比如没有投到6但是让一个棋子出发),而且所有操作都是正常操作(比如不会在投了骰子且有棋子可以移动的情况下什么都不做)。

电脑玩家的操作也应当以某种方式(输出日志或其他)展现出来。

扩展功能

上述描述的实现标准是一个简化改进版本的要求,同学们可以在现有的基础上尝试实现更多效果,包括但不限于:

  • 一个实现得非常好的命令行交互系统或者图形界面;
  • 体现简化版本中没有提到的“飞”,即在某个满足特定要求的特定位置时可以飞到另外一格;
  • 体现简化版本中没有提到的“跳”,即给中间的深蓝色格子进行染色,当棋子移动的最终落点在同色的格子上时可以往前跳到下一个同色格子上;
  • 把跑道改为原版中的环形跑道的设计;
  • ……

你可以随意添加功能,但是理论上不能破坏现在的基本规则或者是使得规则变得更简单改动(例如删除终点的反弹但是又不设计一个更复杂的规则代替将会反过来影响你的成绩)。


关于两种界面的实现,分别给出一定的要求:

命令行字符界面

参见本课程 命令行字符界面要求,只是简单的将模拟的棋盘用一些代表特殊意义的字符不断的输出在屏幕上是可以的。

图形界面

参见本课程 图形界面要求


评分标准

shuilongzhihun 人工评判,实现要求基本正常即可得到 8080% 以上的分数。

使用图形界面并不会得到更高的分数,若图形界面不符合要求或 bug 太多,得分甚至会比命令行字符界面的实现更低。

剩余分数由相应的加分条件给出:

  • 基础功能十分完善,界面美观,动画流畅,没有 bug。(bug 太多会倒扣
  • shuilongzhihun 认为你的代码结构非常合理,或是对 C 语言的掌握较好。
  • 实现上述的扩展功能或自行发挥。

技术要求

编写项目文档(实验报告),须放在项目中容易找到的地方。

文档写得必须简洁,超过两页倒扣分数。

可以是任意格式,推荐 Markdown 格式,清晰即可,不必拘泥于排版(也不会加任何分)。

Markdown 可以在 课程资源-教程视频-Markdown 自行学习。

文档中请务必指出:

  • 平台(操作系统)
  • C 语言环境(mingw、msvc等及其详细的版本)
  • 编译选项(CMakeLists、MakeFile、编译指令等)
  • 使用的第三方库
  • 游戏方式(你设计的键位、操作等)

可以少许地介绍项目设计思路,以及你认为你写的特别好的地方。

同时,你也可以写一些感想与意见,如果你能写出充实的感想或有价值的意见,你将获得教学团队的认可!

项目注解

声明

该部分仅作为参考的建议和提示,如果你有更好的设计思路和实现,完全可以自己采用。

状态

对于棋子和棋盘格状态的存储,用数组和变量的方式就可以实现。比如用数组来记录当前棋子的位置和当前的状态,再用一些数组来存储棋盘格子内的状态。

游戏控制

对于游戏整体逻辑控制 可以参考如下的设计

1
2
3
4
5
6
while(!game_ends){
player=(player+1)%player_num;//切换玩家
die_point=getDieRes();//投骰子
move(player,die_point);//移动部分的判断
game_ends=judge();//判断游戏是否结束
}

命令行字符界面呈现

只是简单的将模拟的棋盘用一些代表特殊意义的字符不断的输出在屏幕上是可以的。例如像下面的模拟输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
------------------------------------------------
GG
GG DDDDD(green)
□□□□□□□□□□□□□□□□□□□□□□□□□
YY DDDDD(yellow)
YY
Green team throw the die and get 2 point!
Green team ubable to move!
------------------------------------------------
GG
GG DDDDD(green)
□Y□□□□□□□□□□□□□□□□□□□□□□□
Y□ DDDDD(yellow)
YY
Yellow team throw the die and get 6 point!
Yellow team throw the die again and get 2 point!
Yellow team moves.

当然,如果你对字符进行了染色并且实现了刷屏的效果(屏幕上不是通过类似上述分隔符分隔的多次输出来展示,而是通过清屏再输出或者修改屏幕上显示的某些字符来实现),这个设计可以被认为是一个扩展功能,根据效果可以作为评分标准里扩展功能部分的分数。

DEBUG模式

为了方便助教测试和自己debug,推荐自己在程序里加入一个“外挂”,比如在输入某些指令后,或者全局定义某些变量值为1或自定义宏(例如#define DEBUG)开启自己定义的debug模式,你可以编写操控骰子投出的点数的代码,从而可以控制棋子的走动。

示例代码:

1
2
3
4
5
6
7
8
9
#define DEBUG
//假设这是一个用于得到投骰子结果点数的函数
int getDieRes(){
#ifdef DEBUG
//TODO
#else
//TODO
#endif
}

该部分不计入分数,但是强烈推荐做一下。

先动脑 再动手!

由于在很多地方都存在共通之处,同学们在编写代码的时候可以先思考一下有些地方的代码是不是可以抽象出来作为一个函数在多个地方复用,而通过设置一些变量来进行区分,这样可以很好的实现代码的压缩,防止出现屎山,也会减少在复制的时候有些地方没改完全导致出错!

对于每个玩家阵营,很多流程是一样的,是否可以把里面的一些操作抽象出来,而不是在每回合的循环里复制四遍代码?

比如对于每个棋子(飞机)可以增加一个标签来标记他是什么颜色的棋子,这样对于棋子的一些操作就可以通用了。

比如对于真人和电脑玩家的区分,本质区别其实只有在移动的时候有区别,而且移动都要检查是否合法,那么可以考虑一个设计是:对于玩家的每个棋子,判断投出骰子对应点数的移动是否合法,如果合法,那么就返回一个所有可以选择移动的棋子列表,电脑是从中随机选一个移动,而真人是自己选择。这样真人和电脑的代码差异就变得很小了。

提交方式

如果使用了第三方库,可以自行针对自己选择的库查阅如何生成release版本的可执行的文件(也就是打包发布的方法,而不是只是单纯在提交的压缩包里复制一个编译生成的可执行文件,这样的文件在使用第三方库的情况下多半是无法在另外一台电脑上直接运行的)并放在bin目录下;如果你没有使用第三方库(比如只是CLI界面),那么直接复制编译生成的可执行文件可能是可行的。注意要注明你的编译生成文件对应的平台,此项不作为强制要求,但是这样的操作可以让你把游戏分享给其他人进行游玩。

将源码的压缩包提交到课程系统中(打开代码编辑器->上传压缩文件),保持原始的目录结构,并在文档中说明如何你的源码如何编译生成对应的可执行文件和基本操作的方式

可以参考如下的结构

1
2
3
4
5
6
7
8
9
10
11
|--学号_姓名.zip
| |--src //(源码文件夹)
| | |--a.c
| | |--b.c
| | |--Makefile
| | |--……
| |--bin //(可执行文件文件夹,如果有)
| | |--win
| | | |--game_x64.exe
| | |--……
| |--实验报告_学号_姓名.pdf

蜘蛛牌

题目描述

出题人:肖江

验题人:肖江

项目概述

单色蜘蛛纸牌是一款经典的休闲益智纸牌游戏,该游戏基本规则如下:

  • 有 �N 副牌,�M 个牌槽,�K次发牌机会
  • 初始牌槽中有 �×13−�×�N×13−K×M 张牌
  • 只可以移动牌槽最上层的连续牌组
  • 移动的目的地要么是空牌槽,要么能够和被移动牌组连接上
  • 形成一组完整的连续牌组后(�A~�K),该牌组被从牌槽中收回
  • 可以申请发牌,每个牌槽中新增一张牌,新增的牌可以和原牌组不连续
  • 所有牌全部收回时判定胜利
  • 无法进行有效操作时判定失败

比如有4个牌槽:

![image](Untitled 2.assets/image.png.7bba8639)

那么此时可以进行的操作是:

  1. 将牌槽1的 A, 2, 3移动到牌槽2;
  2. 将牌槽1的 A, 2 移动到牌槽3;
  3. 将牌槽3的 3 移动到牌槽2;
  4. 申请发牌。

其余均为非法操作。

更具体的规则过程可以观看如下两个视频

项目要求

我们需要在字符界面或者图形界面实现一个单色的蜘蛛纸牌。

该蜘蛛纸牌共包含88副牌(�A~�K),1010 个牌槽,初始时 1010 个牌槽各拥有 66、66、66、66、55、55、55、55、55、55 张牌,最上层的牌可见,其余牌不可见,有 55 次发牌机会,每次给 1010 个牌组各发一张牌,其余规则和游戏介绍中的一致。

基础功能

  • 初始化牌桌,展示牌槽,可发牌次数和已收回牌组;
  • 移动牌组;
  • 发牌;
  • 收回已形成的完整牌组;
  • 结束判定。

扩展功能

  • 撤回一次或多次操作;
  • 提示有效操作:
    • 需要按照要求和优先级进行提示,详见项目注解;
  • 计时器;
  • 多色的游戏模式。

关于两种界面的实现,分别给出一定的要求:

命令行界面

部分功能仍然需要实现动画,详见项目注解中命令行交互方式,并遵守命令行字符界面要求

图形化界面

可以直接参考游戏介绍中的视频,使用拖拽,高亮,鼠标点击按钮等交互方式,但请遵守图形界面要求


评分标准

875C 人工评判,实现要求基本正常即可得到 8080% 以上的分数。

使用图形界面并不会得到更高的分数,若图形界面不符合要求或 bug 太多,得分甚至会比命令行字符界面的实现更低。

剩余分数由相应的加分条件给出:

  • 基础功能十分完善,界面美观,动画流畅,没有 bug。(bug 太多会倒扣
  • 875C 认为你的代码结构非常合理,或是对 C 语言的掌握较好。
  • 实现上述的扩展功能或自行发挥。

技术要求

编写项目文档(实验报告),须放在项目中容易找到的地方。

文档写得必须简洁,超过两页倒扣分数。

可以是任意格式,推荐 Markdown 格式,清晰即可,不必拘泥于排版(也不会加任何分)。

Markdown 可以在 课程资源-教程视频-Markdown 自行学习。

文档中请务必指出:

  • 平台(操作系统)
  • C 语言环境(mingw、msvc等及其详细的版本)
  • 编译选项(CMakeLists、MakeFile、编译指令等)
  • 使用的第三方库
  • 游戏方式(你设计的键位、操作等)

可以少许地介绍项目设计思路,以及你认为你写的特别好的地方。

同时,你也可以写一些感想与意见,如果你能写出充实的感想或有价值的意见,你将获得教学团队的认可!

项目注解

接下来对失败条件判定和有效操作提示,以及交互方式做更进一步的解释

失败条件判定和有效操作提示

由于失败条件以及有效操作的定义和证明不属于C语言课程的范围,在这里我们直接给出它的定义,大家按照定义实现即可。

首先定义最大上层连续牌组,即一个牌槽内的上层的最多的连续牌组,如一个牌槽自底向上的牌是?, ?, ?, K, Q, 9, 7, 8, 3, 2, A?表示未翻面的牌),则最大上层连续牌组为3, 2, A

失败条件

失败条件为同时满足以下三条:

  1. 所有牌槽的最大上层连续牌组并集不包含完整的一副牌;
  2. 无法移动任何最大上层连续牌组;
  3. 无剩余发牌次数。

失败条件示例

假设一共有 33 个牌槽且无法发牌(项目要求是 1010 个,这里示例用 33 个展示逻辑过程)

![image](Untitled 2.assets/image.png.303ee8c1)

他们的最大上层连续牌组为 {A, 2, 3}, {8, 9, 10}, {5,6},并集为 {A, 2, 3, 5, 6, 8, 9, 10},不包含 �A~�K,且所有最大上层连续牌组都无法移动,且无法发牌,故判定失败。

有效操作

有效操作包括以下三种,按优先级从高到低排序,即“能提示1,就不要提示23”

  1. 通过移动牌组,将一副牌直接收回;
  2. 移动某个牌槽的最大上层连续牌组(但不能是全牌组和空牌组互换);
  3. 发牌。

有效操作1示例

假设一共有 44 个牌槽(项目要求是 1010 个,这里示例用 44 个展示逻辑过程)

![image](Untitled 2.assets/image.png.1a93a157)

此时可以通过将牌槽2的A移动到牌槽3,再将牌槽3的A,2,3移动到牌槽1,将一整副牌收回。

提示并不需要提示完整的操作过程,只需要高亮最大上层连续牌组并集能包含 �A~�K 的牌槽即可,样例中可以高亮牌槽1, 2, 3。

有效操作2示例

假设一共有 44 个牌槽(项目要求是 1010 个,这里示例用 44 个展示逻辑过程)

![image](Untitled 2.assets/image.png.34a931dc)

他们的最大上层连续牌组为{A, 2, 3}, {4}, {5, 6, 7},你可以提示将牌槽1的 A, 2, 3 移动到牌槽2,也可以提示将牌槽2的 4 移动到牌槽3,但是不可以提示将牌槽3的所有牌移动到牌槽4。

有效操作3示例

假设一共有 44 个牌槽(项目要求是 1010 个,这里示例用 44 个展示逻辑过程)

image

他们的最大上层连续牌组为{A, 2, 3, 4}, {4}, {6, 7}, {K},此时既没有有效操作1,也没有有效操作2,可以直接提示发牌。(如果无法发牌就说明应该判定失败了)

命令行交互方式

初始化牌桌,展示牌槽,可发牌次数和已收回牌组

无需动画,直接展示,可发牌次数和已收回牌组可以使用数字展示。

牌槽牌面最简单也需要使用:

1
2
3
+--+
| A|
+--+

展示,不能只有一个字符。

移动牌组

可以直接使用命令进行交互,如 m 3 2 1 表示牌槽3移动到牌槽2,移动1张牌。

也可以使用,选定原牌槽->选定牌组->选定目标牌槽的方式进行移动。

动画至少需要三个关键帧,即原牌桌,移动到半途的牌桌,移动完毕的牌桌。

发牌

可以使用按键发牌。

动画需要至少两个关键帧,展示要发的牌,将牌发到各个牌槽。

收回已形成的完整牌组

动画至少需要两个关键帧,高亮完整牌组,收回牌组且计数器增加/牌组增加。

结束判定

弹窗,不要覆盖牌桌,因为需要截图展示,可以在牌桌旁展示弹窗提示胜利或失败。

撤回操作

无需动画,直接使用弹窗提示撤回的操作类型——发牌/移动,并还原牌桌状态。

提示有效操作

按照有效操作一节中的要求和优先级进行有效操作提示,使用按键或命令激活提示。

计时器

直接展示即可,不要出现跳表情况,即不能出现 00:01 直接变成 00:04

提交方式

如果使用了第三方库,可以自行针对自己选择的库查阅如何生成release版本的可执行的文件(也就是打包发布的方法,而不是只是单纯在提交的压缩包里复制一个编译生成的可执行文件,这样的文件在使用第三方库的情况下多半是无法在另外一台电脑上直接运行的)并放在bin目录下;如果你没有使用第三方库(比如只是CLI界面),那么直接复制编译生成的可执行文件可能是可行的。注意要注明你的编译生成文件对应的平台,此项不作为强制要求,但是这样的操作可以让你把游戏分享给其他人进行游玩。

将源码的压缩包提交到课程系统中(打开代码编辑器->上传压缩文件),保持原始的目录结构,并在文档中说明如何你的源码如何编译生成对应的可执行文件和基本操作的方式

可以参考如下的结构

1
2
3
4
5
6
7
8
9
10
11
|--学号_姓名.zip
| |--src //(源码文件夹)
| | |--a.c
| | |--b.c
| | |--Makefile
| | |--……
| |--bin //(可执行文件文件夹,如果有)
| | |--win
| | | |--game_x64.exe
| | |--……
| |--实验报告_学号_姓名.pdf

苏联块

题目描述

出题人:孙博文

验题人:孙博文

Sakiyary:我摆烂了,不想格式化题面了,和其他项目题总要求是一样的……

“俄罗斯方块”(Tetris)是一个经久不衰的小游戏。我们这就来做一个:

原版游戏当中Tetra4。我们做一个 Extended Edition

硬性要求:不做掉大分。

  • 掉落的方块,在原游戏基础上增加几种:

    3方块的 “L” 和 “l” 型, 5 方块的 3 种 “L” 型。

  • 容纳方块大小至少为 12 列 16 行。游戏界面自适应当前终端,如果终端尺寸不够放下全部内容,则不能启动并给出一行提示。

  • 使用上下左右方向键!方向键!方向键!实时控制方块旋转与下落,直到方块的下表面与已有方块接触,则刷出下一个方块

  • 旋转前后,如果没有卡上游戏 tick (即向下掉落一格的时刻),方块的最下端高度不应变化

  • 输入不合法的字符,不应使你的程序卡顿、崩溃、异常。

  • 行填满需要消除,下落。并计分。

  • 屏幕内给出操控指示。支持暂停,和重新开始。

软性:

  • 最好能打印出较为标准的正方形。比如终端内打印两个黑方块▇▇来组成一个。否则方块旋转后会变形
  • 按下方向下键时方块速度增加
  • 支持计分榜等扩展功能

本实验选题不会奖励写图形界面的同学。如果图形界面有bug还可能导致多的扣分。

命令行界面简洁而完整。

评分标准

请仅提交三个文件放在 .zip 压缩包内

1
2
3
4
submission.zip
- tetris.c
- tetris.exe / tetris.out
- report.txt

我们会运行你的程序。基本能玩就给 80 分。游戏交互逻辑较好并且没 bug 就是一伯分。

我们会做包括但不限于下列操作:

  • 正常地游玩游戏
  • 键盘随机输入一大堆随机字母数字,并观察你程序的行为

项目报告

  • 你的平台(Windows/Linux/MacOS),编译选项,(依赖的图形库),具体交互方式(图形化拖拽 or 命令行按键)
  • 简单思路
  • 完成项目的感想和意见
  • 建议使用 txt 纯文本
  • 不要过长,不超过两页,主要是将前两项说清楚,方便助教验收,前两项说清楚就是满分,如果你能写出充实的感想或有价值的意见,你将获得教学团队的认可

期末项目选题及要求

冬津羽戏

题目描述

出题人:张哲恺

验题人:张哲恺

项目概述

俗名打砖块😄打砖块是一款总共可以发射数个小球,并通过控制挡板位置,反弹小球使其击碎路径上砖块并不断反弹的游戏。

同样可以参考一些视频来了解具体的游戏流程。


Corax 收到大冒险家 Sakiyary 想要自己做一个打砖块游戏的委托但是却不知所措,于是他决定转发委托,并作为中间商在其中狠狠捞一笔。不过纯粹的打砖块太过次时代缺乏趣味性,只能让Sakiyary 获得 3030% 的满意度,为了让产品能 100100% 使委托人满意,他需要你附加实现双人联机mod奇幻之旅DLC


实现要求

你可以使用图形界面,也可以使用字符的命令行界面来运行游戏。

如果你选择使用图形界面,请仔细阅读课程网站上的图形界面要求

游戏的逻辑需要正确实现,例如挡板只能在一定的区域内移动,当小球从地图下边界离开时要重新发球,地图中所有砖块都被击碎后要结束游戏或进入下一关(刷新地图),挡板和砖块要能够反弹小球,砖块反弹小球后要判定其受到撞击,反弹的角度需要对称等等。

砖块的不同种类要能够区分(具体内容将在下面继续说明)


双人联机mod要求

你需要使用socket网络编程实现两个客户端在同一局游戏中操控两个不同的挡板一起游戏,采用客户端、服务端二分的模式,因此你需要编写两个程序

服务端:

  • 监听某个主机的某个端口
  • 监听并应答来自客户端的连接和请求
  • 维护游戏状态,包括两个玩家各自的挡板位置等,因此在多人模式下,你需要对两个玩家的挡板做出相应的区分

客户端:

  • 向服务端发起请求或通讯
  • 维护游戏状态,包括小球的运动轨迹,地图的状态(此状态也可交由服务器维护,具体做法由你自己决定)等

在联机游玩的两个客户端程序之间画面的延迟不能太高,状态要能够正确维护。

参考的思路是:服务端创建监听线程,每接收到一个消息,将其放入一个全局队列中,等待处理;另一个线程负责处理来自客户端的请求,while(1) 循环从队列中取一条消息,然后分析其中的内容。客户端创建与服务端连接的线程,负责与服务端进行通信;另一个线程,负责计算游戏进程和输出游戏图形。这里需要用到多线程,具体的做法请STFW/RTFM,至于原因你不妨让服务端对某一种客户端请求一直不做应答试试看:D

既然只是mod,你也需要支持单机游玩,毕竟 Corax 虽然不能双人成行,但是他单人也行,请注意你应该如何实现单机和联机两种模式,减少代码克隆的现象。


奇幻之旅DLC要求

你需要给砖块和小球附加不同的元素属性,砖块可以添加生命值属性(需要额外进行数次撞击才能彻底击碎),还需要添加一系列道具。

不同元素属性的砖块需要通过颜色来区分,例如红色代表火元素,深蓝色代表水元素,浅蓝色代表冰元素,紫色代表雷元素,黄色代表暂时还没有元素。

对于字符界面选手来说,可以使用自己喜欢的符号或画出的图形来代表小球和砖块,为了改变输出的字符颜色,你可以使用以下代码:

1
2
3
4
5
6
7
8
9
#include<windows.h>

int main(){
printf("The color is white now. U'll change it to red.\n");
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 4);
printf("The color is red now. U'll change it again\n");
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 7);
printf("It turns back to white.");
}

你可以尝试改变 SetConsoleTextAttribute 函数的第二个参数来获得不同的颜色。

tips:以上代码均仅适用于 Windows 操作系统环境,如果是 Linux/macos 用户请自行 STFW/RTFM,应该需要用到 UTF-8 字符集。

对于砖块和小球的元素属性,你可以设计丰富有趣的机制,小球的元素可以由上次与挡板相撞时挡板的元素属性来确定,玩家可以控制并改变挡板的元素属性。例如火元素与雷元素相遇会爆炸,对半径一定范围内的砖块全部造成一次攻击;水元素与雷元素、冰元素相遇会引发链式反应,导致相邻的水元素方块全部受到一次攻击;雷元素与冰元素相遇会强化小球,使其在下次与地图边界相撞并反弹前不会被砖块反弹;暂时无元素方块在被相应元素小球撞击后会附着上相应元素等等。

你需要设计一系列丰富有趣的道具,例如在地图下边界暂时生成防护墙保护小球不会离开地图,小球与挡板或砖块碰撞后生成额外的小球,挡板自身发射垂直向前的子弹攻击砖块等等,道具的触发条件也可以多种多样,可以由击碎砖块后随机触发,可以由击碎特殊砖块触发,也可以由当前小球击碎数个砖块后触发。

你可以从链接中的视频参考上述描述的具体实现。


评分标准

打砖块本体占分比重 30%,奇幻之旅DLC占分比重 40%,双人联机mod占分比重 30%,后两者的分数依赖于打砖块本体,也即如果你只写了一个实现完美的联机mod你的分数也只会是 0 分

Corax 人工评判,以上三个模块实现每个模块内的要求基本正常即可得到各自模块内 80% 以上的分数(也可以通过前两项拿满分,第三项水一水来得到 80% 以上的分数)。

使用图形界面并不会得到更高的分数,若图形界面不符合要求或 bug 太多,得分甚至会比命令行字符界面的实现更低。

剩余分数由相应的加分条件给出:

  • 基础功能十分完善,界面美观,没有 bug。(bug 太多会倒扣)
  • Corax 认为你的代码结构非常合理,或是对 C 语言的掌握较好。
  • 对于打砖块本体,能够实现挡板的普通速度和双倍速度移动,一定范围内的上下移动并根据与小球相撞时挡板的移动方向和速度改变小球的反弹方向(不要求严格按照物理规律,有不惊动牛顿棺材板的影响即可,当然如果能模拟真实物理情况也很好,你同样可以参考链接中的视频)加分。
  • 对于奇幻之旅DLC,不同元素的反应和道具丰富有趣无bug,能够实现砖块生命值大于等于2时元素产生反应后自身附着元素消失,根据反应给周围的方块附着元素(例如水与雷的链式反应可能可以使与路径上的砖块相邻的砖块附着上雷元素等),并且小球与挡板发生的元素反应可以使小球的下次碰撞产生相应的反应(例如火球与雷元素挡板相撞并反弹后,下次与砖块相撞时引发爆炸等等,此时小球具有的反应状态应该在程序内对玩家可见)即可加分。(每一点都可以加分,不是全部实现才加分
  • 对于双人联机mod,能够实现游玩过程中两个客户端画面延迟非常小,多线程和要求中提到的问题完成较好即可加分。

技术要求

编写项目文档(实验报告),须放在项目中容易找到的地方。

文档写得必须简洁,超过两页倒扣分数。

可以是任意格式,推荐 Markdown 格式,清晰即可,不必拘泥于排版(也不会加任何分)。

Markdown 可以在 课程资源-教程视频-Markdown 自行学习。

文档中请务必指出:

  • 平台(操作系统)
  • C 语言环境(mingw、msvc等及其详细的版本)
  • 编译选项(CMakeLists、MakeFile、编译指令等)
  • 使用的第三方库
  • 游戏方式(你设计的键位、操作等)。

可以少许地介绍项目设计思路,以及你认为你写的特别好的地方。

同时,你也可以写一些感想与意见,如果你能写出充实的感想或有价值的意见,你将获得教学团队的认可!

提交方式

如果使用了第三方库,可以自行针对自己选择的库查阅如何生成release版本的可执行的文件(也就是打包发布的方法,而不是只是单纯在提交的压缩包里复制一个编译生成的可执行文件,这样的文件在使用第三方库的情况下多半是无法在另外一台电脑上直接运行的)并放在bin目录下;如果你没有使用第三方库(比如只是CLI界面),那么直接复制编译生成的可执行文件可能是可行的。注意要注明你的编译生成文件对应的平台,此项不作为强制要求,但是这样的操作可以让你把游戏分享给其他人进行游玩。

将源码的压缩包提交到课程系统中(打开代码编辑器->上传压缩文件),保持原始的目录结构,并在文档中说明如何你的源码如何编译生成对应的可执行文件和基本操作的方式

可以参考如下的结构

1
2
3
4
5
6
7
8
9
10
11
|--学号_姓名.zip
| |--src //(源码文件夹)
| | |--a.c
| | |--b.c
| | |--Makefile / CMakeLists.txt
| | |--……
| |--bin //(可执行文件文件夹,如果有)
| | |--win
| | | |--game_x64.exe
| | |--……
| |--实验报告_学号_姓名.pdf

中国象棋

题目描述

出题人:肖江

验题人:肖江

项目概述

中国象棋是一种起源于中国,历史悠久的棋类游戏。你需要实现一个双人联机对战的象棋游戏。

实现要求

你可以使用图形界面,也可以使用字符的命令行界面来运行游戏。

如果你选择使用图形界面,请仔细阅读课程网站上的图形界面要求

游戏的逻辑需要正确实现,比如各类棋子不能有非法移动,最终胜负判定能够正常进行

联机要求

你需要使用socket网络编程实现两个客户端在同一棋局内对弈的功能,采用客户端,服务端分离的模式,因此你需要实现两个程序

你可以采取状态同步或者帧同步的方式对客户端和服务端进行分工。

状态同步:棋盘的运算都发生在服务端,客户端只负责和用户交互

帧同步:棋盘的运算都发生在客户端,服务端只负责转发客户端动作消息到对手侧

在联机游玩的两个客户端程序之间画面的延迟不能太高,状态要能够正确维护。

参考的思路是:服务端创建监听线程,每接收到一个消息,将其放入一个全局队列中,等待处理;另一个线程负责处理来自客户端的请求,while(1) 循环从队列中取一条消息,然后分析其中的内容。客户端创建与服务端连接的线程,负责与服务端进行通信;另一个线程,负责计算游戏进程和输出游戏图形。这里需要用到多线程,具体的做法请STFW/RTFM。

额外功能

你可以实现一部分额外功能以获得更高的分数,包括但不限于

  • 悔棋
  • 倒计时
  • 打赏/催促对方
    • “你这么菜你老师不会生气吧哥哥”
    • “每一盘都当最后一盘,然后也不要怕输”
    • “3,2,1,落子!”
  • 添加道具/技能
    • 改变棋子的行为规则
    • 改变整个棋局的状态,比如添加棋子,转换棋子等
    • 可以参考万宁象棋

评分标准

原生象棋移动和胜负判定占分比重 60%,额外功能占分比重 15%,联机功能占分比重 20%,代码风格,项目组织占分比重 5%。

875C 人工评判,以上三个模块实现每个模块内的要求基本正常即可得到各自模块内 80% 以上的分数。

使用图形界面并不会得到更高的分数,若图形界面不符合要求或 bug 太多,得分甚至会比命令行字符界面的实现更低。

技术要求

编写项目文档(实验报告),须放在项目中容易找到的地方。

文档写得必须简洁,超过两页倒扣分数。

可以是任意格式,推荐 Markdown 格式,清晰即可,不必拘泥于排版(也不会加任何分)。

Markdown 可以在课程资源-教程视频-Markdown自行学习。

文档中请务必指出:

  • 平台(操作系统)
  • C 语言环境(mingw、msvc等及其详细的版本)
  • 编译选项(CMakeLists、MakeFile、编译指令等)
  • 使用的第三方库
  • 游戏方式(你设计的键位、操作等)。
  • 可以少许地介绍项目设计思路,以及你认为你写的特别好的地方。

同时,你也可以写一些感想与意见,如果你能写出充实的感想或有价值的意见,你将获得教学团队的认可!

提交方式

如果使用了第三方库,可以自行针对自己选择的库查阅如何生成release版本的可执行的文件(也就是打包发布的方法,而不是只是单纯在提交的压缩包里复制一个编译生成的可执行文件,这样的文件在使用第三方库的情况下多半是无法在另外一台电脑上直接运行的)并放在bin目录下;如果你没有使用第三方库(比如只是CLI界面),那么直接复制编译生成的可执行文件可能是可行的。注意要注明你的编译生成文件对应的平台,此项不作为强制要求,但是这样的操作可以让你把游戏分享给其他人进行游玩。

将源码的压缩包提交到课程系统中(打开代码编辑器->上传压缩文件),保持原始的目录结构,并在文档中说明如何你的源码如何编译生成对应的可执行文件和基本操作的方式

可以参考如下的结构

1
2
3
4
5
6
7
8
9
10
11
|--学号_姓名.zip
| |--src //(源码文件夹)
| | |--a.c
| | |--b.c
| | |--Makefile/ CMakeLists.txt
| | |--……
| |--bin //(可执行文件文件夹,如果有)
| | |--win
| | | |--game_x64.exe
| | |--……
| |--实验报告_学号_姓名.pdf

落井大战

题目描述

出题人:李薛成

验题人:李薛成

项目概述

Downwell,是一款风靡全球的 Roguelike 竖版过关游戏,是一位日本音乐专业的大佬在大四折腾出来的小游戏。你可能从没听说过这款游戏,那么你可以从下面几个链接中初步了解一下:

Downwell - 日本独立游戏佳作 - 知乎

《下井大战》的游戏性是你难以想象的 - 知乎

也可以上这款游戏的官网 Downwell (downwellgame.com) 购买/下载体验,或者上 B 站看大佬的通关视频视频链接

(速去 Steam 买!优惠只要 ¥4.5,1月6日0点结束!)


项目要求

你可以使用图形界面,也可以使用字符的命令行界面来运行游戏。

如果你选择使用图形界面,请仔细阅读课程网站上的图形界面要求

首先我们要明确你需要做一个什么样的游戏出来。显然,如果你能模仿原作实现它的绝大部分功能,拿到满分应该很大。但我们还是来解构一下这款游戏的要素:

基础要素

  • 一位可以操控的主角,主角需要三种基础交互:左、右移动、跳跃/向下射击。
  • 一个竖版的地图(井),地图上随机生成有方块拼成的各种形状平台供主角落脚。
  • 怪物们,可以是在墙上爬、在平台上爬、在空中飞、同时会或不会发射子弹攻击、带刺或不带刺的怪物。
  • 主角在平台上时只能跳跃,在空中可以向下射击子弹,发射的同时获得一定的滞空,同时消耗弹夹中的子弹数量。弹夹需主角停留在平台上方可自动地逐渐地回满。
  • 主角与怪物都有各自的血量,如主角有 4 格血、怪物有 8 格血,受到一次攻击就掉相应的血。
  • 主角踩在不带刺的怪物上方可以对怪物造成伤害,踩在带刺的怪物身上会受到伤害。

到这里,游戏最最基本的解构就完成了。

Rougelike 要素

但原作是 Rougelike 游戏,所谓肉鸽(Rougelike),首先是在一定固定规则上的随机,如随机地形、随机怪物,其次就是通过阶段性的随机奖励/道具,来提升或改变角色的能力(攻击力、攻击方式、弹夹容量、血量、恢复力等等)。同时,地图并不是一图到底无限延伸,而是分为一个个关卡。怪物也会随着关卡层数(下降层数)变强,数量、种类更多、速度更快、攻击更猛。在关底(最后的关卡)有强力 Boss,打败 Boss 游戏就算通关。来解构一下这一部分的要素:

  • 随机性,地形随机生成,怪物(符合关卡难度的基础上)随机生成,道具随机生成等等。
  • 要让主角变强,就要获得奖励/道具。道具可由击杀怪物掉落,或者通过击杀怪物得到的货币来到随机生成的商店处购买(商店内道具随机生成)。这也使得主角需要更多的交互操作,如在商店中购买道具、开启主动技能等等。同时需要有货币机制。
  • 关卡设计,如每次下降一定的单位长度就自动进入下一关,每过一关主角可以获得一些随机道具。关卡中的怪物需要体现递进,即越来越难。难度的提升可以体现在怪物活动方式、攻击方式上的不同与递进。
  • 在关底设计 Boss,拥有独特的存在方式、活动方式与攻击方式,可以模仿上述视频中 Boss 的行为与机制。
  • 通关机制,评分机制。

那么 Rougelike 要素就差不多了,这只是一些粗浅的解构,但对本项目已经足够了。


在初步解构完要素的基础上,开始提出具体的要求:

  • 小恐龙🦖(dino)中大部分要求基本相同。
  • 地图中随机平台的生成,不能完全堵住路,也不能太过散乱。
  • 子弹(我方或敌方)的路线设计(直线、曲线、跟踪)与内存管理。
  • 至少实现三种不同机制的怪物(如只爬动不攻击、爬动且带刺、只飞行不攻击)。
  • 合理的碰撞判定与扣血机制。
  • 弹夹消耗与补充机制,发射子弹时主角的滞空机制。
  • 关于 Rougelike 要素的具体要求全凭你自己的理解,随意发挥即可。

评分标准

Sakiyary 人工评判,正确实现基础要素与其相关的具体要求即可得到 8080% 以上的分数。

使用图形界面并不会得到更高的分数 ,若图形界面不符合要求或 bug 太多,得分甚至会比命令行字符界面的实现更低。

剩余分数由相应的加分条件给出:

  • 基础功能十分完善,界面美观,动画流畅,没有 bug。(bug 太多会 倒扣
  • Sakiyary 认为你的代码结构非常合理,或是对 C 语言的掌握较好。
  • 实现 Rougelike 要素的相关功能与具体要求。
  • 模拟出原作的更多机制与游戏性。(不是贴图,是游戏机制)
  • 在不改变原有游戏性的前提下,实现超出原作的合理的扩展功能。
  • 对于命令行字符界面,实现流畅的动画、绘制更美观且比例适当的字符画则加分。

技术要求

编写项目文档(实验报告),须放在项目中容易找到的地方。

文档写得必须简洁,超过两页倒扣分数。

可以是任意格式,推荐 Markdown 格式,清晰即可,不必拘泥于排版(也不会加任何分)。

Markdown 可以在 课程资源-教程视频-Markdown 自行学习。

文档中请务必指出:

  • 平台(操作系统)
  • C 语言环境(mingw、msvc等及其详细的版本)
  • 编译选项(CMakeLists、MakeFile、编译指令等)
  • 使用的第三方库
  • 游戏方式(你设计的键位、操作等)

可以少许地介绍项目设计思路,以及你认为你写的特别好的地方。

同时,你也可以写一些感想与意见,如果你能写出充实的感想或有价值的意见,你将获得教学团队的认可!

提交方式

如果使用了第三方库,可以自行针对自己选择的库查阅如何生成release版本的可执行的文件(也就是打包发布的方法,而不是只是单纯在提交的压缩包里复制一个编译生成的可执行文件,这样的文件在使用第三方库的情况下多半是无法在另外一台电脑上直接运行的)并放在bin目录下;如果你没有使用第三方库(比如只是CLI界面),那么直接复制编译生成的可执行文件可能是可行的。注意要注明你的编译生成文件对应的平台,此项不作为强制要求,但是这样的操作可以让你把游戏分享给其他人进行游玩。

将源码的压缩包提交到课程系统中(打开代码编辑器->上传压缩文件),保持原始的目录结构,并在文档中说明如何你的源码如何编译生成对应的可执行文件和基本操作的方式

可以参考如下的结构

1
2
3
4
5
6
7
8
9
10
11
|--学号_姓名.zip
| |--src //(源码文件夹)
| | |--a.c
| | |--b.c
| | |--Makefile / CMakeLists.txt
| | |--……
| |--bin //(可执行文件文件夹,如果有)
| | |--win
| | | |--game_x64.exe
| | |--……
| |--实验报告_学号_姓名.pdf

滑雪冒险

题目描述

出题人:李薛成

验题人:不知道

项目概述

好耶,是滑雪大冒险!这款游戏的 BGM 应该耳熟能详吧,不熟也不要紧,我们直接来看 B 站大佬的魔改版👉 我做了个滑雪大冒险,但是纳西妲!


项目要求

你可以使用图形界面,也可以使用字符的命令行界面来运行游戏。(等一下,这道题真的能用字符界面吗?)

如果你选择使用图形界面,请仔细阅读课程网站上的图形界面要求

首先我们要明确你需要做一个什么样的游戏出来。显然,如果你能模仿原作实现它的绝大部分功能,拿到满分应该很大。但我们还是来解构一下这款游戏的要素:

基础要素

  • 一位可以操控的主角,主角只需要一种交互:跳跃。
  • 一个无限延伸山坡,作为地图。
  • 追赶主角的雪崩,随分数升高速度逐渐增大(小幅度)。
  • 主角的“装备”们(如雪橇、企鹅、雪怪、摩托、鹰等)
  • 坡道上的障碍物(石块),碰到障碍物时,若主角无装备,则摔倒;若有装备,则按一定的顺序掉一件装备。

到这里,游戏最最基本的解构就完成了。

难点

这个游戏其实有几个难点,如山坡的生成、雪崩跟着山坡的滑动、装备们的组合等。

  1. 山坡可以是固定的斜着的波浪线,也可以是随机参数的线性函数的平滑拼接。
  2. 雪崩甚至可以直接用一个长方形来模拟,意思到了就行,也可以将雪崩精细地实现为随山坡滑动的有宽度(高度?)的曲线。
  3. 装备的组合首先是视觉上的位置关系,其次就是你想将这个实现到哪种程度,粗糙一些也可以,十分精细也可以。

实现全看自己的想法,但是给分也会根据你实现各难点的方式与难度来评判。


在初步解构完要素的基础上,开始提出具体的要求(基础版):

  • 小恐龙🦖(dino)中大部分要求基本相同。
  • 地图中坡道的生成,曲线要有起伏,不能有断崖或太多的小疙瘩。(可以用固定模式生成)
  • 随分数增长不断加速的追赶主角的雪崩。(可以用简单的图形模拟)
  • 随机出现的障碍物。(如大大小小的石头)
  • “装备”们的运动,至少实现三种装备,且能以合理的不同的方式各自组合。
  • 合理的碰撞判定与穿装备/掉装备/摔倒机制。

评分标准

不知道谁人工评判,正确实现基础版要求(即用最简单的方式完成各个难点)即可得到 8080% 以上的分数。

(不好意思,这道题我真不知道字符界面怎么做……)

剩余分数由相应的加分条件给出:

  • 基础功能十分完善,界面美观,动画流畅,没有 bug。(bug 太多会 倒扣
  • Sakiyary 认为你的代码结构非常合理,或是对 C 语言的掌握较好。
  • 将上述三个难点用精细的方式实现。
  • 模拟出原作的更多机制与游戏性。(不是贴图,是游戏机制)
  • 在不改变原有游戏性的前提下,实现超出原作的合理的扩展功能。
  • 对于命令行字符界面,实现流畅的动画、绘制更美观且比例适当的字符画则加分。

技术要求

编写项目文档(实验报告),须放在项目中容易找到的地方。

文档写得必须简洁,超过两页倒扣分数。

可以是任意格式,推荐 Markdown 格式,清晰即可,不必拘泥于排版(也不会加任何分)。

Markdown 可以在 课程资源-教程视频-Markdown 自行学习。

文档中请务必指出:

  • 平台(操作系统)
  • C 语言环境(mingw、msvc等及其详细的版本)
  • 编译选项(CMakeLists、MakeFile、编译指令等)
  • 使用的第三方库
  • 游戏方式(你设计的键位、操作等)

可以少许地介绍项目设计思路,以及你认为你写的特别好的地方。

同时,你也可以写一些感想与意见,如果你能写出充实的感想或有价值的意见,你将获得教学团队的认可!


提交方式

如果使用了第三方库,可以自行针对自己选择的库查阅如何生成release版本的可执行的文件(也就是打包发布的方法,而不是只是单纯在提交的压缩包里复制一个编译生成的可执行文件,这样的文件在使用第三方库的情况下多半是无法在另外一台电脑上直接运行的)并放在bin目录下;如果你没有使用第三方库(比如只是CLI界面),那么直接复制编译生成的可执行文件可能是可行的。注意要注明你的编译生成文件对应的平台,此项不作为强制要求,但是这样的操作可以让你把游戏分享给其他人进行游玩。

将源码的压缩包提交到课程系统中(打开代码编辑器->上传压缩文件),保持原始的目录结构,并在文档中说明如何你的源码如何编译生成对应的可执行文件和基本操作的方式

可以参考如下的结构

1
2
3
4
5
6
7
8
9
10
11
|--学号_姓名.zip
| |--src //(源码文件夹)
| | |--a.c
| | |--b.c
| | |--Makefile / CMakeLists.txt
| | |--……
| |--bin //(可执行文件文件夹,如果有)
| | |--win
| | | |--game_x64.exe
| | |--……
| |--实验报告_学号_姓名.pdf

内存文件系统

题目描述

出好实验要求了。本题由 OJ 进行自动测试,OJ 最大的特点就是严格,不能蒙混过关了。

实验要求(简单版) 请查看群文件的最新版。部分测试用例也在群文件中。

推荐使用 linux,git,make。可以先尝试着提交,任何问题请提出。

Windows 上的提交不能用 make submit。你需要手动将你的目录下的 .git 文件夹压缩成 zip 手动在 OJ 那里上传。

按时提交即得 20 分诚信分。(与 gitm 一样)前言

这可能是大家第一次做类似的工作:你不是编写一个完整的程序,而是按照要求实现一些函数。这些函数将会被我们调用,以检测是否实现了所要求的功能。

本要求为(基本上是)最终版本。其中重要的描述修正将以这样的形式标出:

1
+++ 这里是修改过的描述,请注意检查 +++

简介

文件系统是操作系统的重要组成部分。调用文件系统 api,我们可以轻松地将数据持久化到磁盘上。C 语言中为我们提供了一组 api,它们基于操作系统 api,允许我们对文件进行操作:

1
2
3
4
5
FILE *fopen(const char *pathname, const char *mode);
void fclose(FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
...

在 Linux 操作系统中,上述的 C 文件操作 api 是基于一组操作系统的文件 api 实现的:

1
2
3
4
5
6
7
8
9
int open(const char *pathname, int flags);
int close(int fd);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
off_t lseek(int fd, off_t offset, int whence);
int mkdir(const char *pathname, mode_t mode);
int rmdir(const char *pathname);
int unlink(const char *pathname);
...

现要求你实现一个内存文件系统(ramfs)。顾名思义,这个文件系统的所有数据并不持久化到磁盘上,而是保存到内存当中,是一个易失性的文件管理系统。

文件系统的约定

基本性质

Ramfs 的目录结构与 Linux 的树形结构一致。在初始状态下,只存在根目录 “/“。文件系统中存在两类对象,目录与文件。目录下可以存放其他对象,而文件不可以。即在树形结构中,文件只能是叶子节点。

例 (#):

1
2
3
4
5
6
/
├── 1.txt "/1.txt"
├── 2.txt "/2.txt"
└── dir "/dir"
├── 1.txt "/dir/1.txt"
└── 2.txt "/dir/2.txt"

可以看到,在根目录下一共有 3 个项目:两个文件,一个目录 dir,而 dir 下还可以拥有两个文件。右侧的字符串称为对象的“绝对路径”。

单个文件和目录名长度 <= 32 字节,

修订1

+++ 是字母、数字、英文句点的任意组合。例如,’.’ 不是当前目录,’..’ 也不是上级目录 +++

对于存在不合法文件名的路径,你的文件系统 api 应当统一通过返回 -1 来拒绝此类操作。

所有 api 调用中,路径长度 <= 1024 字节。(也就是说,文件系统的路径深度是存在上限的)。

文件系统 api 统一使用绝对路径,即以 ‘/‘ 开头。在未创建任何文件时,就已经存在 “/“ 指向的根目录。该目录可打开,不可删除,其余性质与一般目录一致。

数据规模

整个文件系统同时存在的所有文件内容不会超过 512 MiB(不含已经删去的文件和数据),给予 1GiB 的内存限制。

同时存在的文件与目录不会超过 65536 个。

同时活跃着的文件描述符不会超过 4096 个。

对于所有数据点,文件操作读写的总字节数不会超过 10GiB。时限将给到一个非常可观的量级。

各数据点的性质:

  1. 如原始的 main.c

  2. 根目录下少量文件创建 + ropen + rwrite + rclose

  3. 在 2 的基础上,测试 O_APPEND,rseek

  4. 在 3 的基础上扩大规模

  5. 少量子目录创建(<= 5 层)+ 文件创建与随机读写

  6. 在 5 的基础上,测试 rrmdir, runlink。

  7. 大文件测试。多 fd 对少量大文件大量读写 + rseek + O_TRUNCATE

  8. 复杂的文件树结构测试。大量的 O_CREAT,rmkdir, rrmdir, runlink。少量读写

  9. 文件描述符管理测试。大量 ropen、rclose,多 fd 单文件

  10. 综合场景的大型测试。模拟真实的系统。

错误将会分散在各个数据点中。你需要保证你的 API 能正确地判断错误的情况并按照要求的返回值退出。

如果你获得 ”Wrong Answer“,说明仅仅是程序行为与 API 不一致。如读写的结果不正确,应该打开失败的文件却成功了…

如果获得 ”Runtime Error”,说明你的程序会出现运行错误而 crash。比如你在遍历文件树时,解引用了空指针…

接口简述

我们要求你实现如下的 api,以实现文件系统的管理。其具体行为将会在 api 说明部分阐释。

1
2
3
4
5
6
7
8
int ropen(const char *pathname, int flags);
int rclose(int fd);
ssize_t rwrite(int fd, const void *buf, size_t count);
ssize_t rread(int fd, void *buf, size_t count);
off_t rseek(int fd, off_t offset, int whence);
int rmkdir(const char *pathname);
int rrmdir(const char *pathname);
int runlink(const char *pathname);

注意,我们要求你实现的是内存操作系统。故你的程序应当使用内存管理 api(malloc、free)来存放文件所需的数据结构,以及文件的所有内容。请小心地管理好内存注意不要超限。

开始你的项目

我们为你准备了一个 git repo。请基于这个 git repo 进行你的项目。如果你不会 git,请学着使用。

在 git repo 中我们为你提供了一个自动编译脚本 Makefile。并且为你配置好了记录自动追踪。请不要随意修改 Makefile。你的修改记录将成为查重时证明独立完成的重要证据。

推荐在 Linux 操作系统中完成本作业。如果你要使用 Windows,产生的问题由你自己解决。

获取代码框架:

1
git clone "https://git.nju.edu.cn/Tilnel/ramfs.git"

注意:请在默认的 master 分支上进行开发。最终 OJ 的评分也将以你的 master 分支为准。

你应当在 ramfs.c 中包含你的所有实现(包括指定的函数和你使用的所有数据结构)。评测机会用我们自己的 Makefile(和分发版本一致)、ramfs.h(和分发版本一致)、main.c(包含更强力的测试用例)进行编译运行。因此你对 ranfs.h 和 main.c 以及 Makefile 的修改在 OJ 上不会产生效果。

提交:

1
make submit TOKEN=${你的token}

请在题目中“打开代码编辑器”后,获取你的提交 token。注意在校园网环境下提交。然后你就能在提交列表中看到你的提交。

由于服务器现可以通过 public.oj.cpl.icu 访问,你可以对 Makefile 中 submit 目标下的 url 进行修改:

1
2
3
4
5
6
7
@@ -25,5 +25,5 @@ submit:
@cd .. && zip -qr ${FILE} ${BASE}/.git
@echo "Created submission archive ${FILE}"
@curl -m 5 -w "\n" -X POST -F "TOKEN=${TOKEN}" -F "FILE=@${FILE}" \
- https://oj.cpl.icu/api/v2/submission/lab
+ http://public.oj.cpl.icu/api/v2/submission/lab
@rm -r ${TEMP}

注意在 make submit 之前,你需要将最新的改动 commit。同样注意保持你的工作目录整洁,如果你的 git repo 超过 20MiB(这一定是因为你放了很多很多奇怪的玩意),则没有办法提交。

你的 git repo 中不应当包含各种形式的编译产生的中间文件、编译结果。我们的 Makefile 只会在 build 目录下产生文件,我们也会配置好 .gitignore 文件避免 track 这些文件。

API 手册

修订2

+++ 你的实现不应当有任何输出 +++

以下注意区分两种对象的定义:文件(file),目录(directory)。

另一个重要的对象是:文件描述符(file descriptocr),简称 FD。它是所有打开的文件和目录的指示符,为一个非负整数。在 Windows 操作系统中称之为“句柄”。我们使用路径打开一个文件或目录,操作系统就会为这一次文件的打开分配一个文件描述符,它就像是一个“把手”一样。我们用这个文件描述符来指示打开的文件,进行对文件的操作。如:

1
2
int fd = open("/1.txt", O_RDONLY);  // open 返回一个文件描述符
read(fd, buf, 5); // 从打开的 fd (/1.txt) 中读取五个字节
1
int ropen(const char *pathname, int flags);
修订3

+++ 打开 ramfs 中的文件或目录。如果成功,返回一个文件描述符(一个非负整数),用于标识这个对象。+++

如果打开失败,则返回一个 -1。

pathname 为一个字符串,为一个绝对路径。对于所有存在的文件和目录,你的 ropen 调用都应当成功。特别地,在指示一个目录时,pathname 的末尾可以有多余的 ‘/‘。pathname 中间同样可以有冗余的 ‘/‘。

例如,在上文的例 (#) 中,以下的绝对路径是合法的:

1
2
3
4
//dir/        =/dir
////dir =/dir
/1.txt =/1.txt
//dir/1.txt =/dir/1.txt

以下的绝对路径是不存在的。

1
2
3
/3.txt
/1.txt/ (文件路径后不可以有多余的'/')
/di/r/1.txt (不存在这个路径)

flag 指示打开方式,这些打开方式仅对文件起作用。如果被打开的是目录则自动忽略。其取值有如下可能(或可以是它们的组合):

注意,在 C 中,以 0 开头的数字采用 8 进制表示法。

1
2
3
4
5
6
O_APPEND  02000 以追加模式打开文件。即打开后,文件描述符的偏移量指向文件的末尾。若无此标志,则指向文件的开头
O_CREAT 0100 如果 pathname 不存在,就创建这个文件,但如果这个目录中的父目录不存在,则创建失败;如果存在则正常打开
O_TRUNC 01000 如果 pathname 是一个存在的文件,并且同时以可写方式 (O_WRONLY/O_RDWR) 打开了文件,则文件内容被清空
O_RDONLY 00 以只读方式打开
O_WRONLY 01 以只写方式打开
O_RDWR 02 以可读可写方式打开

这些标志位的组合方式是使用按位的或运算。即:

1
2
3
O_TRUNC | O_RDWR   (可读可写,打开时清空)
+++ O_CREAT | O_WRONLY (若不存在,创建后以只写方式打开;否则以只写方式直接打开) +++ (原来写成读写了)
+++ O_APPEND (文件描述符的偏移量指向文件末尾,并可读) +++
修订8

+++ O_TRUNC 但文件以只读方式打开时,在 Linux 中为 unspecified 行为。此处约定为正常只读打开而不清空。 (1.15 聊天记录)+++

修订4

+++ 注意点:+++

+++ O_RDWR | O_WRONLY 共同存在时,取只写的语义; +++

+++ 由于 O_RDONLY 是 0,因此若未指定任何读写方式时,默认是只读的; +++

+++ 同时,易得 O_RDONLY | O_WRONLY == O_WRONLY。因此组合只读只写得到的结果是只写。 +++

1
int rclose(int fd);

关闭打开的文件描述符,并返回 0。如果不存在一个打开的 fd,则返回 -1。

1
ssize_t rwrite(int fd, const void *buf, size_t count);

向 fd 中的偏移量(马上解释)位置写入以 buf 开始的至多 count 字节,覆盖文件原有的数据。如果 count 超过 buf 的大小,仍继续写入(数据保证不因此而产生段错误),将 fd 的偏移量后移 count,并返回实际成功写入的字节数。如果写入的位置超过了原来的文件末尾,则自动为该文件扩容。

如果 fd 不是一个可写的文件描述符,或 fd 指向的是一个目录,则返回 -1。

在本实验中,ramfs 中同时存在的文件大小不会超过限制。因此你的 rwrite 对于一个能够写入的文件,事实上总应返回 count。

1
ssize_t rread(int fd, void *buf, size_t count);

从 fd 中的偏移量位置读出至多 count 字节到 buf 指向的内存空间当中,

修订5

+++ 将偏移量后移实际读出的字节数,并返回实际读出的字节数。+++

因为可能会读到文件末尾,因此返回值有可能小于 count。

如果 fd 不是一个可读的文件描述符,或 fd 指向的是一个目录,则返回 -1。

偏移量(offset)

想象你用手指指着读一本书,offset 相当于你手指指向的位置。你每读一个字,手指就向前前进一个字;如果你想改写书本上的字,每改写一个字,手指也向前前进一个字。

每一个文件描述符都拥有一个偏移量,用来指示读和写操作的开始位置。这个偏移量对应的是文件描述符,而不是“文件”对象。举个例子:

1
2
3
4
5
char buf[6];
int fd1 = open("/1.txt", O_WRONLY | O_CREAT);
int fd2 = open("/1.txt", O_RDONLY);
write(fd1, "helloworld", 11);
read(fd2, buf, 6);
修订6

--- 此时 buf 中将从文件的开头读到”hello\0”。但如果换一种方式: ---

+++ 此时 buf 中将从文件的开头读到”hellow”。但如果换一种方式: +++

假设 “/1.txt” 中原来有数据 “helloworld\0”

1
2
3
4
char buf[6];
int fd = open("/1.txt", O_RDWR);
write(fd, "hello", 5);
read(fd, buf, 6);

此时,write 在读取时,将文件指针前移了 5 个字节。于是read在读取的时候,将会从第6个字节开始读取。也即,read 将会读到 “world\0”。对于同一个文件描述符,读取和写入操作是共享偏移量的;对于不同的文件描述符,它们的偏移量则是各自独立的。

对于 open 操作,如果没有 O_APPEND 标志来将偏移量指向末尾,那么默认指向文件开头。

如何自由地修改和获取文件描述符的偏移量呢?

1
off_t rseek(int fd, off_t offset, int whence);

这个函数用于修改 fd 表示的文件描述符的偏移量,并返回当前文件的实际偏移量。

whence有三种取值:

1
2
3
SEEK_SET 0   将文件描述符的偏移量设置到 offset 指向的位置
SEEK_CUR 1 将文件描述符的偏移量设置到 当前位置 + offset 字节的位置
SEEK_END 2 将文件描述符的偏移量设置到 文件末尾 + offset 字节的位置

rseek 允许将偏移量设置到文件末尾之后的位置,但是并不会改变文件的大小,直到它在这个位置写入了数据。在 超过文件末尾的地方写入了数据后,原来的文件末尾到实际写入位置之间可能出现一个空隙,我们规定应当以 “\0” 填充这段空间。

+++ 但不允许将偏移量设置到文件开头之前,也就是一个负数的绝对偏移量。这种情况下返回 -1。 +++

1
int rmkdir(const char *pathname);

创建目录,成功则返回 0。如果目录的父目录不存在或此路径已经存在,则失败返回 -1。

如,原来系统中只存在根目录 “/“,调用:rmkdir("/path/to/dir") 返回 -1。

1
int rrmdir(const char *pathname);

删除一个空目录,成功则返回 0。如果目录不存在或不为空,或 pathname 指向的不是目录,返回 -1。测试保证不对打开的 pathname 做 rrmdir。

1
int runlink(const char *pathname);

删除一个文件,成功则返回 0。如果文件不存在或 pathname 指向的不是文件,则返回 -1。测试保证不对打开的 pathname 做 runlink。

额外的一个 api:

1
void init_ramfs();

可以用于初始化你的文件系统。比如创建根目录。我们用于测试的 main() 将总会包含它。(要在里面做什么取决于你自己!)

我们的测试用例长什么样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* our main.c */
#include "ramfs.h"
#include <assert.h>
#include <string.h>

int main() {
init_ramfs(); // 你的初始化操作
assert(rmkdir("/dir") == 0); // 应当成功
assert(rmkdir("//dir") == -1); // 应当给出 error,因为目录已存在
assert(rmkdir("/a/b") == -1); // 应当给出 error,因为父目录不存在
int fd;
assert((fd = ropen("//dir///////1.txt", O_CREAT | O_RDWR)) > 0); // 创建文件应当成功
assert(rwrite(fd, "hello", 5) == 5); // 应当完整地写入
assert(rseek(fd, 0, SEEK_CUR) == 5); // 当前 fd 的偏移量应该为 5
assert(rseek(fd, 0, SEEK_SET) == 0); // 应当成功将 fd 的偏移量复位到文件开头
char buf[10];
assert(rread(fd, buf, 7) == 5); // 只能读到 5 字节,因为文件只有 5 字节
assert(memcmp(buf, "hello", 5) == 0); // rread 应当确实读到 "hello" 5 个字节
assert(rseek(fd, 3, SEEK_END) == 8); // 文件大小为 5,向后 3 字节则是在第 8 字节
assert(rwrite(fd, "world", 5) == 5); // 再写 5 字节
assert(rseek(fd, 5, SEEK_SET) == 5); // 将偏移量重设到 5 字节
assert(rread(fd, buf, 8) == 8); // 在第 8 字节后写入了 5 字节,文件大小 13 字节;那么从第 5 字节后应当能成功读到 8 字节
assert(memcmp(buf, "\0\0\0world", 8) == 0); // 3 字节的空隙应当默认填 0
assert(rclose(fd) == 0); // 关闭打开的文件应当成功
assert(rclose(fd + 1) == -1); //关闭未打开的文件应当失败
return 0;
}
修订7

+++ 我们将会在这份手册的最后,提供几份测试代码供大家参考。大家可以将这些代码放到你的 main.c 中,并使用 make run 进行测试。+++

实现指南

首先是目录树。这里给出一个参考的文件对象结构体:

1
2
3
4
5
6
7
8
typedef struct node {
enum { FILE_NODE, DIR_NODE } type;
struct node *dirents; // if it's a dir, there's subentries
void *content; // if it's a file, there's data content
int nrde; // number of subentries for dir
int size; // size of file
char *name; // it's short name
} node;

目录的子项的数量会变化;文件的内容大小也会变化。因次我们可能需要对 direntscontent 的内存大小进行动态的改变。

1
void *realloc(void *ptr, size_t size);

这一函数会创建一段新的空间,将原来的内容复制到新的空间上,并释放原来的指针。注意原有指针一定也是动态分配的。

其次是文件描述符。对于文件描述符来说,其重要的只有这几个属性:读写性质,偏移量,指向的实际文件。

1
2
3
4
5
typedef struct FD {
int offset;
int flags;
node *f;
} FD;

最初的根文件可以直接定义成全局变量:

1
node root;

然后在 init_ramfs 中进行初始化。

接下来的事情,就很显然了:

添加文件和目录,就是往树里添加节点;

删除文件,就是删除节点;

读取内容,就是从 content 里复制出一段…

一个小建议:使用 memcpy 而不是 strcpy。(区别在哪?读手册)

测试代码

第一个数据点已经给出。

test2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
int notin(int fd, int *fds, int n) {
for (int i = 0; i < n; i++) {
if (fds[i] == fd) return 0;
}
return 1;
}

int genfd(int *fds, int n) {
for (int i = 0; i < 4096; i++) {
if (notin(i, fds, n))
return i;
}
return -1;
}

int test2() {
init_ramfs();
int fd[10];
int buf[10];
assert(ropen("/abc==d", O_CREAT) == -1);
assert((fd[0] = ropen("/0", O_RDONLY)) == -1);
assert((fd[0] = ropen("/0", O_CREAT | O_WRONLY)) >= 0);
assert((fd[1] = ropen("/1", O_CREAT | O_WRONLY)) >= 0);
assert((fd[2] = ropen("/2", O_CREAT | O_WRONLY)) >= 0);
assert((fd[3] = ropen("/3", O_CREAT | O_WRONLY)) >= 0);
assert(rread(fd[0], buf, 1) == -1);
assert(rread(fd[1], buf, 1) == -1);
assert(rread(fd[2], buf, 1) == -1);
assert(rread(fd[3], buf, 1) == -1);
for (int i = 0; i < 100; i++) {
assert(rwrite(fd[0], "\0\0\0\0\0", 5) == 5);
assert(rwrite(fd[1], "hello", 5) == 5);
assert(rwrite(fd[2], "world", 5) == 5);
assert(rwrite(fd[3], "\x001\x002\x003\x0fe\x0ff", 5) == 5);
}
assert(rclose(fd[0]) == 0);
assert(rclose(fd[1]) == 0);
assert(rclose(fd[2]) == 0);
assert(rclose(fd[3]) == 0);

assert(rclose(genfd(fd, 4)) == -1);

assert((fd[0] = ropen("/0", O_CREAT | O_RDONLY)) >= 0);
assert((fd[1] = ropen("/1", O_CREAT | O_RDONLY)) >= 0);
assert((fd[2] = ropen("/2", O_CREAT | O_RDONLY)) >= 0);
assert((fd[3] = ropen("/3", O_CREAT | O_RDONLY)) >= 0);
assert(rwrite(fd[0], buf, 1) == -1);
assert(rwrite(fd[1], buf, 1) == -1);
assert(rwrite(fd[2], buf, 1) == -1);
assert(rwrite(fd[3], buf, 1) == -1);
for (int i = 0; i < 50; i++) {
assert(rread(fd[0], buf, 10) == 10);
assert(memcmp(buf, "\0\0\0\0\0\0\0\0\0\0", 10) == 0);
assert(rread(fd[1], buf, 10) == 10);
assert(memcmp(buf, "hellohello", 10) == 0);
assert(rread(fd[2], buf, 10) == 10);
assert(memcmp(buf, "worldworld", 10) == 0);
assert(rread(fd[3], buf, 10) == 10);
assert(memcmp(buf, "\x001\x002\x003\x0fe\x0ff\x001\x002\x003\x0fe\x0ff", 10) == 0);
}
assert(rread(fd[0], buf, 10) == 0);
assert(rread(fd[1], buf, 10) == 0);
assert(rread(fd[2], buf, 10) == 0);
assert(rread(fd[3], buf, 10) == 0);
assert(rclose(fd[0]) == 0);
assert(rclose(fd[1]) == 0);
assert(rclose(fd[2]) == 0);
assert(rclose(fd[3]) == 0);
return 0;
}

其他的请再等等吧(

2023.1.14 补:

上线了测试点 4,在 main() 的开头做了一件事:

1
rread(-100000000, buf, 10);

所有人都炸掉了。但按照手册,它应该返回 -1。这只是测试中不合理数据的一角,请大家保证自己的实现的可靠性。

如果你对某些特例会产生什么行为抱有疑问,欢迎提问。

修订9

+++ 再次强调请小心地管理内存,否则内存容易超限,特别是注意释放掉已经不用的空间。

+++ OJ 评测结果解释:

+++ 答案错误:你的函数行为与规定的行为不一致。可能是文件系统中的内容不一致,也可能是函数返回值不符合约定。

+++ 运行错误:你的函数在内部崩溃了。

版本控制系统

题目描述

实验要求 请看群文件最新版。

项目框架将发布在 https://git.nju.edu.cn/Tilnel/gitm.git

目前题目只有一个样例,直接交就是一伯分。但我出好数据之后会重新评测。

有一个送分样例。正确按时提交保底 20 分。

Windows 上的提交不能用 make submit。你需要手动将你的目录下的 .git 文件夹压缩成 zip 手动在 OJ 那里上传。

前言

git 是当今世界上最流行的版本控制系统。本实验要求你通过提交 git repo 的方式来提交一个迷你版的命令行工具 gitm(inus)。

注意:本实验将只能在 Linux 操作系统中完成,因为你不得不使用系统调用,而 OJ 是 Linux 的。提交 Windows 上可以编译运行的代码,在评测机上注定不可兼容。

首先,你需要学习 git ,否则你将完全不明白 gitm 的功能,并且也无法用 git 来管理本次作业的代码。

是的!我们将会发布一个由 git 管理的框架代码,并且要求你一直使用 git 来管理,最终提交一个 git repo。

获取框架代码:

1
git clone https://git.nju.edu.cn/Tilnel/gitm.git

提交要求

所有代码,包括 .c 和 .h 文件需要放在 git repo 的根目录下。对自己使用的头文件的引用请以双引号的形式,以便编译脚本能够正常工作。

例如:

1
2
3
4
5
6
7
8
9
10
11
gitm
├── gitm.c
├── gitm.h
├── whateveryouwant.c
├── whateveryouwant.h
├── ...
└── Makefile

/* gitm.c */
#include "gitm.h"
...

你可以修改 Makefile,但请不要删除其中的 git 目标依赖。我们在 Makefile 中确保了你的每一次编译运行都能够自动进行 git commit。这些自动的 commit 可以帮助你回滚到自己想要的任意版本,并且在未来查重工作中产生疑问时,良好的 commit 记录将成为重要的证明。

在你的 git repo 里请包含所有编译所需的源文件,但不要出现编译不需要的多余的源文件。

尝试编译:

1
make

你将看到根目录下产生了一个名为 gitm 的可执行文件。

1
./gitm version

你将看到一个小彩蛋(你之后可以自由地删掉它或修改掉,不影响成绩)。

注意:本次实验你编写的是一个 “命令行工具”。也就是说,我们将以和使用 git 相同的方式来使用它:在命令行里输入命令和参数。这意味着,这次你需要真正 “解析参数” (被 parse.c 支配的恐惧)。

而且,这次我们将会在运行中多次调用你的程序。也就是说,你的程序并不是在一直运行着,每一次调用都会做不同的事。你存储在内存里的数据都将随着功能完成,进程结束而消失。所以,关于 gitm repository 的有用的信息,你需要将它们持久化到磁盘上,以便进行后续的操作。因此学习 C 语言的文件操作是必不可少的。

为了实现一个 git,首先你要了解 git 的功能

实现功能

假设我们当前在一个文件夹 dir 下

1
gitm init

初始化当前的 dir 为一个 gitm repository。如果当前 dir 已经是一个 gitm repo,则不做任何操作。

此时的 gitm 中应当不存在任何 commit,gitm 的仓库中应不存在任何文件。

具体来说,你可以在当前目录下创建一个 .gitm 目录,用于存放一些记录仓库状态的文件。

对,就像 git 的 .git 那样!

1
gitm commit

将当前仓库中文件改动后状态作为一个提交,并记录下来。然后不重复地给出一个长度为 8 的小写十六进制数(例如 3bdc8902),用于唯一地指示这一次 commit。

git 中的提交是一个树形的结构。我们希望你在 gitm 中,同样实现这样的树形结构。

img

gitm 中不要求实现对分支的命名

1
gitm checkout commit

checkout 用于将当前目录的状态切换到 commit 所指示的提交上。

若当前目录的状态较 gitm 当前所处的 commit 有改动,则拒绝本次 checkout,并且你的 main() 函数以返回值 1 退出

checkout 正常完成后,你目录中文件的状态(除了 .gitm 目录以外)必须与指定的 commit 相同。

1
gitm checkout .

特殊地,这一条命令用于将目录文件恢复到当前所处的 commit 时的状态。也就是说,放弃此时对文件的所有改动。

1
gitm merge commit

找到当前所处 commit 与命令指定的 commit 的公共祖先,并将两个 commit 合并起来。

具体来说,是将命令指定的 commit 相对于公共祖先的修改,应用于当前所处的 commit。

如果合并的两个 commit 相对于公共祖先,均对同一个文件产生了修改(创建、删除、编辑),那么命令直接拒绝执行,输出 “conflict\n” 并使 main 函数返回 1

在其他情况下,你需要合并,并产生一个新的 commit。逻辑上,这个 commit 将成为被合并的两个 commit 的共同后继。

我们如何检测这一点?

假设有 commit a-g,b, c 由 a 分支而来,d 由 b, c 合并而来,e 是 b 的后继,f 是 c 的后继,g 是 d 的后继。

你的程序应当有能力找到 e, g 的公共祖先是 b,f, g 公共祖先是 c,在此基础上合并是无冲突的。如果你只能找到 a,则合并有可能产生冲突,因为 e 相对 a 改变了 a.c,而 g 相对 a 也改变了 a.c。

image-20230103113625830

测试脚本

我们会将你的 repo 里所有的 C 源文件和头文件收集起来进行编译,并生成一个名为 gitm 的可执行文件。然后原地创建一个文件夹,作为你的 gitm 需要管理的 repository。例如(其中 > 开头的行表示命令行输出):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mkdir dir
cd dir
../gitm init
../gitm commit
> 3bce5ff0 # 空 commit,我们的 OJ 一定会创建一个空 commit 作为第一个
echo "hello world" > hello.txt # 创建文件并写入
../gitm commit
> b926d817
echo "this is my git" > readme.txt
../gitm checkout 3bce5ff0
> You've made change. Please commit or garbage your change.
echo $? # 给出上一条命令的返回值。正常退出的程序应当为 0
> 1
../gitm commit
> ef938aa6
ls -a
> hello.txt readme.txt .gitm
../gitm checkout b926d817
ls -a
> hello.txt .gitm

随着时间的流逝,我将会发布进一步的实验指南。

实现要求

  • 你创建的所有文件都要放到运行目录的 .gitm 目录下。
    • +++ 对目录的体积要求:不要每一次都追踪没有发生变化的文件即可。不需要对每个文件进行增量存储。 +++
  • commit 数量不会超过 10000 个。
  • 你的 gitm 只需要管理文本类型的文件。其他类型的文件不会出现。
  • 不要求追踪空目录

本实验的测试点预计如下:

1、hello world(保留 gitm version 的打印信息即得分)

2、和上面的脚本相似的一段小测试,基础功能

3、文件数量增加,提交数量增加;但并不会出现子目录

4、在 3 的基础上,有一定的目录结构

5、在 4 的基础上,测试 merge 功能(不会很刁钻,只要该拒绝的拒绝,该成功的 merge 对就行了)

6、测试 .gitm 的空间管理,要求未发生改动的文件不重复存储,不要求单文件的增量存储

能够恢复对文件就可以了,不会太刁钻。

实现指南

一个更加 naïve 的思路。从一个 commit 刚刚被提交说起…

此时,所有的目录结构和文件改动都被提交了,我们可以在当下的目录中进行新的改动。为了能够恢复到刚刚提交的“干净”状态,我们需要为当下的状态做一个暂存,以便之后进行对比。

现在我们做了一些改动,想要 commit。这里,需要记录下改动的部分,没有改动的部分则默认是保持的。我们可以用文件系统的 api 遍历当前目录和暂存下来的目录,检测文件的增删等。对于依然存在的文件,则需要逐字符对比其中的改动。当所有的改动全部检测完毕后,在 .git 下保存好本次改动中:

  • 删除了哪些文件
  • 增加了哪些文件和这些文件的内容
  • 编辑了哪些文件和这些文件的新版本

并将本次 commit 及其父节点 commit 号记录下来。

可以使用的一些函数:

1
2
3
4
5
6
7
8
9
10
11
#include <dirent.h>
#include <sys/types.h>
DIR *opendir(const char *name); // 打开目录
struct dirent *readdir(DIR *dirp); // 读取目录中的项目
int closedir(DIR *dirp);

#include <sys/stat.h>
int mkdir(const char *path, mode_t mode); // 创建新目录

#include <unistd.h>
int rmdir(const char *pathname); // 删除目录

如何更简单地判断新文件是否发生改动,特别是较大文件?

可以对所有存储下来的文件做 md5sum,为文件生成一个摘要。之后再有文件变动时,先去找是否存在相同的 md5,如果新旧文件的 md5 相同,就不用重复保存了。

如何调用命令:

1
2
3
#include <stdio.h>
FILE *popen(const char *command, const char *type); // 执行命令,读取它的输出
int pclose(FILE *stream);

废弃的旧思路(依然可以尝试)

一个 naïve 的思路。

首先介绍两个工具,一个叫做 diff,一个叫做 patch。这两个工具是大部分发行版自带的。看到这里请打开你的命令行一起尝试一下。

准备任意一个代码文件 a.c,复制到 b.c,在 b.c 中加入一些行,删去一些行,

1
diff a.c b.c

能看到:

img

可知,diff 可以计算出两个文本文件之间的差距。

用重定向将这一结果定向到 diff.out 中,我们再用 patch:

1
2
patch a.c diff.out
diff a.c b.c

这次什么也没有输出。a.c 和 b.c 变成一样的了。

diff 不仅可以给 a.c 打补丁,还可以把补丁从文件中拆下来:

1
patch -r b.c diff.out

再打开 b.c,就能发现文件变回了之前 a.c 的样子。

great。所以只要你能够在 commit 的时候遍历目录中的所有文件,挨个 diff 一下,就可以算出当前版本和上个版本的差距了。然后你把这些差距全都写 .gitm 中的某一个文件,大功告成。

diff 和 patch 可以对两个目录直接计算差值和补丁/回退。如果你学会怎么用,省去很多麻烦事。

如何调用命令:

1
2
3
#include <stdio.h>
FILE *popen(const char *command, const char *type); // 执行命令,读取它的输出
int pclose(FILE *stream);

总结

总结:在 .gitm 中我们需要存储的元素

  • 一个文件,能指示当前所处的提交
  • 一个文件,包含了所有的提交号,并需要能够指明每个提交在树结构上的祖先节点。对于新 commit 是一个,对于 merge 是两个(这与 git 并不完全相同)
  • 若干个目录,每个代表一个 commit;每个目录若干个文件,记录它相对祖先节点的变化:增减文件目录,编辑文件
  • 一个目录,包含了当前 commit 的暂存状态,以便之后用于与编辑后的状态进行比较

在 gitm 执行的过程中,有这样一些子功能需要实现:

  • 解析参数,执行对应功能
  • 遍历当前文件树,与已提交的文件树对比
  • 将对比的文件树中公共部分进行比较
  • 分析 commit 记录,确定提交之间的关系
  • 通过提交之间关系,决定前进或后退