{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "4c02b15a",
   "metadata": {},
   "source": [
    "# 21-08 · Полная игра целиком\n",
    "\n",
    "Практика к разделу [«Первая рабочая версия игры»](../../site/chapters/glava-21/21-08-polnyj-kod-itogi.html). Ячейка ниже собирает вместе весь код разделов 21.1–21.7."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3c5088e",
   "metadata": {},
   "source": [
    "## Цель\n",
    "\n",
    "Прогнать игру много кадров подряд и убедиться, что движение корабля, появление врагов, отрисовка и завершение игры работают вместе, как единое целое."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac707f78",
   "metadata": {},
   "source": [
    "## Про игровой цикл в этом ноутбуке\n",
    "\n",
    "В обычном `.py`-файле игровой цикл — это `while rabotaet:`, зависящий от событий пользователя (закрытие окна, нажатия клавиш). В автоматически выполняемом ноутбуке некому создавать такие события, поэтому здесь мы прогоняем фиксированное число кадров через `for kadr in range(N):` — логика каждого отдельного кадра (движение, столкновения, отрисовка) при этом точно такая же, как в настоящей игре из `projects/pygame/space-shooter/space_shooter.py`. Скорость по-прежнему задана в пикселях в секунду (px/s), а не в пикселях за кадр — но чтобы результат ноутбука не зависел от того, насколько быстро выполняется код на конкретном компьютере, `dt` здесь берётся не из `clock.tick()`, а фиксируется заранее как `1 / FPS`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b53b27ff",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "import pygame\n",
    "\n",
    "SHIRINA, VYSOTA = 480, 720\n",
    "FPS = 60\n",
    "\n",
    "KORABL_SHIRINA, KORABL_VYSOTA = 44, 44\n",
    "KORABL_SKOROST = 260.0   # px/s\n",
    "\n",
    "PULYA_SHIRINA, PULYA_VYSOTA = 6, 18\n",
    "PULYA_SKOROST = 560.0   # px/s\n",
    "\n",
    "VRAG_SHIRINA, VRAG_VYSOTA = 32, 28\n",
    "VRAG_SKOROST = 150.0                # px/s\n",
    "INTERVAL_POYAVLENIYA_VRAGA = 0.75   # секунд между новыми врагами\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",
    "    korabl = pygame.Rect(\n",
    "        SHIRINA // 2 - KORABL_SHIRINA // 2,\n",
    "        VYSOTA - KORABL_VYSOTA - 20,\n",
    "        KORABL_SHIRINA,\n",
    "        KORABL_VYSOTA,\n",
    "    )\n",
    "    return {\n",
    "        \"korabl\": korabl,\n",
    "        \"korabl_x\": float(korabl.x),\n",
    "        \"puli\": [],\n",
    "        \"vragi\": [],\n",
    "        \"schet\": 0,\n",
    "        \"vremya_do_vraga\": INTERVAL_POYAVLENIYA_VRAGA,\n",
    "        \"igra_okonchena\": False,\n",
    "    }\n",
    "\n",
    "\n",
    "def obrabotat_klavishi(state, klavishi, dt):\n",
    "    napravlenie = klavishi[pygame.K_RIGHT] - klavishi[pygame.K_LEFT]\n",
    "    korabl_x = state[\"korabl_x\"] + napravlenie * KORABL_SKOROST * dt\n",
    "    state[\"korabl_x\"] = max(0.0, min(korabl_x, SHIRINA - KORABL_SHIRINA))\n",
    "    state[\"korabl\"].x = round(state[\"korabl_x\"])\n",
    "\n",
    "\n",
    "def vystrelit(state):\n",
    "    korabl = state[\"korabl\"]\n",
    "    pulya_rect = pygame.Rect(\n",
    "        korabl.centerx - PULYA_SHIRINA // 2,\n",
    "        korabl.top,\n",
    "        PULYA_SHIRINA,\n",
    "        PULYA_VYSOTA,\n",
    "    )\n",
    "    state[\"puli\"].append({\"rect\": pulya_rect, \"y\": float(pulya_rect.y)})\n",
    "\n",
    "\n",
    "def sozdat_vraga():\n",
    "    x = random.randint(0, SHIRINA - VRAG_SHIRINA)\n",
    "    return {\"rect\": pygame.Rect(x, -VRAG_VYSOTA, VRAG_SHIRINA, VRAG_VYSOTA), \"y\": float(-VRAG_VYSOTA)}\n",
    "\n",
    "\n",
    "def obnovit_igru(state, dt):\n",
    "    if state[\"igra_okonchena\"]:\n",
    "        return\n",
    "\n",
    "    for pulya in state[\"puli\"]:\n",
    "        pulya[\"y\"] -= PULYA_SKOROST * dt\n",
    "        pulya[\"rect\"].y = round(pulya[\"y\"])\n",
    "    state[\"puli\"] = [p for p in state[\"puli\"] if p[\"rect\"].bottom > 0]\n",
    "\n",
    "    state[\"vremya_do_vraga\"] -= dt\n",
    "    if state[\"vremya_do_vraga\"] <= 0.0:\n",
    "        state[\"vragi\"].append(sozdat_vraga())\n",
    "        state[\"vremya_do_vraga\"] += INTERVAL_POYAVLENIYA_VRAGA\n",
    "\n",
    "    for vrag in state[\"vragi\"]:\n",
    "        vrag[\"y\"] += VRAG_SKOROST * dt\n",
    "        vrag[\"rect\"].y = round(vrag[\"y\"])\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[\"rect\"].colliderect(vrag[\"rect\"]):\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[\"rect\"].bottom >= VYSOTA or vrag[\"rect\"].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[\"rect\"])\n",
    "    for vrag in state[\"vragi\"]:\n",
    "        pygame.draw.rect(screen, KRASNYJ, vrag[\"rect\"])\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": "markdown",
   "id": "fa7e1fc1",
   "metadata": {},
   "source": [
    "## Полная симуляция — движение и появление врагов без стрельбы\n",
    "\n",
    "Чтобы результат был предсказуемым (враги не уничтожаются случайными попаданиями), в этом прогоне корабль просто двигается, а один из врагов долетает до низа экрана и завершает игру — ровно так, как описано в разделе «Когда враг уничтожает корабль»."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dadee6bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "state = novaya_igra()\n",
    "random.seed(3)\n",
    "dt = 1 / FPS\n",
    "\n",
    "# При VRAG_SKOROST = 150 px/s и dt = 1/60 с враг проходит около 2.5 px за кадр,\n",
    "# то есть все 720 px высоты экрана — примерно за 288 кадров после появления.\n",
    "# Первый враг появляется на 45-м кадре (интервал 0.75 с = 45 кадров при 60 FPS),\n",
    "# так что 500 кадров дают уверенный запас.\n",
    "for kadr in range(500):\n",
    "    klavishi = {\n",
    "        pygame.K_LEFT: kadr % 20 < 10,\n",
    "        pygame.K_RIGHT: kadr % 20 >= 10,\n",
    "    }\n",
    "    obrabotat_klavishi(state, klavishi, dt)\n",
    "    obnovit_igru(state, dt)\n",
    "    narisovat(state)\n",
    "    clock.tick(FPS)\n",
    "    if state[\"igra_okonchena\"]:\n",
    "        break\n",
    "\n",
    "print(\"Кадров прогнано:\", kadr + 1)\n",
    "print(\"Финальный счёт:\", state[\"schet\"])\n",
    "print(\"Игра окончена:\", state[\"igra_okonchena\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "81a9519c",
   "metadata": {},
   "source": [
    "## Проверка результата"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "106d38b9",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert state[\"igra_okonchena\"] is True, \"за 500 кадров хотя бы один враг должен долететь до низа экрана\"\n",
    "assert kadr + 1 <= 500\n",
    "print(f\"Верно: игра завершилась на кадре {kadr + 1} со счётом {state['schet']}.\")\n",
    "pygame.quit()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Cartesian Python 3.14",
   "language": "python",
   "name": "cartesian-python314"
  },
  "language_info": {
   "name": "python",
   "version": "3.14.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
