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

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

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

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

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

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

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

Exp55.py
  1. from machine import Pin, SPI
  2. from tft import TFT_GREEN
  3. _init()
  4. machine.freq(160000000)
  5.  
  6. dc = Pin(4, Pin.OUT)
  7. cs = Pin(2, Pin.OUT)
  8. rst = Pin(5, Pin.OUT)
  9. spi = SPI(1, baudrate=40000000, polarity=0, phase=0)
  10.  
  11. tft = TFT_GREEN(128, 160, spi, dc, cs, rst, rotate=0)
  12.  
  13. Map = [
  14. [1,1,0,1,1,1,0,1],
  15. [0,1,1,1,1,1,1,0],
  16. [1,1,0,0,0,1,1,1],
  17. [0,1,0,1,0,1,0,1],
  18. [0,1,0,0,0,1,0,1],
  19. [1,1,1,1,0,0,0,1],
  20. [1,0,0,0,0,0,0,1],
  21. [1,0,0,0,1,0,0,1],
  22. [1,0,0,0,1,1,1,1],
  23. [1,1,1,1,1,0,0,0]
  24. ]
  25.  
  26. Gates = []
  27. Boxes = []
  28.  
  29. class Box:
  30. def __init__(self, tft, x, y):
  31. self.tft = tft
  32. self.x = x
  33. self.y = y
  34. self.picture = 'box.bmp'
  35. self.picture_onGate = 'boxngate.bmp'
  36. self.onGate = False
  37. self.draw()
  38.  
  39. def draw(self):
  40. if (self.onGate):
  41. self.tft.draw_bmp(self.x * 16,self.y * 16, self.picture_onGate)
  42. else:
  43. self.tft.draw_bmp(self.x * 16,self.y * 16, self.picture)
  44.  
  45. def setOnGate(self, state):
  46. self.onGate = state
  47.  
  48. def getOnGate(self):
  49. return self.onGate
  50.  
  51. def getPos(self):
  52. return (self.x, self.y)
  53.  
  54. def setPos(self, x, y):
  55. self.x = x
  56. self.y = y
  57. self.draw()
  58.  
  59. class Gate:
  60. def __init__(self, tft, x, y):
  61. self.tft = tft
  62. self.x = x
  63. self.y = y
  64. self.picture = 'gate.bmp'
  65. self.draw()
  66.  
  67. def draw(self):
  68. self.tft.draw_bmp(self.x * 16,self.y * 16, self.picture)
  69.  
  70. def getPos(self):
  71. return (self.x, self.y)
  72.  
  73. class Man:
  74. def __init__(self, tft, x, y):
  75. self.tft = tft
  76. self.x = x
  77. self.y = y
  78. self.picture = 'man.bmp'
  79. self.draw()
  80.  
  81. def draw(self):
  82. self.tft.draw_bmp(self.x * 16,self.y * 16, self.picture)
  83.  
  84. def getPos(self):
  85. return (self.x, self.y)
  86.  
  87. def setPos(self, x, y):
  88. self.tft.rect(self.x * 16, self.y * 16, 16, 16, tft.COLOR_BLACK)
  89. self.x = x
  90. self.y = y
  91. self.draw()
  92.  
  93. class Button:
  94. def __init__(self, p, pressSate):
  95. self.pin = Pin(p, Pin.IN)
  96. self.pressSate = pressSate
  97. self.oldState = 0
  98.  
  99. def onPress(self):
  100. state = self.pin.value()
  101. if state != self.oldState:
  102. self.oldState = state
  103. if state == self.pressSate:
  104. return True
  105. return False
  106.  
  107. tft.initr(tft.BGR) # tft.initr(tft.RGB) #Если вместо синего цвета отображается красный, а вместо красного синий
  108. tft.clear(tft.COLOR_BLACK)
  109.  
  110. x = 0
  111. y = 0
  112.  
  113. for row in Map:
  114. for col in row:
  115. if col:
  116. tft.draw_bmp(x * 16, y * 16,'brick.bmp')
  117. x+=1
  118. x=0
  119. y+=1
  120.  
  121. Boxes.append(Box(tft, 3,4))
  122. Boxes.append(Box(tft, 4,6))
  123. Boxes.append(Box(tft, 2,7))
  124.  
  125. Gates.append(Gate(tft, 6,3))
  126. Gates.append(Gate(tft, 6,4))
  127. Gates.append(Gate(tft, 6,5))
  128.  
  129. man = Man(tft, 5, 6)
  130.  
  131. btnUp = Button(16, 1)
  132. btnDown = Button(15, 1)
  133. btnLeft = Button(12, 1)
  134. btnRight = Button(0, 0)
  135.  
  136. def canMove(x,y):
  137. if (Map[y][x]):
  138. return False
  139. else:
  140. return True
  141.  
  142. def feelBox(x,y):
  143. for B in Boxes:
  144. if B.getPos() == (x,y):
  145. return B
  146. return False
  147.  
  148. def boxFeelGate(x,y):
  149. for G in Gates:
  150. if G.getPos() == (x,y):
  151. return True
  152. return False
  153.  
  154. while True:
  155. mPos = man.getPos()
  156. newPos = (-1,-1)
  157.  
  158. if btnUp.onPress():
  159. newPos = (mPos[0], mPos[1]-1)
  160. newPosNext = (mPos[0], mPos[1]-2)
  161.  
  162. if btnDown.onPress():
  163. newPos = (mPos[0], mPos[1]+1)
  164. newPosNext = (mPos[0], mPos[1]+2)
  165.  
  166. if btnLeft.onPress():
  167. newPos = (mPos[0]-1, mPos[1])
  168. newPosNext = (mPos[0]-2, mPos[1])
  169.  
  170. if btnRight.onPress():
  171. newPos = (mPos[0]+1, mPos[1])
  172. newPosNext = (mPos[0]+2, mPos[1])
  173.  
  174. if newPos != (-1,-1):
  175. B = feelBox(newPos[0], newPos[1])
  176. if B and canMove(newPosNext[0], newPosNext[1]):
  177. if boxFeelGate(newPosNext[0], newPosNext[1]):
  178. B.setOnGate(True)
  179. else:
  180. B.setOnGate(False)
  181. B.setPos(newPosNext[0], newPosNext[1])
  182. man.setPos(newPos[0], newPos[1])
  183. if not B and canMove(newPos[0], newPos[1]):
  184. man.setPos(newPos[0], newPos[1])
  185.  
  186. for G in Gates:
  187. gPos = G.getPos()
  188. if not (feelBox(gPos[0], gPos[1])) and gPos != man.getPos():
  189. G.draw()
  190.  
  191. win = 1
  192. for B in Boxes:
  193. if (not B.getOnGate()):
  194. win = 0
  195. break
  196.  
  197. if (win):
  198. tft.draw_bmp(0, 16, 'win.bmp')
  199. raise SystemExit

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

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

  1. def canMove(x,y):
  2. if (Map[y][x]):
  3. return False
  4. else:
  5. return True

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

  1. def feelBox(x,y):
  2. for B in Boxes:
  3. if B.getPos() == (x,y):
  4. return B
  5. return False

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

  1. def boxFeelGate(x,y):
  2. for G in Gates:
  3. if G.getPos() == (x,y):
  4. return True
  5. return False

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

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

  1. B = feelBox(newPos[0], newPos[1])

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

  1. if B and canMove(newPosNext[0], newPosNext[1]):

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

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

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

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

  1. for G in Gates:
  2. gPos = G.getPos()
  3. if not (feelBox(gPos[0], gPos[1])) and gPos != man.getPos():
  4. G.draw()

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

  1. win = 1
  2. for B in Boxes:
  3. if (not B.getOnGate()):
  4. win = 0
  5. break
  6.  
  7. if (win):
  8. tft.draw_bmp(0, 16, 'win.bmp')
  9. raise SystemExit

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

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