
Це фрагмент книги Python з нуля, яка допоможе вам навчитися програмуванню з нуля. Ви можете знайти його на Allegro, Empik та в інтернет-книгарнях.
У цьому розділі ми визначимо поведінку "змійки" та її харчування.
snake— список, який зберігає положення елементів "змійки" (де перший елемент — це "хвіст", а останній — "голова");food— позиція "їжі;direction— напрямок руху "змійки";field_size— розмір поля.
direction. Все інше змінюватиметься ніби саме собою. На кожному кроці (скажімо, що 100 мс) "змійка" пересуватиметься у заданому напрямку. Якщо вона натрапить на "їжу", вона подовжиться, а нова "їжа" з’явиться на полі у випадковому місці. Якщо вона натрапить на власний "хвіст", ми повернемося до вихідної точки. Так працюватиме наша "змійка", і це ми реалізуємо в цьому розділі.game_state.py. Всередині нього ми створимо клас GameState, який буде представляти стан нашої гри. Отже, він повинен містити snake, food, direction і field_size.int
class GameState: def __init__(self, snake, direction, food, field_size): self.snake = snake self.direction = direction self.food = food self.field_size = field_size
direction ми представлятимемо послідовними номерами, які відповідатимуть можливим напрямкам. Однією з практик є зберігання таких чисел у спеціальному класі, де всі атрибути є статичними [^302_1]. Викладемо його в окремому файлі direction.py.class Direction: UP = 1 DOWN = 2 LEFT = 3 RIGHT = 4
GameState, нам потрібен метод step, який послідовно робитиме все, що має статися в певний час. Але як ми дізнаємося, чи наша реалізація цієї функції правильна? На це запитання відповідь дасть модульне тестування.step, а перевіркою — оцінка, чи правильне нове положення "змійки".# визначення початкового стану state = GameState( snake=[ Position(4, 0), Position(4, 1), Position(4, 2) ], direction=Direction.DOWN, food=Position(10, 10), field_size=20 ) # виклик функції, яка тестується state.step() # перевірка результату роботи функції expected_state = [ Position(4, 1), Position(4, 2), Position(4, 3) ] self.assertEqual(expected_state, state.snake)

Точка (4, 2) та її околиці.
unittest. Він вимагає, щоб наші тести були методами класу із назвами, які починаються від "test_". У великих проєктах створюється багато класів із тестами, нам достатньо буде одного. Для початку ми напишемо дуже простий тест, який перевіряє, чи працює належним чином функція upper. Він використовує метод assertEqual на self, який порівнює два об’єкти та, у випадку, якщо вони не були ідентичні, виводить відповідну помилку. Тут ми використовуємо його для порівняння результату роботи функції та очікуваного значення. Якщо функція upper працює нормально, тест буде пройдено. Перевірмо. Нам потрібно створити файл game_state_test.py з таким змістом:import unittest class GameStateTest(unittest.TestCase): def test_example(self): self.assertEqual('foo'.upper(), 'FOO')
Щоб тест працював, після назви класу потрібно використати дужки зі значеннямunittest.TestCase. Це функціонал, відомий як успадкування. Крім того, методи, які містять тести, повинні починатися зtest_.



