# Сценарий практикума — 60 минут / 6 глав

Возможен один часовой ролик с главами или шесть самостоятельных выпусков. Время предполагает готовые зависимости и PNG; не включает установку и ручной набор каждой служебной строки.

## 1. Окно и космический фон · 00:00–08:00

Реплика: «Начинаем с игрового окна. Сегодня собираем одну игру по шести задачам; после каждого блока сразу проверяем результат.»

В кадре: Покажите готовую игру за 30 секунд. Затем создайте окно, загрузите PNG и подключите draw. Обратите внимание на последнюю строку arcade.run().

### Создай окно

Открой game.py в распакованной мастерской. Вставь этот стартовый блок. Python и Arcade должны быть установлены до начала занятия.

```python

import arcade

WIDTH = 1280
HEIGHT = 800
window = arcade.Window(WIDTH, HEIGHT, "Звезда и Метеор")

```

- WIDTH и HEIGHT задают размеры в пикселях. Заглавные имена обозначают настройки по соглашению.

- arcade.Window создаёт окно. Переменная window позволяет подключить к нему функции.

### Загрузи фон

После создания окна загрузи background.png. Папка assets должна находиться рядом с game.py; запускаем из этой папки проекта.

```python

background = arcade.load_texture("assets/background.png")

```

- load_texture читает PNG. Эту операцию выполняем один раз, до начала игры.

### Нарисуй кадр и запусти

Добавь функцию draw ниже загрузки. Подключения и arcade.run() всегда оставляй внизу файла.

```python

def draw():
    window.clear()
    arcade.draw_texture_rect(background, arcade.LBWH(0, 0, WIDTH, HEIGHT))

window.on_draw = draw
arcade.run()

```

- clear очищает предыдущий кадр; следующий вызов рисует фон.

- LBWH описывает прямоугольник: левый край, нижний край, ширина, высота. Фон занимает всё окно.

- window.on_draw = draw передаёт функцию окну без немедленного вызова. Цикл обновлений запускает arcade.run().

Вопрос: Почему картинку загружаем один раз, а рисуем много раз?

Ответ: Файл достаточно прочитать при подготовке. Каждый новый кадр должен заново показать изображение.

Проверка: В отдельном окне виден фон; в Shell нет ошибки загрузки файла.

Завершение: Проверьте фон и изменённый заголовок. Дальше добавляем персонажа.

Если не работает: FileNotFoundError: проверьте распаковку, assets/background.png и запуск из папки проекта. ModuleNotFoundError: открыт Python без Arcade.

## 2. Добавляем космонавта · 08:00–15:00

Реплика: «Картинка героя уже готова. Наша задача — загрузить её и указать, где рисовать.»

В кадре: Добавьте текстуру и две координаты. Покажите разницу между LBWH для фона и XYWH для персонажа. Вынесите повторяемое рисование в функцию.

### Загрузи PNG и задай позицию

Добавь x и y после настроек. После загрузки фона добавь текстуру hero.

```python

x = 210
y = 235
hero = arcade.load_texture("assets/astronaut.png")

```

- x и y задают центр рисунка; hero хранит текстуру персонажа.

- Прозрачные области PNG позволяют показать фон вокруг космонавта.

### Сделай функцию рисования объекта

Перед draw опиши draw_image. Её же позже используем для звезды и метеора.

```python

def draw_image(texture, x, y, size):
    arcade.draw_texture_rect(texture, arcade.XYWH(x, y, size, size))

```

- texture — изображение; x и y — его центр; size — размер квадратной области.

- Здесь обычная функция Python и прямой вызов Arcade. Все правила остаются в game.py.

### Покажи героя поверх фона

Внутри draw после строки с фоном добавь вызов. Сохрани, закрой старое окно и запусти файл снова.

```python

draw_image(hero, x, y, 132)

```

- Внутри draw у вызова четыре пробела отступа.

- Порядок рисования важен: фон первым, персонаж после него.

Вопрос: Что изменится при увеличении y на 100?

Ответ: Персонаж поднимется на 100 пикселей; горизонтальная позиция останется прежней.

Проверка: Персонаж виден на фоне и меняет положение после изменения x или y.

Завершение: Попросите изменить положение одним присваиванием. Следующий блок свяжет координаты с клавишами.

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

## 3. Плавное управление · 15:00–28:00

Реплика: «Разделяем ввод, движение и рисование. Клавиша задаёт желание игрока, обновление меняет координаты, кадр показывает новое место.»

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

### Запомни нажатия и отпускания

Под настройками создай SPEED = 270 и keys = set(). После draw добавь два обработчика.

