{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "05573303",
   "metadata": {},
   "source": [
    "# 21-03 · Движение корабля и враги\n",
    "\n",
    "Практика к разделу [«Перемещаем корабль. Создаём и перемещаем врагов»](../../site/chapters/glava-21/21-03-dvizhenie-vragi.html)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "59b0c6fd",
   "metadata": {},
   "source": [
    "## Цель\n",
    "\n",
    "Ограничить движение корабля краями экрана и заставить врагов появляться и спускаться вниз."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2bd263a3",
   "metadata": {},
   "source": [
    "## Про игровой цикл в этом ноутбуке\n",
    "\n",
    "В обычном `.py`-файле игровой цикл — это `while rabotaet:`, зависящий от событий пользователя (закрытие окна, нажатия клавиш). В автоматически выполняемом ноутбуке некому создавать такие события, поэтому здесь мы прогоняем фиксированное число кадров через `for kadr in range(N):` — логика каждого отдельного кадра (движение, столкновения, отрисовка, `clock.tick()`) при этом точно такая же, как в настоящей игре из `projects/pygame/space-shooter/space_shooter.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d9d83fb9",
   "metadata": {},
   "source": [
    "## Рабочий пример"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "00716889",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-14T20:59:32.013643Z",
     "iopub.status.busy": "2026-08-14T20:59:32.013528Z",
     "iopub.status.idle": "2026-08-14T20:59:32.160642Z",
     "shell.execute_reply": "2026-08-14T20:59:32.159606Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "pygame-ce 2.5.8 (SDL 2.32.10, Python 3.14.6)\n"
     ]
    }
   ],
   "source": [
    "import random\n",
    "\n",
    "import pygame\n",
    "\n",
    "SHIRINA, VYSOTA = 500, 600\n",
    "FPS = 60\n",
    "\n",
    "KORABL_SHIRINA, KORABL_VYSOTA = 50, 40\n",
    "KORABL_SKOROST = 6\n",
    "\n",
    "PULYA_SHIRINA, PULYA_VYSOTA = 4, 12\n",
    "PULYA_SKOROST = 9\n",
    "\n",
    "VRAG_SHIRINA, VRAG_VYSOTA = 40, 30\n",
    "VRAG_SKOROST = 2\n",
    "INTERVAL_POYAVLENIYA_VRAGA = 45\n",
    "\n",
    "BELYJ = (255, 255, 255)\n",
    "CHERNYJ = (10, 10, 20)\n",
    "ZELYONYJ = (80, 220, 120)\n",
    "KRASNYJ = (230, 60, 60)\n",
    "ZHYOLTYJ = (240, 220, 80)\n",
    "\n",
    "pygame.init()\n",
    "screen = pygame.display.set_mode((SHIRINA, VYSOTA))\n",
    "pygame.display.set_caption(\"Космический шутер\")\n",
    "clock = pygame.time.Clock()\n",
    "shrift = pygame.font.SysFont(None, 32)\n",
    "shrift_bolshoj = pygame.font.SysFont(None, 64)\n",
    "\n",
    "\n",
    "def novaya_igra():\n",
    "    return {\n",
    "        \"korabl\": pygame.Rect(\n",
    "            SHIRINA // 2 - KORABL_SHIRINA // 2,\n",
    "            VYSOTA - KORABL_VYSOTA - 20,\n",
    "            KORABL_SHIRINA,\n",
    "            KORABL_VYSOTA,\n",
    "        ),\n",
    "        \"puli\": [],\n",
    "        \"vragi\": [],\n",
    "        \"schet\": 0,\n",
    "        \"kadrov_do_vraga\": INTERVAL_POYAVLENIYA_VRAGA,\n",
    "        \"igra_okonchena\": False,\n",
    "    }\n",
    "\n",
    "\n",
    "def obrabotat_klavishi(state, klavishi):\n",
    "    korabl = state[\"korabl\"]\n",
    "    if klavishi[pygame.K_LEFT]:\n",
    "        korabl.x -= KORABL_SKOROST\n",
    "    if klavishi[pygame.K_RIGHT]:\n",
    "        korabl.x += KORABL_SKOROST\n",
    "    korabl.x = max(0, min(korabl.x, SHIRINA - KORABL_SHIRINA))\n",
    "\n",
    "\n",
    "def vystrelit(state):\n",
    "    korabl = state[\"korabl\"]\n",
    "    pulya = pygame.Rect(\n",
    "        korabl.centerx - PULYA_SHIRINA // 2,\n",
    "        korabl.top,\n",
    "        PULYA_SHIRINA,\n",
    "        PULYA_VYSOTA,\n",
    "    )\n",
    "    state[\"puli\"].append(pulya)\n",
    "\n",
    "\n",
    "def sozdat_vraga():\n",
    "    x = random.randint(0, SHIRINA - VRAG_SHIRINA)\n",
    "    return pygame.Rect(x, -VRAG_VYSOTA, VRAG_SHIRINA, VRAG_VYSOTA)\n",
    "\n",
    "\n",
    "def obnovit_igru(state):\n",
    "    if state[\"igra_okonchena\"]:\n",
    "        return\n",
    "\n",
    "    for pulya in state[\"puli\"]:\n",
    "        pulya.y -= PULYA_SKOROST\n",
    "    state[\"puli\"] = [p for p in state[\"puli\"] if p.bottom > 0]\n",
    "\n",
    "    state[\"kadrov_do_vraga\"] -= 1\n",
    "    if state[\"kadrov_do_vraga\"] <= 0:\n",
    "        state[\"vragi\"].append(sozdat_vraga())\n",
    "        state[\"kadrov_do_vraga\"] = INTERVAL_POYAVLENIYA_VRAGA\n",
    "\n",
    "    for vrag in state[\"vragi\"]:\n",
    "        vrag.y += VRAG_SKOROST\n",
    "\n",
    "    novye_puli = []\n",
    "    novye_vragi = list(state[\"vragi\"])\n",
    "    for pulya in state[\"puli\"]:\n",
    "        popala = False\n",
    "        for vrag in list(novye_vragi):\n",
    "            if pulya.colliderect(vrag):\n",
    "                novye_vragi.remove(vrag)\n",
    "                state[\"schet\"] += 10\n",
    "                popala = True\n",
    "                break\n",
    "        if not popala:\n",
    "            novye_puli.append(pulya)\n",
    "    state[\"puli\"] = novye_puli\n",
    "    state[\"vragi\"] = novye_vragi\n",
    "\n",
    "    for vrag in state[\"vragi\"]:\n",
    "        if vrag.bottom >= VYSOTA or vrag.colliderect(state[\"korabl\"]):\n",
    "            state[\"igra_okonchena\"] = True\n",
    "            break\n",
    "\n",
    "\n",
    "def narisovat(state):\n",
    "    screen.fill(CHERNYJ)\n",
    "    pygame.draw.rect(screen, ZELYONYJ, state[\"korabl\"])\n",
    "    for pulya in state[\"puli\"]:\n",
    "        pygame.draw.rect(screen, ZHYOLTYJ, pulya)\n",
    "    for vrag in state[\"vragi\"]:\n",
    "        pygame.draw.rect(screen, KRASNYJ, vrag)\n",
    "\n",
    "    tablo = shrift.render(f\"Счёт: {state['schet']}\", True, BELYJ)\n",
    "    screen.blit(tablo, (10, 10))\n",
    "\n",
    "    if state[\"igra_okonchena\"]:\n",
    "        nadpis = shrift_bolshoj.render(\"ИГРА ОКОНЧЕНА\", True, BELYJ)\n",
    "        rect = nadpis.get_rect(center=(SHIRINA // 2, VYSOTA // 2))\n",
    "        screen.blit(nadpis, rect)\n",
    "\n",
    "    pygame.display.flip()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "2db7b064",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-14T20:59:32.162297Z",
     "iopub.status.busy": "2026-08-14T20:59:32.161949Z",
     "iopub.status.idle": "2026-08-14T20:59:32.955750Z",
     "shell.execute_reply": "2026-08-14T20:59:32.955255Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Корабль после 50 кадров движения вправо: Rect(450, 540, 50, 40)\n",
      "Врагов появилось: 1\n"
     ]
    }
   ],
   "source": [
    "state = novaya_igra()\n",
    "\n",
    "# симулируем: стрелка вправо зажата 50 кадров подряд\n",
    "for kadr in range(50):\n",
    "    klavishi = {pygame.K_LEFT: False, pygame.K_RIGHT: True}\n",
    "    obrabotat_klavishi(state, klavishi)\n",
    "    obnovit_igru(state)\n",
    "    narisovat(state)\n",
    "    clock.tick(FPS)\n",
    "\n",
    "print(\"Корабль после 50 кадров движения вправо:\", state[\"korabl\"])\n",
    "print(\"Врагов появилось:\", len(state[\"vragi\"]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dfcd020f",
   "metadata": {},
   "source": [
    "## Проверка результата"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "47715fd8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-14T20:59:32.956780Z",
     "iopub.status.busy": "2026-08-14T20:59:32.956664Z",
     "iopub.status.idle": "2026-08-14T20:59:32.959317Z",
     "shell.execute_reply": "2026-08-14T20:59:32.958824Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Верно: корабль остановился у правого края экрана, не выйдя за его пределы.\n",
      "Найдено врагов: 1\n"
     ]
    }
   ],
   "source": [
    "assert state[\"korabl\"].x == SHIRINA - KORABL_SHIRINA, \"корабль должен упереться в правый край\"\n",
    "print(\"Верно: корабль остановился у правого края экрана, не выйдя за его пределы.\")\n",
    "\n",
    "assert len(state[\"vragi\"]) >= 1, \"за 50 кадров должен появиться хотя бы один враг (интервал 45)\"\n",
    "print(f\"Найдено врагов: {len(state['vragi'])}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b032660",
   "metadata": {},
   "source": [
    "## Задание ★ Базовая практика\n",
    "\n",
    "Прогоните ещё 45 кадров без движения корабля и убедитесь, что появился второй враг."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "55f6466f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-14T20:59:32.960396Z",
     "iopub.status.busy": "2026-08-14T20:59:32.960281Z",
     "iopub.status.idle": "2026-08-14T20:59:33.022110Z",
     "shell.execute_reply": "2026-08-14T20:59:33.021466Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Врагов теперь: 2\n",
      "Верно: появился второй враг.\n"
     ]
    }
   ],
   "source": [
    "for kadr in range(45):\n",
    "    klavishi = {pygame.K_LEFT: False, pygame.K_RIGHT: False}\n",
    "    obrabotat_klavishi(state, klavishi)\n",
    "    obnovit_igru(state)\n",
    "    narisovat(state)\n",
    "\n",
    "print(\"Врагов теперь:\", len(state[\"vragi\"]))\n",
    "assert len(state[\"vragi\"]) >= 2\n",
    "print(\"Верно: появился второй враг.\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Cartesian Python 3.14",
   "language": "python",
   "name": "cartesian-python314"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.14.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