unittest.main().if __name__ == '__main__': unittest.main()
assertEqual має показувати деталі переданих йому значень, що зазвичай допомагає вивчити причини провалу тесту.
import unittest from game_state import GameState from position import Position from direction import Direction class GameStateTest(unittest.TestCase): def test_snake_should_move_right(self): state = GameState( snake=[ Position(1, 2), Position(1, 3), Position(1, 4) ], direction=Direction.RIGHT, food=Position(10, 10), field_size=20 ) state.step() expected_state = [ Position(1, 3), Position(1, 4), Position(2, 4) ] self.assertEqual(expected_state, state.snake) def test_snake_should_move_left(self): state = GameState( snake=[ Position(1, 2), Position(1, 3), Position(1, 4) ], direction=Direction.LEFT, food=Position(10, 10), field_size=20 ) state.step() expected_state = [ Position(1, 3), Position(1, 4), Position(0, 4) ] self.assertEqual(expected_state, state.snake)
__str__ і __repr__ в об’єкті Position ми повинні отримати чітке порівняння, як виглядає "змійка" під час перевірки, та як вона повинна виглядати.
Тести пересування вгору та вниз напиши самостійно.
self.snake = self.snake[1:]
append, а його положення ми визначимо в окремій функції next_head.new_head = self.next_head(self.direction) self.snake.append(new_head)
next_head має повертати нове положення "голови" "змійки". Тому потрібно буде визначити поточне положення "голови". Це останній елемент у змінній snake, тобто snake[-1]. Потім ми повинні створити позицію, зміщену в заданому напрямку. Погляньмо ще раз на сітку координат.
Точка (4, 2) та її околиці.
y ми повинні відняти 1. Якщо внизу — додати 1. Якщо ліворуч, то від змінної x ми повинні відняти 1. Якщо праворуч — додати 1. Ми можемо реалізувати це, використовуючи конструкцію if з elif.# Метод у класі GameState def next_head(self, direction): pos = self.snake[-1] if direction == Direction.UP: return Position(pos.x, pos.y — 1) elif direction == Direction.DOWN: return Position(pos.x, pos.y + 1) elif direction == Direction.RIGHT: return Position(pos.x + 1, pos.y) elif direction == Direction.LEFT: return Position(pos.x — 1, pos.y)
Ця функція, в принципі, може мати власні тести. Однак це не обов’язково, оскільки ми опосередковано перевіряємо її роботу під час тестування руху "змійки".
step.# Метод у класі GameState def step(self): new_head = self.next_head(self.direction) self.snake.append(new_head) self.snake = self.snake[1:]
# Нові методи у класі GameStateTest def test_snake_should_move_up_on_top(self): state = GameState( snake=[ Position(2, 2), Position(2, 1), Position(2, 0) ], direction=Direction.UP, food=Position(10, 10), field_size=20 ) state.step() expected_state = [ Position(2, 1), Position(2, 0), Position(2, 19) ] self.assertEqual(expected_state, state.snake) def test_snake_should_move_right_on_edge(self): state = GameState( snake=[ Position(17, 1), Position(18, 1), Position(19, 1) ], direction=Direction.RIGHT, food=Position(10, 10), field_size=20 ) state.step() expected_state = [ Position(18, 1), Position(19, 1), Position(0, 1) ] self.assertEqual(expected_state, state.snake)
Тестування проходження через нижню та ліву стінки напиши самостійно.
next_head. Якщо після зміни будь-яка точка координат може становити -1, то замість цього вона має бути field_size — 1, тобто за замовчуванням 19. Якби вона мала 20, то становила би 0. Пам’ятаєш оператор залишку від ділення %? Щоб нагадати, як він працює: 123 % 5 буде 3, оскільки найбільша кратність числа 5, менша ніж 123 — 120. Тож залишається 3 як остача від ділення. Що важливо, 20 % 20 == 0, а -1 % 20 == 19. Цей оператор ідеально відповідає нашим потребам. Після додавання або віднімання від позиції x або y ми повинні виконати операцію остачі від ділення на field_size.# Метод у класі GameState def next_head(self, direction): pos = self.snake[-1] if direction == Direction.UP: return Position( pos.x, (pos.y — 1) % self.field_size ) elif direction == Direction.DOWN: return Position( pos.x, (pos.y + 1) % self.field_size ) elif direction == Direction.RIGHT: return Position( (pos.x + 1) % self.field_size, pos.y ) elif direction == Direction.LEFT: return Position( (pos.x — 1) % self.field_size, pos.y )
step, вона має збільшуватися. Ми могли би перевірити це, порівнявши попередню та нову довжину "змійки", але ще точніше буде перевірити, чи новий стан змінної snake відповідає нашим очікуванням.# Новий метод у класі GameStateTest def test_snake_eats_food(self): state = GameState( snake=[ Position(1, 2), Position(2, 2), Position(3, 2) ], direction=Direction.UP, food=Position(3, 1), field_size=20 ) state.step() expected_state = [ Position(1, 2), Position(2, 2), Position(3, 2), Position(3, 1) ] self.assertEqual(expected_state, state.snake)
# Метод у класі GameState def step(self): new_head = self.next_head(self.direction) self.snake.append(new_head) found_food = new_head == self.food if not found_food: self.snake = self.snake[1:]
self.assertEqual(False, state.food in state.snake)
set_random_food_position.# Метод у класі GameState def step(self): new_head = self.next_head(self.direction) self.snake.append(new_head) if new_head == self.food: self.set_random_food_position() else: self.snake = self.snake[1:]
random та його функцією randint, яка дозволяє отримувати випадкове ціле число в певному діапазоні. В якості аргументів ми задамо мінімальне і максимальне значення. Мінімальне значення x та y має бути 0, а максимальне — field_size — 1. Отже, просте визначення випадкового значення може виглядати так:from random import randint # Новий метод у класі GameState def set_random_food_position(self): self.food = Position( randint(0, self.field_size — 1), randint(0, self.field_size — 1) )
while, але можна також просто викликати функцію знову:# Метол у класі GameState def set_random_food_position(self): self.food = Position( randint(0, self.field_size — 1), randint(0, self.field_size — 1) ) if self.food in self.snake: self.set_random_food_position() # або def set_random_food_position(self): search = True while search: self.food = Position( randint(0, self.field_size — 1), randint(0, self.field_size — 1) ) search = self.food in self.snake
game_state.py.# Нові змінні у файлі game_state.py INITIAL_SNAKE = [ Position(1, 2), Position(2, 2), Position(3, 2) ] INITIAL_DIRECTION = Direction.RIGHT
# Новий метод у класі GameStateTest def test_snake_dies(self): state = GameState( snake=[ Position(1, 2), Position(2, 2), Position(3, 2), Position(3, 3), Position(2, 3), ], direction=Direction.UP, food=Position(3, 1), field_size=25 ) state.step() from game_state import INITIAL_SNAKE self.assertEqual(INITIAL_SNAKE, state.snake) self.assertFalse(state.food in state.snake) from game_state import INITIAL_DIRECTION self.assertEqual(INITIAL_DIRECTION, state.direction) self.assertEqual(25, state.field_size)
Імпорт значеньINITIAL_SNAKEтаINITIAL_DIRECTIONможна перемістити на початок файлу, але, якщо значення потрібне лише в одному місці, рекомендується розміщувати імпорт перед його єдиним використанням. Імпорт створює об’єкт, тому найголовніше, щоб він був запущений перед його першим використанням.
step ми вже знаємо нове положення "голови" і положення всіх частин "змійки". Тепер достатньо перевірити, чи ця нова позиція "голови" не збігається з позицією жодної іншої частини "змійки".# Всередині метода step collision = new_head in self.snake
return ми можемо завершити виклик функції step на цьому кроці.# Метод у класі GameState def step(self): new_head = self.next_head(self.direction) collision = new_head in self.snake if collision: self.set_initial_position() return self.snake.append(new_head) if new_head == self.food: self.set_random_food_position() else: self.snake = self.snake[1:]
set_initial_position. Ця функція повинна повернути "змійку" у вихідне положення, задати початковий напрямок і вибрати нове випадкове місце для "їжі". При розміщенні "змійки" за допомогою INITIAL SNAKE варто зробити копію через [:], щоб захистити цю змінну від внутрішніх змін. Без цього наш виклик append у step міг би змінити INITIAL_SNAKE, що призвело б до неправильного скидання гри.# Новий метод у класі GameState def set_initial_position(self): self.snake = INITIAL_SNAKE[:] self.direction = INITIAL_DIRECTION self.set_random_food_position()
GameState:from position import Position from direction import Direction from random import randint INITIAL_SNAKE = [ Position(1, 2), Position(2, 2), Position(3, 2) ] INITIAL_DIRECTION = Direction.RIGHT class GameState: def __init__(self, snake=None, direction=INITIAL_DIRECTION, food=None, field_size=20): if snake is None: snake = INITIAL_SNAKE[:] self.snake = snake self.direction = direction self.field_size = field_size if food is None: self.set_random_food_position() else: self.food = food def set_initial_position(self): self.snake = INITIAL_SNAKE[:] self.direction = INITIAL_DIRECTION self.set_random_food_position() def next_head(self, direction): pos = self.snake[-1] if direction == Direction.UP: return Position( pos.x, (pos.y — 1) % self.field_size ) elif direction == Direction.DOWN: return Position( pos.x, (pos.y + 1) % self.field_size ) elif direction == Direction.RIGHT: return Position( (pos.x + 1) % self.field_size, pos.y ) elif direction == Direction.LEFT: return Position( (pos.x — 1) % self.field_size, pos.y ) def set_random_food_position(self): search = True while search: self.food = Position( randint(0, self.field_size — 1), randint(0, self.field_size — 1) ) search = self.food in self.snake def can_turn(self, direction): new_head = self.next_head(direction) return new_head != self.snake[-2] def step(self): new_head = self.next_head(self.direction) collision = new_head in self.snake if collision: self.set_initial_position() return self.snake.append(new_head) if new_head == self.food: self.set_random_food_position() else: self.snake = self.snake[1:]
