cs61c-project1-snek

简单的c语言贪吃蛇游戏

Posted by 渚汐 on October 21, 2024

免责声明

请不要抄袭代码,否则你的代码能力得不到任何锻炼!

projec课程主页

Setup

请参考cs61c课程官方网站lab0自行设置,伯克利校外的不需要搭建虚拟环境即可完成项目,课程提供的本地测评足够使用了

按照如下步骤进行:

1
2
3
4
5
mkdir pro1
cd pro1
git init
git remote add starter https:/github.com/61c-teach/fa24-proj1-starter.git
git pull starter main

接下来就可以工作了,博主使用的编程配置是wsl+linux,省去了很多麻烦,具体食用方法请直接搜索微软wsl官方教程

总体概念介绍

本部分主要介绍了snek的相关概念

Snake

游戏有一个playboard:

##############
#            #
#    dv      #
#     v   #  #
#     v   #  #
#   s >>D #  #
#   v     #  #
# *A<  *  #  #
#            #
##############

各种元素的代表含义如下:

  • # 墙
  • 空格 空间
  • * 果子
  • wasd 蛇尾,并代表方向
  • ^<v> 蛇身,并代表方向
  • WASD 蛇头,并代表方向
  • x 代表蛇头死亡

游戏规则:

  1. 每次蛇朝头方向移动一格
  2. 如果蛇头碰到身体或者墙壁,蛇头被x替换
  3. 如果吃到果子,蛇头前进,蛇尾位置保持不变

Numbering snakes

蛇在palyboard上的编号是以蛇尾为标志,按照从左到右从上到下的顺序进行排序的

Game board

由一组字符组成,形状不规则,但由#字符进行封闭

主要结构体

1
2
game_state_t
snake_t

具体请参考课程主页

编译,测试,调试

请使用课程提供的makefile进行以上工作,不要自行使用gcc

包含两个可执行文件:

  1. unit-test
  2. snake

unit-test用于测试task1-6 snake:包含完整游戏的文件,集成在task7中

尽情的使用gdb进行调试吧!(什么,你不会gdb?请参考”c debugging”)

Task1 crate_default_state

创建默认state,需要自己创建一个由18行,每行20列的playboard 果子:2×9 尾巴:2×2 头:2×4

注意事项:

  1. 需要在heap中分配内存,使用malloc
  2. 使用strcpy简化工作(不用也行)

代码如下:

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
game_state_t *create_default_state()
{

  // board definition
  char *top_bottom = "####################";
  char *normal_row = "#                  #";
  char *snake_and_fruit = "# d>D    *         #";
  char **default_board = malloc(sizeof(char *) * 18); // 18 lines each line is a char*
  // initial board for use
  int i;
  for (i = 0; i < 18; i++)
  {
    default_board[i] = malloc(21); // 21 bytes including '\0'
    if (i == 0 || i == 17)
      strcpy(default_board[i], top_bottom);
    else if (i == 2)
      strcpy(default_board[i], snake_and_fruit);
    else
      strcpy(default_board[i], normal_row);
  }
  // initial state board and nums
  game_state_t *state = malloc(sizeof(game_state_t));
  state->board = default_board;
  state->num_rows = 18;
  state->num_snakes = 1;
  // initial state's snake
  state->snakes = malloc(sizeof(snake_t) * state->num_snakes); // first allocate heap memory
  state->snakes->tail_row = 2;
  state->snakes->tail_col = 2;
  state->snakes->head_row = 2;
  state->snakes->head_col = 4;
  state->snakes->live = true;
  return state;
}

Task 2 free_state

记得把所有分配的内存都释放就好了

1
2
3
4
5
6
7
8
9
10
11
void free_state(game_state_t *state)
{

  free(state->snakes);
  unsigned int rows = state->num_rows;
  for (int i = 0; i < rows; i++)
    free(state->board[i]);
  free(state->board);
  free(state);
  return;
}

Task 3 print_board

这个任务后面在调试中用到,十分方便,请务必完整实现! 内容很简单,把board打印出来即可,因为设计到文件操作,所以要用到fprint函数