```python

def key_pressed(key, modifiers):
    keys.add(key)

def key_released(key, modifiers):
    keys.discard(key)

```

- Arcade передаёт key — код клавиши; modifiers содержит Shift, Ctrl и подобные признаки, здесь они не нужны.

- При удержании клавиша остаётся в keys, при отпускании удаляется. Множество не создаёт дубликатов.

### Обновляй координаты

Добавь update перед строками подключения. Скорость измеряется в пикселях в секунду.

```python

def update(delta_time):
    global x, y
    if arcade.key.RIGHT in keys:
        x += SPEED * delta_time
    if arcade.key.LEFT in keys:
        x -= SPEED * delta_time
    if arcade.key.UP in keys:
        y += SPEED * delta_time
    if arcade.key.DOWN in keys:
        y -= SPEED * delta_time

```

- += прибавляет к текущему значению, -= вычитает. По x двигаемся вправо/влево, по y вверх/вниз.

- Независимые if позволяют держать два направления. В этой базовой версии диагональ быстрее прямого движения.

- keys.add меняет существующее множество; global нужен для присваиваний x и y.

### Ограничь поле

В конце update, после движения, добавь ограничения для центра героя.

```python

x = max(96, min(1184, x))
y = max(171, min(589, y))

```

- min ограничивает сверху, max снизу. Внутри update эти строки имеют отступ четыре пробела.

### Подключи события

Перед arcade.run() добавь подключения. on_draw уже подключён в первом уроке.

```python

window.on_key_press = key_pressed
window.on_key_release = key_released
window.on_update = update
window.on_deactivate = keys.clear

```

- Обработчик ввода меняет набор клавиш, update рассчитывает координаты, draw показывает результат.

- При уходе в другое окно keys.clear сбрасывает клавиши, чтобы персонаж не продолжал движение сам.

Вопрос: Почему движение умножается на delta_time, а не просто на один шаг за кадр?

Ответ: Так скорость привязана к секундам, а не к количеству обновлений на конкретном компьютере.

Проверка: Удержание даёт плавное движение; отпускание останавливает; выйти за поле нельзя.

Завершение: Проверьте все направления и отпускание. Теперь добавим цель и движущуюся опасность.

Если не работает: Нет остановки — проверьте key_released и его подключение. UnboundLocalError — проверьте global. Не ставьте while True вокруг arcade.run().

## 4. Цель и движущееся препятствие · 28:00–38:00

Реплика: «Используем прежний код рисования для двух новых объектов. Отличие метеора — собственная скорость и правило разворота.»

В кадре: Добавьте две текстуры и вызовы draw_image. После проверки сцены включите патруль и дождитесь обоих разворотов.

### Создай второй и третий объекты

После координат героя добавь координаты цели и препятствия; после hero загрузи ещё две текстуры.

```python

star_x, star_y = 1070, 540
meteor_x, meteor_y = 720, 385
meteor_speed = 200
star = arcade.load_texture("assets/star.png")
meteor = arcade.load_texture("assets/meteor.png")

```

- Запись a, b = 1, 2 присваивает двум именам два значения.

- У каждого объекта отдельные координаты. Высота метеора постоянна, меняется только meteor_x.

### Нарисуй оба объекта

Внутри draw после космонавта добавь два вызова уже написанной функции.

```python

draw_image(star, star_x, star_y, 90)
draw_image(meteor, meteor_x, meteor_y, 132)

```

- Меняются текстура, координаты и размер. Новая функция рисования для каждого PNG не требуется.

### Запусти патруль

В update добавь global meteor_x, meteor_speed. В конце функции добавь движение и два разворота.

```python

meteor_x += meteor_speed * delta_time
if meteor_x > 1110:
    meteor_x = 1110
    meteor_speed = -200
elif meteor_x < 340:
    meteor_x = 340
    meteor_speed = 200

```

- Справа скорость становится отрицательной, слева положительной.

- Позицию возвращаем точно на границу, чтобы объект не накапливал выход за неё.

- Внутри update весь этот блок сдвинут на четыре пробела.

Вопрос: Что будет, если у правой границы оставить положительную скорость?

Ответ: Метеор не полетит обратно влево; будет упираться в правую границу.

Проверка: На экране три объекта; метеор разворачивается у обеих границ; звезда остаётся на месте.

Завершение: Убедитесь, что сценой можно управлять и обходить метеор. Добавляем последствия касаний.

Если не работает: Пока касания ничего не делают — проверка будет в следующем блоке. Если метеор не движется, проверьте global и расположение кода внутри update.

## 5. Столкновения и результат · 38:00–50:00

Реплика: «Графическая сцена превращается в игру, когда появляются цель, опасность и конечный результат.»

