Эксперимент 55. Игровая логика. "Сокобан"

В прошлом эксперименте у нас уже было игровое поле, кладовщик, цели и ящики. Кроме того мы подключили кнопки и кладовщик реагировал на их нажатия. Теперь нам нужно реализовать логику игры, описать ограничения (такие как запрет на прохождение сквозь стены и ящики).

Схема эксперимента

Схема эксперимента не изменилась.

Рисунок 1. Монтажная схема эксперимента с 8 выводами

Рисунок 2. Монтажная схема эксперимента с 11 выводами

Программный код эксперимента

Exp55.ino
  1. #include <SPI.h>
  2. #include <Adafruit_ST7735.h>
  3. #include "LittleFS_ImageReader.h"
  4.  
  5. #define PIN_CS 2
  6. #define PIN_DC 4
  7. #define PIN_RST 5
  8.  
  9. Adafruit_ST7735 tft = Adafruit_ST7735(PIN_CS, PIN_DC, PIN_RST);
  10. LittleFS_ImageReader reader;
  11.  
  12. bool Map[10][8] = {
  13. {1,1,0,1,1,1,0,1},
  14. {0,1,1,1,1,1,1,0},
  15. {1,1,0,0,0,1,1,1},
  16. {0,1,0,1,0,1,0,1},
  17. {0,1,0,0,0,1,0,1},
  18. {1,1,1,1,0,0,0,1},
  19. {1,0,0,0,0,0,0,1},
  20. {1,0,0,0,1,0,0,1},
  21. {1,0,0,0,1,1,1,1},
  22. {1,1,1,1,1,0,0,0}
  23. };
  24.  
  25. struct Pos {
  26. int x = 0;
  27. int y = 0;
  28.  
  29. bool operator == (const Pos &pos) const {
  30. return x == pos.x && y == pos.y;
  31. }
  32. };
  33.  
  34. class Box {
  35. private:
  36. Adafruit_ST7735 *tft_ptr;
  37. LittleFS_ImageReader *reader_ptr;
  38. Pos pos;
  39. String picture = "/box.bmp";
  40. String picture_on_gate = "/boxngate.bmp";
  41. bool on_gate = false;
  42.  
  43. public:
  44. Box(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) {
  45. tft_ptr = _tft_ptr;
  46. reader_ptr = _reader_ptr;
  47. pos.x = x;
  48. pos.y = y;
  49. }
  50.  
  51. void draw() {
  52. if (on_gate) reader_ptr->drawBMP(picture_on_gate, *tft_ptr, pos.x * 16, pos.y * 16);
  53. else reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16);
  54. }
  55.  
  56. void setOnGate(bool state) {
  57. on_gate = state;
  58. }
  59.  
  60. bool getOnGate() const {
  61. return on_gate;
  62. }
  63.  
  64. Pos getPos() const {
  65. return pos;
  66. }
  67.  
  68. void setPos(Pos _pos) {
  69. pos.x = _pos.x;
  70. pos.y = _pos.y;
  71. draw();
  72. }
  73. };
  74.  
  75. class Gate {
  76. private:
  77. Adafruit_ST7735 *tft_ptr;
  78. LittleFS_ImageReader *reader_ptr;
  79. Pos pos;
  80. String picture = "/gate.bmp";
  81.  
  82. public:
  83. Gate(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) {
  84. tft_ptr = _tft_ptr;
  85. reader_ptr = _reader_ptr;
  86. pos.x = x;
  87. pos.y = y;
  88. }
  89.  
  90. void draw() const {
  91. reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16);
  92. }
  93.  
  94. Pos getPos() const {
  95. return pos;
  96. }
  97. };
  98.  
  99. class Man {
  100. private:
  101. Adafruit_ST7735 *tft_ptr;
  102. LittleFS_ImageReader *reader_ptr;
  103. Pos pos;
  104. String picture = "/man.bmp";
  105.  
  106. public:
  107. Man(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) {
  108. Serial.println("Man constructor");
  109. tft_ptr = _tft_ptr;
  110. reader_ptr = _reader_ptr;
  111. pos.x = x;
  112. pos.y = y;
  113. }
  114.  
  115. void draw() const {
  116. reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16);
  117. }
  118.  
  119. Pos getPos() const {
  120. return pos;
  121. }
  122.  
  123. void setPos(Pos _pos) {
  124. tft_ptr->fillRect(pos.x * 16, pos.y * 16, 16, 16, ST77XX_BLACK);
  125. pos.x = _pos.x;
  126. pos.y = _pos.y;
  127. draw();
  128. }
  129. };
  130.  
  131. class Button {
  132. private:
  133. int pin;
  134. bool pressState;
  135. bool oldState;
  136.  
  137. public:
  138. Button(int _pin, bool _pressState) {
  139. pin = _pin;
  140. setPinMode();
  141. pressState = _pressState;
  142. oldState = not _pressState;
  143. }
  144.  
  145. bool onPress() {
  146. bool state = digitalRead(pin);
  147. if (state != oldState){
  148. oldState = state;
  149. if (state == pressState) return true;
  150. }
  151. return false;
  152. }
  153.  
  154. void setPinMode() const {
  155. pinMode(pin, INPUT);
  156. }
  157. };
  158.  
  159. const int boxes_number = 3;
  160. Box boxes[boxes_number] = {
  161. {&tft, &reader, 3, 4},
  162. {&tft, &reader, 4, 6},
  163. {&tft, &reader, 2, 7},
  164. };
  165.  
  166. const int gates_number = 3;
  167. Gate gates[gates_number] = {
  168. {&tft, &reader, 6, 3},
  169. {&tft, &reader, 6, 4},
  170. {&tft, &reader, 6, 5},
  171. };
  172.  
  173. Man man(&tft, &reader, 5, 6);
  174.  
  175. Button btn_up(16, HIGH);
  176. Button btn_down(15, HIGH);
  177. Button btn_left(12, HIGH);
  178. Button btn_right(0, LOW);
  179.  
  180. bool canMove(Pos pos) {
  181. if (Map[pos.y][pos.x]) return false;
  182. else return true;
  183. }
  184.  
  185. int feelBox(Pos pos) {
  186. for (int i = 0; i < boxes_number; i++) {
  187. if (boxes[i].getPos() == pos) return i;
  188. }
  189. return -1;
  190. }
  191.  
  192. bool boxOnGate(Pos pos) {
  193. for (int i = 0; i < gates_number; i++) {
  194. if (gates[i].getPos() == pos) return true;
  195. }
  196. return false;
  197. }
  198.  
  199. void setup() {
  200. Serial.begin(9600);
  201. Serial.println();
  202. Serial.println("Setup");
  203. LittleFS.begin();
  204. tft.initR(INITR_BLACKTAB);
  205. tft.setRotation(2);
  206. tft.fillScreen(ST77XX_BLACK);
  207.  
  208. for (int y = 0; y < 10; y++) {
  209. for (int x = 0; x < 10; x++) {
  210. if (Map[y][x]) reader.drawBMP("/brick.bmp", tft, x * 16, y * 16);
  211. }
  212. }
  213.  
  214. for (int i = 0; i < boxes_number; i++) {
  215. boxes[i].draw();
  216. }
  217.  
  218. for (int i = 0; i < gates_number; i++) {
  219. gates[i].draw();
  220. }
  221.  
  222. man.draw();
  223.  
  224. btn_left.setPinMode();
  225. }
  226.  
  227. void loop() {
  228. Pos man_pos = man.getPos();
  229. Pos new_pos = {-1, -1};
  230. Pos new_pos_next = {-1, -1};
  231.  
  232. if (btn_up.onPress()) {
  233. new_pos = {man_pos.x, man_pos.y - 1};
  234. new_pos_next = {man_pos.x, man_pos.y - 2};
  235. }
  236.  
  237. if (btn_down.onPress()) {
  238. new_pos = {man_pos.x, man_pos.y + 1};
  239. new_pos_next = {man_pos.x, man_pos.y + 2};
  240. }
  241.  
  242. if (btn_left.onPress()) {
  243. new_pos = {man_pos.x - 1, man_pos.y};
  244. new_pos_next = {man_pos.x - 2, man_pos.y};
  245. }
  246.  
  247. if (btn_right.onPress()) {
  248. new_pos = {man_pos.x + 1, man_pos.y};
  249. new_pos_next = {man_pos.x + 2, man_pos.y};
  250. }
  251.  
  252. Pos not_pos = {-1, -1};
  253.  
  254. if (!(new_pos == not_pos)) {
  255. int box = feelBox(new_pos);
  256. if (box >= 0 && canMove(new_pos_next)) {
  257. if (boxOnGate(new_pos_next)) boxes[box].setOnGate(true);
  258. else boxes[box].setOnGate(false);
  259.  
  260. boxes[box].setPos(new_pos_next);
  261. man.setPos(new_pos);
  262. }
  263.  
  264. if (box == -1 && canMove(new_pos)) man.setPos(new_pos);
  265.  
  266. for (int i = 0; i < gates_number; i++) {
  267. if (feelBox(gates[i].getPos()) == -1 and !(man.getPos() == gates[i].getPos())) {
  268. gates[i].draw();
  269. }
  270. }
  271.  
  272. }
  273.  
  274. bool win = true;
  275.  
  276. for (int i = 0; i < boxes_number; i++)
  277. {
  278. if (!boxes[i].getOnGate()){
  279. win = false;
  280. break;
  281. }
  282. }
  283.  
  284. if (win) reader.drawBMP("/win.bmp", tft, 0, 0);
  285. }