1
2
3
4
5
6
7
8
9
10
11
void print_board(game_state_t *state, FILE *fp)
{

  char **print_board = state->board;
  unsigned int rows = state->num_rows;
  for (int i = 0; i < rows; i++)
  {
    fprintf(fp, "%s\n", print_board[i]);
  }
  return;
}

Task 4 update_state

4.1 helper functions

主要实现一些简单的功能,进行简单的逻辑判断,后期用得上,有一种从简单到复杂的感觉

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/* Task 4.1 */

/*
  Helper function to get a character from the board
  (already implemented for you).
*/
char get_board_at(game_state_t *state, unsigned int row, unsigned int col) { return state->board[row][col]; }

/*
  Helper function to set a character on the board
  (already implemented for you).
*/
static void set_board_at(game_state_t *state, unsigned int row, unsigned int col, char ch)
{
  state->board[row][col] = ch;
}

/*
  Returns true if c is part of the snake's tail.
  The snake consists of these characters: "wasd"
  Returns false otherwise.
*/
static bool is_tail(char c)
{
  if (c == 'w' || c == 'a' || c == 's' || c == 'd')
    return true;
  else
    return false;
}

/*
  Returns true if c is part of the snake's head.
  The snake consists of these characters: "WASDx"
  Returns false otherwise.
*/
static bool is_head(char c)
{
  if (c == 'W' || c == 'A' || c == 'S' || c == 'D' || c == 'x')
    return true;
  else
    return false;
}

/*
  Returns true if c is part of the snake.
  The snake consists of these characters: "wasd^<v>WASDx"
*/
static bool is_snake(char c)
{
  if (is_tail(c) || is_head(c) || c == '^' || c == '<' || c == 'v' || c == '>')
    return true;
  else
    return false;
}

/*
  Converts a character in the snake's body ("^<v>")
  to the matching character representing the snake's
  tail ("wasd").
*/
static char body_to_tail(char c)
{

  if (c == '^')
    return 'w';
  else if (c == '>')
    return 'd';
  else if (c == '<')
    return 'a';
  else if (c == 'v')
    return 's';
  else
    return '?';
}

/*
  Converts a character in the snake's head ("WASD")
  to the matching character representing the snake's
  body ("^<v>").
*/
static char head_to_body(char c)
{
  if (c == 'W')
    return '^';
  else if (c == 'A')
    return '<';
  else if (c == 'S')
    return 'v';
  else if (c == 'D')
    return '>';
  else
    return '?';
}

/*
  Returns cur_row + 1 if c is 'v' or 's' or 'S'.
  Returns cur_row - 1 if c is '^' or 'w' or 'W'.
  Returns cur_row otherwise.
*/
static unsigned int get_next_row(unsigned int cur_row, char c)
{

  if (c == 'v' || c == 's' || c == 'S')
  {
    if (cur_row == UINT32_MAX)
    {
      printf("Current row:%u.Input char:%c.Already the lowest!\n", cur_row, c);
      return cur_row;
    }
    return cur_row + 1;
  }

  else if (c == '^' || c == 'w' || c == 'W')
  {
    if (cur_row == 0)
    {
      printf("Current row:%u.Input char:%c.Already the uppest!\n", cur_row, c);
      return cur_row;
    }
    return cur_row - 1;
  }

  else
    return cur_row;
}

/*
  Returns cur_col + 1 if c is '>' or 'd' or 'D'.
  Returns cur_col - 1 if c is '<' or 'a' or 'A'.
  Returns cur_col otherwise.
*/
static unsigned int get_next_col(unsigned int cur_col, char c)
{
  if (c == '>' || c == 'd' || c == 'D')
  {
    if (cur_col == UINT32_MAX)
    {
      printf("Current col:%u.Input char:%c.Already the rightest!\n", cur_col, c);
      return cur_col;
    }
    return cur_col + 1;
  }

  else if (c == '<' || c == 'a' || c == 'A')
  {
    if (cur_col == 0)
    {
      printf("Current col:%u.Input char:%c.Already the leftest!\n", cur_col, c);
      return cur_col;
    }
    return cur_col - 1;
  }

  else
    return cur_col;
}