В кадре: Разберите touches на двух парах координат. Добавьте состояние и ранний выход; отдельно покажите проигрыш и победу.

### Добавь состояние и подпись

Под координатами задай state. После создания окна и загрузки текстур создай надпись message.

```python

state = "playing"
message = arcade.Text("", 640, 710, (255, 226, 136), 30,
                      anchor_x="center", font_name="Arial")

```

- Строки playing, won и lost — наши значения состояния, не команды языка.

- Text создаёт надпись. Аргументы задают текст, позицию, цвет RGB и размер шрифта. anchor_x центрирует по x.

### Опиши проверку близости

Перед draw добавь функцию, которая подходит и для звезды, и для метеора.

```python

def touches(ax, ay, bx, by):
    return abs(ax - bx) < 60 and abs(ay - by) < 60

```

- Сравниваем разницу по двум осям. Если обе меньше 60 пикселей, считаем объекты соприкоснувшимися.

- Это простая прямоугольная область вокруг центров, не контур PNG.

### Выбери исход и останови правила

В update добавь global state. Сразу после global поставь ранний выход, а после движения и патруля — проверки исхода.

```python

if state != "playing":
    return

# После движения и патруля:
if touches(x, y, meteor_x, meteor_y):
    state = "lost"
elif touches(x, y, star_x, star_y):
    state = "won"

```

- После победы или проигрыша update сразу завершится. draw продолжит показывать кадр.

- Опасность проверяем первой: цепочка if/elif выбирает один исход.

### Покажи результат

В конце draw выбери текст и нарисуй подготовленную надпись.

```python

message.text = ""
if state == "won":
    message.text = "Звезда найдена!"
elif state == "lost":
    message.text = "Столкновение."
message.draw()

```

- Меняем свойство text у уже созданной надписи; draw() показывает её на текущем кадре.

- Для проверки двух исходов перезапусти файл. В следующем блоке добавим новую попытку клавишей R.

Вопрос: Почему return стоит в update, а не в draw?

Ответ: Нужно остановить изменение мира, сохранив рисование результата.

Проверка: Касание метеора даёт проигрыш, звезды — победу; после исхода герой и метеор стоят.

Завершение: У игры есть оба исхода. Осталось дать возможность начать новый раунд без перезапуска файла.

Если не работает: Если исход меняется повторно, ранний return стоит ниже проверок. Если надпись не видна, проверьте message.draw() и порядок рисования.

## 6. Перезапуск и проверка игры · 50:00–60:00

Реплика: «Последняя задача — замкнуть цикл: начало, движение, результат, новый раунд. После этого получаем завершённую мини-игру.»

В кадре: Добавьте restart и R. Пройдите весь проверочный маршрут, затем попросите ученика изменить старт или скорость и объяснить, где ещё нужно поправить значения.

### Собери сброс в функцию

Добавь restart перед обработчиком клавиш. Возвращаем старт героя, положение и направление метеора, состояние и набор клавиш.

```python

def restart():
    global x, y, meteor_x, meteor_speed, state
    x, y = 210, 235
    meteor_x = 720
    meteor_speed = 200
    state = "playing"
    keys.clear()

```

- global перечисляет только переменные, которым присваиваем новые значения.

- star_x, star_y и meteor_y не меняются в раунде, поэтому сбрасывать их не требуется.

- keys.clear() убирает ввод от предыдущей попытки.

### Подключи R

Замени прежний key_pressed на этот блок. Остальные подключения окна сохраняются.

```python

def key_pressed(key, modifiers):
    keys.add(key)
    if key == arcade.key.R:
        restart()

```

- R вызывает сброс в ходе игры, после победы и после проигрыша.

- Правила движения повторно не пишем: состояние playing снова разрешает update.

### Проверь цикл целиком

Запусти игру: двигайся, отпусти клавишу, проверь края, столкнись, нажми R, найди звезду, снова нажми R. Затем сверяй свой файл с полным итогом.

- После каждого R начальные координаты и направление должны совпадать.

- Если изменил старт героя или скорость патруля для своей версии, обнови и начальные значения, и restart.

Вопрос: Почему недостаточно вернуть только координаты героя?

Ответ: Метеор, его направление, состояние результата и старый ввод тоже влияют на новый раунд.

Проверка: Оба исхода проверены; R работает после каждого; старая нажатая клавиша не переносится в новую попытку.

Завершение: Покажите итог и перечислите шесть написанных блоков. Домашнее задание — один собственный маршрут или одна новая механика.

Если не работает: Если новая попытка сразу завершена, забыли playing. Если герой летит сам, проверьте keys.clear(). Если меняли старт, сверьте значения в двух местах.