Мы добавили несколько дополнительных функций. А именно:

canMove(x, y) — проверяет возможность перемещения кладовщика или ящика в заданную координату. Эта функция просто проверяет наличие стены в данном месте. Если ее там нет — значит переместиться в эту координату можно. Если стена есть, то нельзя.

  1. bool canMove(Pos pos) {
  2. if (Map[pos.y][pos.x]) return false;
  3. else return true;
  4. }

Функция feelBox(x, y) — проверяет наличие ящика в заданной координате. Эта проверка происходит при движении кладовщика. Если перед ним находится ящик, то он должен сдвинуться. Функция перебирает все объекты ящиков и сравнивает их координаты с заданной. Если на пути есть ящик, то возвращается индекс объекта этого ящика в массиве, иначе возвращается -1.

  1. int feelBox(Pos pos) {
  2. for (int i = 0; i < boxes_number; i++) {
  3. if (boxes[i].getPos() == pos) return i;
  4. }
  5. return -1;
  6. }

Функция boxOnGate() — проверяет находится ли ящик на цели. В качестве параметров функция принимает координату ящика и проверяет наличие цели с такой же координатой.

  1. bool boxOnGate(Pos pos) {
  2. for (int i = 0; i < gates_number; i++) {
  3. if (gates[i].getPos() == pos) return true;
  4. }
  5. return false;
  6. }

