• c语言贪吃蛇项目的实现


    ncurse的引入

    ncurse的概念

    ncurse(new curses)是一套编程库,它提供了一系列的函数,以便使用者调用它们去生成基于文本的用户界面。 ncurses是一个能提供功能键定义(快捷键),屏幕绘制以及基于文本终端的图形互动功能的动态库。ncurses用得最多的地方是linux内核编译之前的内核配置,ncurses早已淡出舞台,甚至体验感完爆ncurses的C图形库GTK、C++图形库QT也区趋于落伍嵌入式设备上的Android 系统。这个游戏只是使用ncurses并不以学习它为目的,主要还是通过这个游戏锻炼我们C语言的能力。


    ncurse的格式

    1. #include //记得包含头文件
    2. int main()
    3. {
    4. initscr();//ncurse界面的初始化函数
    5. printw("this is a test:n");//在ncurse模式下的printf
    6. getch();//等待用户输入,如果没有这句话,程序就退出来了,看不到运行结果,也就是看不到上面的那句话
    7. endwin();//程序退出,调用改函数来恢复shell终端的显示,如果没有这句话,shell终端字乱码,会坏掉
    8. return 0;
    9. }

    ncurse的编译

    ncurse的编译需要在文件名.c后加-lcurses

    上述格式代码编译结果如下:


    nurses的上下左右键值获取及演示

    如果不用nurses,那么每次输入上下左右得输入回车小蛇才会有反应,而运用nurses只需输入上下左右小蛇便会做出反应,提高灵敏度,因为该项目键盘中只需用到上下左右,所以我们只需获得上下左右键值即可。

    进入nurses头文件方式

    vi /usr/include/curses.h

    上下左右键值宏定义

    1. #define KEY_DOWN 0402
    2. #define KEY_UP 0403
    3. #define KEY_LEFT 0404
    4. #define KEY_RIGHT 0405

    代码演示

    1. #include
    2. int main()
    3. {
    4. int key;
    5. initscr();
    6. keypad(stdscr,1);//函数调用→接受功能键,是否接收(1是接收)
    7. while(1)
    8. {
    9. key = getch();
    10. switch(key)
    11. {
    12. case 0402:
    13. printw("DOWN\n");
    14. break;
    15. case 0403:
    16. printw("UP\n");
    17. break;
    18. case 0404:
    19. printw("LEFT\n");
    20. break;
    21. case 0405:
    22. printw("RIGHT\n");
    23. break;
    24. }
    25. }
    26. endwin();
    27. return 0;
    28. }

    编译结果


    地图规划


    地图边界

    代码演示

    1. #include
    2. void gameMap()
    3. {
    4. int hang,lie;
    5. for(hang = 0;hang <20;hang++)
    6. {
    7. if(hang == 0)
    8. {
    9. for(lie = 0;lie < 20;lie++)
    10. {
    11. printw("--");
    12. }
    13. printw("\n");
    14. }
    15. if(hang >= 0 && hang <= 18)
    16. {
    17. for(lie = 0;lie <= 20;lie++)
    18. {
    19. if(lie == 0 || lie == 20)
    20. {
    21. printw("|");
    22. }
    23. else
    24. {
    25. printw(" ");
    26. }
    27. }
    28. printw("\n");
    29. }
    30. if(hang == 19)
    31. {
    32. for(lie = 0;lie < 20;lie++)
    33. {
    34. printw("--");
    35. }
    36. printw("\n");
    37. }
    38. }
    39. }
    40. int main()
    41. {
    42. gameMap();
    43. getch();
    44. endwin();
    45. return 0;
    46. }

    代码运行结果:


    贪吃蛇节点身子

    代码演示

    1. #include
    2. #include
    3. struct snakebody
    4. {
    5. int hang;
    6. int lie;
    7. struct snakebody *next;
    8. };
    9. struct snakebody *head;
    10. struct snakebody *tail;
    11. int snakeNode(int i,int j)
    12. {
    13. struct snakebody *p;
    14. p = head;//指向链表头
    15. while(p != NULL)
    16. {
    17. if(p->hang == i && p->lie == j)
    18. {
    19. return 1;
    20. }
    21. p = p->next;
    22. }
    23. return 0;
    24. }
    25. void gameMap()
    26. {
    27. int hang,lie;
    28. for(hang = 0;hang <20;hang++)
    29. {
    30. if(hang == 0)
    31. {
    32. for(lie = 0;lie < 20;lie++)
    33. {
    34. printw("--");
    35. }
    36. printw("\n");
    37. }
    38. if(hang >= 0 && hang <= 18)
    39. {
    40. for(lie = 0;lie <= 20;lie++)
    41. {
    42. if(lie == 0 || lie == 20)
    43. {
    44. printw("|");
    45. }
    46. else if(snakeNode(hang,lie))
    47. {
    48. printw("[]");//蛇结点身子的出现
    49. }
    50. else
    51. {
    52. printw(" ");
    53. }
    54. }
    55. printw("\n");
    56. }
    57. if(hang == 19)
    58. {
    59. for(lie = 0;lie < 20;lie++)
    60. {
    61. printw("--");
    62. }
    63. printw("\n");
    64. }
    65. }
    66. }
    67. void addNode()
    68. {
    69. struct snakebody *new = (struct snakebody *)malloc(sizeof(struct snakebody));
    70. new->hang = tail->hang;
    71. new->lie = tail->lie+1;
    72. new->next = NULL;
    73. tail->next = new;//新的节点赋给尾的下一个
    74. tail = new;//当尾后面没有节点时,新节点就是尾
    75. }
    76. void initSnake()//身子初始化
    77. {
    78. head = (struct snakebody *)malloc(sizeof(struct snakebody));
    79. head->hang = 2;
    80. head->lie = 2;
    81. head->next = NULL;
    82. tail = head;//一开始头就是尾
    83. addNode();//头已经定义 调用一次该函数,就是多一节
    84. addNode();
    85. addNode();
    86. }
    87. int main()
    88. {
    89. initcurses();
    90. initSnake();
    91. gameMap();
    92. getch();
    93. endwin();
    94. return 0;
    95. }

    代码运行结果:


    贪吃蛇的移动和“复活”

    完成了地图规划就应该对蛇进行操控使得更加有体验感 接下来一步一步让代码使得体验更加逼真。

    实现贪吃蛇向右移动

    向右移动主要分为两个步骤:第一个是头结点删除,使得第二个结点成为头结点;第二个是新结点从尾部加入。代码如下:

    1. #include
    2. #include
    3. void gameMap()
    4. {
    5. int hang,lie;
    6. move(0,0);//每次偏移时在地图里展现出来
    7. for(hang = 0;hang <20;hang++)
    8. {
    9. if(hang == 0)
    10. {
    11. for(lie = 0;lie < 20;lie++)
    12. {
    13. printw("--");
    14. }
    15. printw("\n");
    16. }
    17. if(hang >= 0 && hang <= 18)
    18. {
    19. for(lie = 0;lie <= 20;lie++)
    20. {
    21. if(lie == 0 || lie == 20)
    22. {
    23. printw("|");
    24. }
    25. else if(snakeNode(hang,lie))
    26. {
    27. printw("[]");
    28. }
    29. else
    30. {
    31. printw(" ");
    32. }
    33. }
    34. printw("\n");
    35. }
    36. if(hang == 19)
    37. {
    38. for(lie = 0;lie < 20;lie++)
    39. {
    40. printw("--");
    41. }
    42. printw("\n");
    43. }
    44. }
    45. }
    46. void addNode()
    47. {
    48. struct snakebody *new = (struct snakebody *)malloc(sizeof(struct snakebody));
    49. new->hang = tail->hang;
    50. new->lie = tail->lie+1;
    51. new->next = NULL;
    52. tail->next = new;
    53. tail = new;
    54. }
    55. void initSnake()
    56. {
    57. head = (struct snakebody *)malloc(sizeof(struct snakebody));
    58. head->hang = 2;
    59. head->lie = 2;
    60. head->next = NULL;
    61. tail = head;
    62. addNode();
    63. addNode();
    64. addNode();
    65. }
    66. void deletNode()
    67. {
    68. struct snakebody *p;
    69. p = head;
    70. head = head->next;//删除头结点
    71. free(p);//对删除的头结点进行空间内存释放
    72. }
    73. void movesnake()
    74. {
    75. addNode();//在尾部增加结点
    76. deletNode();/调用删除头结点函数
    77. }
    78. int main()
    79. {
    80. int control;
    81. initcurses();
    82. initSnake();
    83. gameMap();
    84. while(1)
    85. {
    86. control = getch();
    87. if(control == KEY_RIGHT)
    88. {
    89. movesnake();
    90. gameMap();//偏移后刷新地图
    91. }
    92. }
    93. getch();
    94. endwin();
    95. return 0;
    96. }

    贪吃蛇撞墙回到起始位置

    这里先实现蛇在水平方向的偏移,如果蛇的尾结点碰到地图边界,则让其初始化。代码如下

    1. #include
    2. #include
    3. void initcurses()
    4. {
    5. initscr();
    6. keypad(stdscr,1);
    7. }
    8. struct snakebody
    9. {
    10. int hang;
    11. int lie;
    12. struct snakebody *next;
    13. };
    14. struct snakebody *head = NULL;
    15. struct snakebody *tail = NULL;
    16. int snakeNode(int i,int j)
    17. {
    18. struct snakebody *p;
    19. p = head;
    20. while(p != NULL)
    21. {
    22. if(p->hang == i && p->lie == j)
    23. {
    24. return 1;
    25. }
    26. p = p->next;
    27. }
    28. return 0;
    29. }
    30. void gameMap()
    31. {
    32. int hang,lie;
    33. move(0,0);
    34. for(hang = 0;hang <20;hang++)
    35. {
    36. if(hang == 0)
    37. {
    38. for(lie = 0;lie < 20;lie++)
    39. {
    40. printw("--");
    41. }
    42. printw("\n");
    43. }
    44. if(hang >= 0 && hang <= 18)
    45. {
    46. for(lie = 0;lie <= 20;lie++)
    47. {
    48. if(lie == 0 || lie == 20)
    49. {
    50. printw("|");
    51. }
    52. else if(snakeNode(hang,lie))
    53. {
    54. printw("[]");
    55. }
    56. else
    57. {
    58. printw(" ");
    59. }
    60. }
    61. printw("\n");
    62. }
    63. if(hang == 19)
    64. {
    65. for(lie = 0;lie < 20;lie++)
    66. {
    67. printw("--");
    68. }
    69. printw("\n");
    70. }
    71. }
    72. }
    73. void addNode()
    74. {
    75. struct snakebody *new = (struct snakebody *)malloc(sizeof(struct snakebody));
    76. new->hang = tail->hang;
    77. new->lie = tail->lie+1;
    78. new->next = NULL;
    79. tail->next = new;
    80. tail = new;
    81. }
    82. void initSnake()
    83. {
    84. struct snakebody *p;
    85. while(head != NULL)//蛇撞墙后初始化时原来的身子还占内存空间,需对其进行释放
    86. {
    87. p = head;//指向链表头
    88. head = head->next;//遍历整个链表
    89. free(p);//释放整个链表内存空间
    90. }
    91. head = (struct snakebody *)malloc(sizeof(struct snakebody));
    92. head->hang = 1;
    93. head->lie = 1;
    94. head->next = NULL;
    95. tail = head;
    96. addNode();
    97. addNode();
    98. addNode();
    99. }
    100. void deletNode()
    101. {
    102. struct snakebody *p;
    103. p = head;
    104. head = head->next;
    105. free(p);
    106. }
    107. void movesnake()
    108. {
    109. addNode();
    110. deletNode();
    111. if(tail->hang == 0 || tail->lie == 0 || tail->hang == 20 || tail->lie == 20)
    112. {
    113. initSnake();//蛇撞到边界时初始化
    114. }
    115. }
    116. int main()
    117. {
    118. int control;
    119. initcurses();
    120. initSnake();
    121. gameMap();
    122. while(1)
    123. {
    124. control = getch();
    125. if(control == KEY_RIGHT)
    126. {
    127. movesnake();
    128. gameMap();
    129. }
    130. }
    131. getch();
    132. endwin();
    133. return 0;
    134. }

    贪吃蛇的自由移动

    线程的引入

    若要蛇的移动和按键检测同时进行,用两个while循环是无法实现的,其只会执行第一个循环中的内容,此时就得需要用到线程。线程是Linux中常见的函数库,在此项目中的作用是能够使两个循环同时进行。线程的定义格式是pthread_t+任意名字,创建线程格式是pthread_create(前面的任意名字取地址,NULL,循环的名字,NULL)同时线程有独自的头文件和链接库,如下:

    1. void* changeDir()
    2. {
    3. }
    4. pthread_t t1;
    5. pthread_create(&t1,NULL,changeDir,NULL);
    #include 
    gcc a.c -lcurses -lpthread

    自由移动代码演示:

    1. #include
    2. #include
    3. #include
    4. #define UP 1
    5. #define DOWN -1
    6. #define LEFT 2
    7. #define RIGHT -2
    8. void initcurses()
    9. {
    10. initscr();
    11. keypad(stdscr,1);
    12. noecho();//ncurses自带的函数,作用是不让无关信息出现在显示屏上
    13. }
    14. struct snakebody
    15. {
    16. int hang;
    17. int lie;
    18. struct snakebody *next;
    19. };
    20. struct snakebody *head = NULL;
    21. struct snakebody *tail = NULL;
    22. int key;
    23. int dir;
    24. int snakeNode(int i,int j)
    25. {
    26. struct snakebody *p;
    27. p = head;
    28. while(p != NULL)
    29. {
    30. if(p->hang == i && p->lie == j)
    31. {
    32. return 1;
    33. }
    34. p = p->next;
    35. }
    36. return 0;
    37. }
    38. void gameMap()
    39. {
    40. int hang,lie;
    41. move(0,0);
    42. for(hang = 0;hang <20;hang++)
    43. {
    44. if(hang == 0)
    45. {
    46. for(lie = 0;lie < 20;lie++)
    47. {
    48. printw("--");
    49. }
    50. printw("\n");
    51. }
    52. if(hang >= 0 && hang <= 18)
    53. {
    54. for(lie = 0;lie <= 20;lie++)
    55. {
    56. if(lie == 0 || lie == 20)
    57. {
    58. printw("|");
    59. }
    60. else if(snakeNode(hang,lie))
    61. {
    62. printw("[]");
    63. }
    64. else
    65. {
    66. printw(" ");
    67. }
    68. }
    69. printw("\n");
    70. }
    71. if(hang == 19)
    72. {
    73. for(lie = 0;lie < 20;lie++)
    74. {
    75. printw("--");
    76. }
    77. printw("\n");
    78. printw("key=%d\n",key);
    79. }
    80. }
    81. }
    82. void addNode()
    83. {
    84. struct snakebody *new = (struct snakebody *)malloc(sizeof(struct snakebody));
    85. new->next = NULL;
    86. switch(dir)
    87. {
    88. case UP:
    89. new->hang = tail->hang-1;//蛇向上,行减一即可
    90. new->lie = tail->lie;
    91. break;
    92. case DOWN:
    93. new->hang = tail->hang+1;//蛇向下,行加一即可
    94. new->lie = tail->lie;
    95. break;
    96. case LEFT:
    97. new->hang = tail->hang;//蛇向左,列减一即可
    98. new->lie = tail->lie-1;
    99. break;
    100. case RIGHT:
    101. new->hang = tail->hang;//蛇向右,行加一即可
    102. new->lie = tail->lie+1;
    103. break;
    104. }
    105. tail->next = new;
    106. tail = new;
    107. }
    108. void initSnake()
    109. {
    110. struct snakebody *p;
    111. dir = RIGHT;
    112. while(head != NULL)
    113. {
    114. p = head;
    115. head = head->next;
    116. free(p);
    117. }
    118. head = (struct snakebody *)malloc(sizeof(struct snakebody));
    119. head->hang = 1;
    120. head->lie = 1;
    121. head->next = NULL;
    122. tail = head;
    123. addNode();
    124. addNode();
    125. addNode();
    126. }
    127. void deletNode()
    128. {
    129. struct snakebody *p;
    130. p = head;
    131. head = head->next;
    132. free(p);
    133. }
    134. void movesnake()
    135. {
    136. addNode();
    137. deletNode();
    138. if(tail->hang == 0 || tail->lie == 0 || tail->hang == 20 || tail->lie == 20)
    139. {
    140. initSnake();
    141. }
    142. }
    143. void* refreshJiemian()
    144. {
    145. while(1)
    146. {
    147. movesnake();
    148. gameMap();
    149. refresh();//ncurses自带刷新函数
    150. usleep(100000);//每个100000us向右移动一次
    151. }
    152. }
    153. void turn(int direction)
    154. {
    155. if(abs(dir) != abs(direction))//abs是一个运算符 表示的是参数的绝对值
    156. //如果输入方向的绝对值等于蛇前进方向的绝对值,则蛇的路径不发生改变
    157. //如果输入方向的绝对值不等于蛇前进方向的绝对值,则原有方向改变成输入方向
    158. {
    159. dir = direction;
    160. }
    161. }
    162. void* changeDir()
    163. {
    164. while(1)
    165. {
    166. key = getch();
    167. switch(key)
    168. {
    169. case 0402:
    170. turn(DOWN);//调用turn函数,并将方向传过去,判断输入方向与原有方向绝对值是否一致
    171. break;
    172. case 0403:
    173. turn(UP);
    174. break;
    175. case 0404:
    176. turn(LEFT);
    177. break;
    178. case 0405:
    179. turn(RIGHT);
    180. break;
    181. }
    182. }
    183. }
    184. int main()
    185. {
    186. pthread_t t1;
    187. pthread_t t2;
    188. initcurses();
    189. initSnake();
    190. gameMap();
    191. pthread_create(&t1,NULL,refreshJiemian,NULL);
    192. pthread_create(&t2,NULL,changeDir,NULL);
    193. while(1);
    194. getch();
    195. endwin();
    196. return 0;
    197. }

    贪吃蛇的食物及"食"物(结尾)

    贪吃蛇食物关心的是其位置和符号,我们这里把符号定义为##,位置可以在蛇的身体结构体定义一个食物结构体,让食物的行、列在地图中被扫描,从而出现食物。食物的出现是随机的,这里需要调用一个函数,是rand(),其作用是生成一个随机数,定义格式在下面代码中展示,同时我们的地图是20x20,只需要使其范围在19以内(0开始)即可,这里运用取余的方法:行列都是20,让生成的随机数对20取余,即%20,那么出现的数的范围就在0到19(刚好为20倍数是为0)。"食"物的根本是判断蛇的尾部的行列值是否与食物行列值一致,如果一致则在吃到那一瞬间新增一个身体结点,不执行删除函数;如果不一致,则按原来一样自由移动。代码如下:

    1. #include
    2. #include
    3. #include
    4. #define UP 1
    5. #define DOWN -1
    6. #define LEFT 2
    7. #define RIGHT -2
    8. void initcurses()
    9. {
    10. initscr();
    11. keypad(stdscr,1);
    12. noecho();
    13. }
    14. struct snakebody
    15. {
    16. int hang;
    17. int lie;
    18. struct snakebody *next;
    19. };
    20. struct snakebody *head = NULL;
    21. struct snakebody *tail = NULL;
    22. int key;
    23. int dir;
    24. struct snakebody food;
    25. void initfood()
    26. {
    27. int x = rand()%20;
    28. int y = rand()%20;
    29. food.hang = x;
    30. food.lie = y;
    31. }
    32. int snakeNode(int i,int j)
    33. {
    34. struct snakebody *p;
    35. p = head;
    36. while(p != NULL)
    37. {
    38. if(p->hang == i && p->lie == j)
    39. {
    40. return 1;
    41. }
    42. p = p->next;
    43. }
    44. return 0;
    45. }
    46. int creatfood(int i,int j)
    47. {
    48. if(food.hang == i && food.lie == j)//如果食物与传进来的参数行列值一样 就运行该函数 该函数主要用于后面地图出现食物符号
    49. {
    50. return 1;
    51. }
    52. return 0;
    53. }
    54. void gameMap()
    55. {
    56. int hang,lie;
    57. move(0,0);
    58. for(hang = 0;hang <20;hang++)
    59. {
    60. if(hang == 0)
    61. {
    62. for(lie = 0;lie < 20;lie++)
    63. {
    64. printw("--");
    65. }
    66. printw("\n");
    67. }
    68. if(hang >= 0 && hang <= 18)
    69. {
    70. for(lie = 0;lie <= 20;lie++)
    71. {
    72. if(lie == 0 || lie == 20)
    73. {
    74. printw("|");
    75. }
    76. else if(snakeNode(hang,lie))
    77. {
    78. printw("[]");
    79. }
    80. else if(creatfood(hang,lie))
    81. {
    82. printw("##");//设置食物在地图出现
    83. }
    84. else
    85. {
    86. printw(" ");
    87. }
    88. }
    89. printw("\n");
    90. }
    91. if(hang == 19)
    92. {
    93. for(lie = 0;lie < 20;lie++)
    94. {
    95. printw("--");
    96. }
    97. printw("\n");
    98. }
    99. }
    100. }
    101. void addNode()
    102. {
    103. struct snakebody *new = (struct snakebody *)malloc(sizeof(struct snakebody));
    104. new->next = NULL;
    105. switch(dir)
    106. {
    107. case UP:
    108. new->hang = tail->hang-1;
    109. new->lie = tail->lie;
    110. break;
    111. case DOWN:
    112. new->hang = tail->hang+1;
    113. new->lie = tail->lie;
    114. break;
    115. case LEFT:
    116. new->hang = tail->hang;
    117. new->lie = tail->lie-1;
    118. break;
    119. case RIGHT:
    120. new->hang = tail->hang;
    121. new->lie = tail->lie+1;
    122. break;
    123. }
    124. tail->next = new;
    125. tail = new;
    126. }
    127. void initSnake()
    128. {
    129. struct snakebody *p;
    130. dir = RIGHT;
    131. while(head != NULL)
    132. {
    133. p = head;
    134. head = head->next;
    135. free(p);
    136. }
    137. initfood();
    138. head = (struct snakebody *)malloc(sizeof(struct snakebody));
    139. head->hang = 1;
    140. head->lie = 1;
    141. head->next = NULL;
    142. tail = head;
    143. addNode();
    144. addNode();
    145. addNode();
    146. }
    147. void deletNode()
    148. {
    149. struct snakebody *p;
    150. p = head;
    151. head = head->next;
    152. free(p);
    153. }
    154. int ifSnakeDie()
    155. {
    156. struct snakebody *p;
    157. p = head;
    158. if(tail->hang < 0 || tail->lie == 0 || tail->hang == 20 || tail->lie == 20)
    159. {
    160. return 1;//碰到边界则运行该函数,该函数主要用于后面进行碰壁后蛇的初始化
    161. }
    162. while(p->next != NULL)
    163. {
    164. if(p->hang == tail->hang && p->lie == tail->lie)
    165. {
    166. return 1;
    167. }
    168. p = p->next;
    169. }
    170. return 0;
    171. }
    172. void movesnake()
    173. {
    174. addNode();
    175. if(creatfood(tail->hang,tail->lie))//将蛇的尾巴参数传到该函数 相当于蛇吃到食物 则初始化食物
    176. {
    177. initfood();
    178. }
    179. else
    180. {
    181. deletNode();
    182. }
    183. if(ifSnakeDie())
    184. {
    185. initSnake();
    186. }
    187. }
    188. void* refreshJiemian()
    189. {
    190. while(1)
    191. {
    192. movesnake();
    193. gameMap();
    194. refresh();
    195. usleep(100000);
    196. }
    197. }
    198. void turn(int direction)
    199. {
    200. if(abs(dir) != abs(direction))
    201. {
    202. dir = direction;
    203. }
    204. }
    205. void* changeDir()
    206. {
    207. while(1)
    208. {
    209. key = getch();
    210. switch(key)
    211. {
    212. case 0402:
    213. turn(DOWN);
    214. break;
    215. case 0403:
    216. turn(UP);
    217. break;
    218. case 0404:
    219. turn(LEFT);
    220. break;
    221. case 0405:
    222. turn(RIGHT);
    223. break;
    224. }
    225. }
    226. }
    227. int main()
    228. {
    229. pthread_t t1;
    230. pthread_t t2;
    231. initcurses();
    232. initSnake();
    233. gameMap();
    234. pthread_create(&t1,NULL,refreshJiemian,NULL);
    235. pthread_create(&t2,NULL,changeDir,NULL);
    236. while(1);
    237. getch();
    238. endwin();
    239. return 0;
    240. }

  • 相关阅读:
    CleanMyMac X2023测评是不是恶意软件?
    wps快速生成目录及页码设置(自备)
    LeetCode-高频 SQL 50 题:查询 篇
    c++编程实例
    01【什么是设计模式】
    REACT全家桶(4)----组件
    人脸识别技术,如何解决学校门禁安全?
    实验十七:模拟霍尔传感器实验
    《中学科技》是什么级别的刊物?如何投稿?
    爱创科技携手洽洽食品,探索渠道数字化最优解!
  • 原文地址:https://blog.csdn.net/2301_78772787/article/details/134249651