游戏的所有对象都由一个或多个特定字符来表示
##############
# #
# dv #
# v # #
# v # #
# s >>D # #
# v # #
# *A< * # #
# #
##############
main() {
Read board from file or stdin or create a default board
update game
write updated board to file or stdout
}
typedef struct snake_t {
unsigned int tail_row;
unsigned int tail_col;
unsigned int head_row;
unsigned int head_col;
bool live;
} snake_t;
typedef struct game_t {
unsigned int num_rows;
char **board;
unsigned int num_snakes;
snake_t *snakes;
} game_t;
update_game()
board file预设了所有对象的位置:蛇,水果,墙
在加载board之后,一时刻的游戏运行逻辑:
蛇的移动实际上只需要更新头部和尾部的位置,即
update_head()
, update_tail()
两个函数根据下一刻的位置是空地,墙还是水果做不同的更新
而这两个函数的实现有许多helper完成,大概来说有:
body_to_tail()
, head_to_body()
update_tail()
需要incident
这一信息(对正常移动,撞墙,吃到水果编码),但为了让代码逻辑更精简,不再做与update_head()
相同的检查逻辑,而是总是先执行update_head()
,其将incident
返回,再用if语句控制load_board()
一个问题是在不知道文件中的board有多大的情况下,而且在C中无法通过不读取来获取board的大小,因为FILE
object type不包含也不蕴含这一信息
Object type that identifies a stream and contains the information needed to control it, including a pointer to its buffer, its position indicator and all its state indicators.
要向game->board
分配多大的内存呢?
答案是动态分配,与一个动态数组类似,当读取的行数超过分配的大小时,原来的大小翻倍,通过realloc()
重新分配内存。game->board[r]
的内存分配也是如此,和其他语句结合,封装在另一个函数read_line()
中,通过fgets()
copy string from stream,整体逻辑是
fgets()
之后检查读取的字符串最后一个字符是否为\n
fgets()
被截断,大小翻倍,line
重新分配内存line
指针不移动,通过pointer arithmetic来计算从哪里开始读取新的num
个字符
heuristically, 有两个变量size
: 分配的内存大小, len
: 实际大小。由此来计算fgets()
的参数str
, num
自动附加的\0
需要被覆写,所以读取的num
有+1,len
因为不包含\0
,所以作为offset,值刚刚好
char *read_line(FILE *fp) {
char *line = malloc(256);
int size = 256;
int len = 0;
while ((fgets(line + len, size - len + 1, fp)) != NULL) {
// check if fgets() is truncated
len = strlen(line);
if (line[len-1] != '\n' && !feof(fp)) {
size *= 2;
line = realloc(line, (size_t) size);
continue;
}
return line;
}
free(line);
return NULL;
}
snake
的初始化是与load_board
分离的,实现很简单
board
找到tail位置snake
的各个field
到此为止cs61c/proj1 的所有工作已完成,因为不是从头写起的,骨架都搭好了,难度不大,作为C语言初学者的练习再合适不过了。