这部分函数并不在自动评测范围内,请自己在custom_tests.c中编写测试函数,这里不做展示

4.2 next_square

函数返回指定编号的蛇的头部要移动到的下一个方块内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static char next_square(game_state_t *state, unsigned int snum)
{
  if (snum + 1 > state->num_snakes)
    return '?';

  if (!state)
    return '?';

  unsigned int head_row = state->snakes[snum].head_row;
  unsigned int head_col = state->snakes[snum].head_col;

  char head = state->board[head_row][head_col];
  head_row = get_next_row(head_row, head);
  head_col = get_next_col(head_col, head);

  return state->board[head_row][head_col];
}

4.3 update_head

根据上面实现的函数更新playboard的状态即可

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
static void update_head(game_state_t *state, unsigned int snum)
{
  if (snum + 1 > state->num_snakes)
    return;
  if (!state)
    return;

  unsigned int head_row = state->snakes[snum].head_row; // get row
  unsigned int head_col = state->snakes[snum].head_col; // get col

  char head = state->board[head_row][head_col]; // store head

  state->board[head_row][head_col] = head_to_body(state->board[head_row][head_col]); // head to body

  // get next row and col
  head_row = get_next_row(head_row, head);
  head_col = get_next_col(head_col, head);

  // change next cell to current head
  state->board[head_row][head_col] = head;
  // store in snake_t
  state->snakes->head_row = head_row;
  state->snakes->head_col = head_col;

  return;
}

4.4 update_tail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void update_tail(game_state_t *state, unsigned int snum)
{
  if (!state || (snum + 1 > state->num_snakes))
    return;

  unsigned tail_row = state->snakes[snum].tail_row;
  unsigned tail_col = state->snakes[snum].tail_col;

  char tail = state->board[tail_row][tail_col];

  state->board[tail_row][tail_col] = ' ';

  tail_row = get_next_row(tail_row, tail);
  tail_col = get_next_col(tail_col, tail);
  char body = state->board[tail_row][tail_col];
  state->board[tail_row][tail_col] = body_to_tail(body);

  state->snakes->tail_row = tail_row;
  state->snakes->tail_col = tail_col;
}

4.5 update_state

根据游戏规则进行playboard更新即可

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
void update_state(game_state_t *state, int (*add_food)(game_state_t *state))
{
  unsigned int snum = state->num_snakes;
  unsigned int head_row;
  unsigned int head_col;
  unsigned int new_head_row;
  unsigned int new_head_col;
  char head, head_next;
  for (unsigned int i = 0; i < snum; i++)
  {
    // get head and head's next character
    head_row = state->snakes[i].head_row;
    head_col = state->snakes[i].head_col;
    head = state->board[head_row][head_col]; // get head now

    new_head_row = get_next_row(head_row, head);
    new_head_col = get_next_col(head_col, head);
    head_next = state->board[new_head_row][new_head_col]; // get head's next character

    // if dead,only head turns to 'x'
    if (head_next == '#' || is_snake(head_next))
    {
      state->board[head_row][head_col] = 'x';
      state->snakes[i].live = false;
    }
    // if a fruit
    else if (head_next == '*')
    {
      update_head(state, i);
      add_food(state);
    }
    else
    {
      update_head(state, i);
      update_tail(state, i);
    }
  }
  return;
}

Task 5 load_board

函数从给定文件中读取board,你需要将其存储到内存中,注意只能使用fgets,可以使用strchr进行辅助.

5.1 read_line

这个函数需要用到动态内存分配,因为文件中每行的长度并不确定,既要保证能读取任意长度的行,又要保证不浪费的内存空间,可以使用realloc实现内存的释放和重新分配