В основном цикле программы мы получаем текущую координату кладовщика и задаем новую координату как (-1, -1). При обнаружении нажатия на кнопку, новая координата пересчитывается исходя из текущей координаты и нажатой кнопки. Кроме того высчитывается следующая координата в направлении движения после новой. Например если мы перемещаемся вверх, то вычисляем координаты на 1 клетку вверх и на 2 клетки вверх. Вторая координата нужна на случай если перед кладовщиком окажется ящик. Тогда он должен будет перейти на 1 клетку вверх, и ящик тоже (на 2 клетки вверх относительно текущей координаты кладовщика). Если нажатия на кнопку не было, то на этом итерация цикла завершается.

Если нажатие было и новая координата определена, то происходит проверка новой координаты. Сначала мы проверяем есть ли на пути кладовщика ящик:

  1. int box = feelBox(new_pos);

Если ящик есть (индекс больше или равен 0), значит мы должны двигаться вместе с ящиком. Но для этого нужно проверить может ли передвинуться ящик (нет ли стены перед ящиком).

  1. if (box >= 0 && canMove(new_pos_next)) {

Если все в порядке и ящик может передвинуться, то проверяем еще одно условие — находится ли ящик на цели. Если да, то boxes[box].setOnGate(True), если нет, то boxes[box].setOnGate(False).

После чего передвигаем ящик и кладовщика в новую координату.

Этим описывается вся логика перемещения кладовщика и ящиков.

Далее идет вспомогательная логика. В частности мы перерисовываем цели, если на них нет ящика или кладовщика. Это нужно, чтобы после прохождения ящика или кладовщика по цели они прорисовывались заново, но не прорисовывались поверх ящика или кладовщика:

  1. for (int i = 0; i < gates_number; i++) {
  2. if (feelBox(gates[i].getPos()) == -1 and !(man.getPos() == gates[i].getPos())) {
  3. gates[i].draw();
  4. }
  5. }

Далее идет проверка условия выигрыша. Игра считается выигранной когда все ящики установлены на свои цели. Для этого мы проверяем условие нахождения ящика на цели для каждого ящика. Если у всех ящиков соблюдается данное условие, то игра выиграна. А когда она выиграна на дисплее отображается победная картинка.

  1. bool win = true;
  2.  
  3. for (int i = 0; i < boxes_number; i++)
  4. {
  5. if (!boxes[i].getOnGate()){
  6. win = false;
  7. break;
  8. }
  9. }
  10.  
  11. if (win) reader.drawBMP("/win.bmp", tft, 0, 0);

Таким образом мы реализовали известную игру Sokoban.

Попробуй нарисовать свой уровень с другой конфигурацией стен и другими положениями ящиков и целей