这部分请务必自己完成!!!

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
char *read_line(FILE *fp)
{
  if (!fp)
    return NULL;
  size_t size = 25;
  char *buf = malloc(sizeof(char *) * size);
  char *current_pos = buf; // 当前缓冲区位置,开始时指向buf
  size_t total_len = 0;

  while (fgets(current_pos, (int)(size - total_len), fp) != NULL)
  {
    size_t len = strlen(current_pos);
    total_len += len;
    if (strchr(current_pos, '\n')) // 如果有换行符说明找到了
      break;

    size_t new_size = size * 2; // 没有则更新读取区大小,重新读取
    char *new_buf = realloc(buf, sizeof(char *) * new_size);
    if (!new_buf)
      return NULL;

    size = new_size;
    buf = new_buf;
    current_pos = buf + total_len;
  }
  if (!total_len)
  {
    free(buf);
    return NULL;
  }

  return buf; // 返回完整的行
}

5.2 load_board

同样由于不知道文件中有多少行,需要实现动态内存分配

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
game_state_t *load_board(FILE *fp)
{
  if (!fp)
    return NULL;

  game_state_t *state = malloc(sizeof(game_state_t));
  if (!state)
    return NULL;
  state->snakes = NULL;
  state->num_snakes = 0;
  state->num_rows = 0;

  size_t capacity = 10;
  state->board = malloc(sizeof(char *) * capacity);

  char *line;
  while ((line = read_line(fp)) != NULL)
  {
    if (state->num_rows >= capacity)
    {
      capacity *= 2;
      char **new_board = realloc(state->board, sizeof(char *) * capacity);
      if (!new_board) // if alloc fails,free all and return NULL
      {
        return NULL;
      }
      state->board = new_board;
    }
    size_t len = strlen(line);
    line[len - 1] = '\0';
    state->board[state->num_rows++] = line;
  }

  return state;
}

Task 6 initialize_snake

这个函数实现扫描给定的playboard并初始化蛇对象

6.1 find_head

由于尾巴位置是给定的,所以从尾巴开始按照方向遍历直到头部即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void find_head(game_state_t *state, unsigned int snum)
{
  unsigned int row = state->snakes[snum].tail_row;
  unsigned int col = state->snakes[snum].tail_col;
  while (!is_head(state->board[row][col]))
  {
    unsigned temp_row = row;
    unsigned temp_col = col;
    row = get_next_row(temp_row, state->board[temp_row][temp_col]);
    col = get_next_col(temp_col, state->board[temp_row][temp_col]);
  }
  state->snakes[snum].head_row = row;
  state->snakes[snum].head_col = col;
  return;
}

6.2 initialize_snake

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
game_state_t *initialize_snakes(game_state_t *state)
{
  unsigned find_order = 0;
  size_t capacity = 10;
  state->snakes = malloc(sizeof(snake_t) * capacity);
  if (!state->snakes)
    return NULL;
  for (unsigned int i = 0; i < state->num_rows; i++)
  {
    char *line = state->board[i];
    size_t len = strlen(line);
    for (unsigned int j = 0; j < len; j++)
    {
      if (is_tail(state->board[i][j]))
      {
        if (state->num_snakes >= capacity)
        {
          capacity *= 2;
          snake_t *new_snakes = realloc(state->snakes, sizeof(snake_t) * capacity);
          if (!new_snakes)
          {
            free(new_snakes);
            return NULL;
          }
          state->snakes = new_snakes;
        }
        state->snakes[find_order].tail_row = i;
        state->snakes[find_order].tail_col = j;
        state->num_snakes += 1;
        state->snakes[find_order].live = true;
        find_head(state, find_order);
        find_order += 1;
      }
    }
  }
  snake_t *final_snakes = realloc(state->snakes, sizeof(snake_t) * (state->num_snakes + 1));
  if (final_snakes)
    state->snakes = final_snakes;
  return state;
}

Task 7 main

按照main函数中的注释补上之前完成的函数就好

测试:

1
2
3
4
5
make run-integration-tests
# 调试特定测试
gdb --args ./snake -i tests/TESTNAME-in.snk -o tests/TESTNAME-out.snk
# 进行内存泄漏检查
valgrind ./snake -i tests/TESTNAME-in.snk -o tests/TESTNAME-out.snk

作为cs61c的第一个project,主要内容算是c语言的基本语法和内存操作,比较简单,继续加油叭~