Вероятно для вас это будет большой сюрприз, но на текущий момент вы уже знаете всё, что нужно для того, чтобы написать эту игру. Предыдущие уроки нам дали достаточно материала для того, чтобы мы написали то, о чем говорили в интро текущего курса, поэтому этот урок посвящен геймдизайну.
Мы будем прорабатывать именно момент того, как работает игра и как она выглядит на программном уровне. И практикой к этому уроку будет уже создание непосредственно самой игры.
Правила
Давайте еще раз посмотрим на то, как устроена наша игра.
Начинаем бой!
Игрок 'John' применил 'Прохладный чыонг-бонг рыка'
против 'Ada' и нанес урон '3'
Игрок 'Ada' применил 'Воздушный змей клеветы'
против 'John' и нанес урон '1'
Игрок 'John' применил 'Проказливый рубитель крови'
против 'Ada' и нанес урон '2'
Ada был убит
Итак, у нас есть начало боя, у нас есть конец боя в котором кто-то умирает и 2 игрока по очереди наносят урон друг другу. При этом игра включает в себя какой-то набор карт, который кстати говоря бесконечен (карты никогда не заканчиваются). Фактически мы просто создаем некие карты в которых описываем какой будет урон и после этого они рандомно применяются к конкретным игрокам для того, чтобы нанести этот урон. Карты – это просто список из которого рандомно выбирается какая-то карта и применяется. Это означает что в следующий раз может быть выбрана та же самая карта. Все зависит от того, как работает алгоритм выбора.
Test Driven Development
Давайте начнём с тестов и подумаем о том, а как игра может вообще выглядеть и что нужно реализовать в коде для того, чтобы она заработала.
Первое, что нам нужно – это список карт. Выше видно, что список карт cards – это действительно список с точки зрения структур, которые у нас есть.
Каждый элемент этого списка является парой, которая состоит из 2х элементов: первый элемент пары – это имя карты, второй элемент – это функция, которая внутри себя содержит урон, который будет нанесён.
Обратите внимание, что этот урон не содержит знака, то есть это не какая-то отрицательная величина, это именно то число, которое будет вычтено из жизни игрока к которому этот урон применяется.
Почему здесь функция, но при этом просто конкретное число? Дело в том, что в будущем мы обязательно расширим нашу функциональность и урон будет не статический, как сейчас (когда карта наносит какую-то конкретную цифру). Например, текущая карта наносит урон 6 очков.
Но карта может быть более хитрая. Например, она может брать текущее здоровье и на основе него вычислять какой урон нанести. Именно поэтому мы сразу это делаем функцией, потому что это достаточно очевидная вещь, которую в этой игре стоит вводить (тем более так было задумано изначально). Конечно, мы могли бы просто обойтись числом, но нам в конце концов пришлось бы вводить тут функцию.
После того, как мы создали список этих карт (карта может быть и одна, это не имеет никакого значения), мы вызываем функцию make из нашего пакета hexlet-card-game, который мы и будем разрабатывать.
make – это некая функция, которую сейчас мы будем использовать. Она принимает на вход список карт и создаёт игру. После этого мы вызываем game и передаем туда имена наших игроков и на этом все, больше ничего делать не надо. Игра идёт автоматически, то есть мы сами ими не управляем.
Естественно в реальной жизни у нас вызывались бы ходы по очереди, а после этого ожидалось бы какое-то действие от пользователя и мы бы его применяли. В нашем случае это не нужно, поэтому функция game проводит автоматическую игру и в конце концов возвращает log игры.
Здесь есть очень важный момент, который к тому же еще и очень интересный. То, что вы видели до этого выглядело, как будто мы просто печатаем на экран то, что происходит внутри игры и game мог бы так работать (внутри после каждого хода мы бы печатали что-то на экран). Но если вспомнить наш курс Основы программирования, то там был очень важный урок посвящённый чистым функциям и важности отсутствия побочных эффектов.
Печать на экран – это побочный эффект. Это автоматически означает, что вы не можете безопасно запустить 2 раза подряд (как минимум) эту функцию game.
Во-вторых, вы никак не контролируете этот вывод и вообще не можете, например, банально протестировать функцию и узнать что происходит. Конечно, можно технически перехватывать вывод, который идет на экран, но это совершенно неправильный способ тестировать софт. Он очень не надёжный. Это связано с тем, что вам придётся парсить строчки и проверять конкретное содержимое внутри них, что совершенно не хорошо.
Гораздо более правильный подход и способ работы здесь – это не печатать на экран результаты каждого хода, а фактически формировать некий log, который представляет из себя список, внутри которого содержится вся необходимая информация связанная с текущим ходом.
Во-первых, это позволяет нам, как минимум, гораздо легче и удобнее анализировать, то что происходит.
Во-вторых, у нас отсутствуют побочные эффекты и функция всегда возвращает некий чистый результат, то есть она сама по себе является чистой, что очень хорошо. Мы можем её повторно запускать, безопасно анализировать и такой код проще всего тестируется.
Ниже мы можем посмотреть первый тест. Самый очевидный и простой – это проверка длины лога, то что он равен 5.
Наверно проще данного теста не написать. Ну может быть только проверить, что log является списком, но это не нужно, потому что это по интерфейсу и так должно быть очевидно. В любом случае здесь бы упало с ошибкой, если бы log не был списком.
Итак, у нас получилось 5 ходов, давайте же теперь посмотрим какие это ходы.
Ходы
Здесь мы видим следующий набор тестов, который тоже должен присутствовать (и он присутствует) в наших тестах, в котором мы извлекаем из лога конкретные шаги (элементы) и проверяем, что происходит на каждом шаге.
Перед тем, как мы посмотрим, непосредственно, как происходит сравнение и что внутри, давайте оценим из чего состоит каждый шаг.
log – это набор шагов, который приводит к завершению игры. Каждый шаг – это пара, внутри которой 2 элемента.
Первый элемент – тоже пара, потому что у нас пока только пары для того, чтобы делать какие-то сложно-составные структуры.
Второй – это message. Message – это как раз то самое сообщение, которое мы видели в самом начале, что кто-то кого-то убил, то есть это просто текстовое сообщение.
Первый элемент (который пара) содержит здоровье первого игрока и здоровье второго игрока. Это нам нужно как раз для того, чтобы мы могли оценивать “а правильно ли работает логика нашей программы?” и мы это используем в тестах.
Теперь давайте посмотрим, как мы это используем. У нас есть функция get, которая определена в пакете для работы со списками. Она извлекает элемент по индексу и здесь мы её просим дать нам первый элемент.
Ну и соответственно второй, третий, четвёртый, пятый, то есть как раз все 5 элементов.
А почему их 5? Если мы вспомним был тест, который как раз проверяет, что их 5.
Это связано с тем, что текущий уровень жизни у всех мы выставили равным 10, а damage (так называемый урон) у нас равен цифре 6. И давайте теперь посмотрим, что происходит в таком случае.
В начале, когда происходит старт игры, мы получаем message, который называется начало игры и первый элемент в паре, который является парой со здоровьем по умолчанию, то есть это 10 10. В log мы складываем не сам ход, а начальное состояние игры.
Как мы извлекаем здоровье?
Из step берем car (это первый элемент) и просто превращаем его в строчку, для того, чтобы сравнить и используем assert.equal, который проверяет, что левое значение должно совпадать с правым. То есть здесь слева actual – то, что пришло на самом деле, а справа expected (10, 10) – то, что мы ожидаем. И таким вот образом мы сравниваем каждый элемент.
Теперь мы можем сосредоточиться только на части expected (10, 10) и посмотреть, как изменяются жизни в нашей текущей игре.
Сначала у обоих игроков по 10, после этого применяется урон, который был равен 6 ко второму игроку и соответственно мы получаем 10, 4. После этого, поскольку карта у нас одна и её урон 6, она применяется к первому игроку – мы получаем 4, 4. Затем снова применяется ко второму и у нас получается 4, -2, это 4-й шаг. На 5 шаге видно, что здесь абсолютно тот же самый вывод, но почему? Потому что 5-й шаг, так же как и 1-й – это специальный шаг, в котором пишется сообщение, что кто-то был убит и здоровье показывается ровно такое какое было на предыдущем шаге, потому что здесь уже никому не наносится урон и мы просто фиксируем некое состояние, в котором закончилась наша игра.
Свойства
Какими свойствами обладает наш дизайн, то что мы сделали и написали и какими свойствами обладает наш тестовый пакет?
- Интерфейс – одна функция (make)
- Автоматическая игра
- Логика покрыта тестами
Во-первых, весь интерфейс – это по сути одна функция make, то есть публичный интерфейс, в котором происходит работа, который в свою очередь генерирует функцию game и она уже проигрывает нам какую-то игру.
Ранее также говорилось, что игра автоматическая, у нас нет никакой возможности манипулировать ходом игры просто потому, что это не нужно. На текущий момент это отвлечёт нас от самой задачи и слишком сильно её усложнит.
Ну и как мы заметили логика покрыта тестами. То есть после того, как мы пишем эти тесты мы уже знаем какой у нас будет дизайн, мы легко можем запускать, перезапускать и дописывать наш код и самое главное, что в будущем эти тесты будут нам постоянно помогать. После любого рефакторинга, после любого изменения – мы уже всегда гарантированно сможем проверить, что результат такой, как мы ожидаем.
<!DOCTYPE html>
<html class="h-100" data-bs-theme="light" data-mantine-color-scheme="light" lang="ru" prefix="og: https://ogp.me/ns#">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<link crossorigin="true" href="https://cdn.hexlet.io" rel="preconnect">
<link href="https://mc.yandex.ru" rel="preconnect">
<meta content="aa2vrdtq64dub8knuf83lwywit311w" name="facebook-domain-verification">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/manifest.webmanifest" rel="manifest">
<script>
//<![CDATA[
window.gon={};gon.ym_counter="25559621";gon.is_bot=true;gon.applications={};gon.current_user={"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26 17:15:08 UTC","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false};gon.token="LKmLq43EP5owtY6N5jy0jOPmhtN5PXHDDM9LX7qL0jvDeECcf7qS-ob2qhXqM0T7I--reXEKj2GxL9EL6Iw1VQ";gon.locale="ru";gon.language="ru";gon.theme="light";gon.rails_env="production";gon.mobile=false;gon.google={"analytics_key":"UA-1360700-51","optimize_key":"GTM-5QDVFPF"};gon.captcha={"google_v3_site_key":"6LenGbgZAAAAAM7HbrDbn5JlizCSzPcS767c9vaY","yandex_site_key":"ysc1_Vyob5ZPPUdPBsu0ykt8bVFdzsfpoVjQChLGl2b4g19647a89","verification_failed":null};gon.social_signin=false;gon.typoreporter_google_form_id="1FAIpQLSeibfGq-KvWQ2Fyru-zkFFRVTLBuzXAHAoEyN1p49FtDmNoNA";
//]]>
</script>
<meta charset="utf-8">
<title>Игровой дизайн: карточный бой | JS: Программирование, управляемое данными</title>
<meta name="description" content="Игровой дизайн: карточный бой / JS: Программирование, управляемое данными: Продумать игровой дизайн нашей карточный игры: как работает игра, как выглядит структура программы и как устроен процесс игры. Проработать поведение программы через тесты.">
<link rel="canonical" href="https://ru.hexlet.io/courses/ddp/lessons/game_design/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Игровой дизайн: карточный бой">
<meta property="og:title" content="JS: Программирование, управляемое данными">
<meta property="og:description" content="Игровой дизайн: карточный бой / JS: Программирование, управляемое данными: Продумать игровой дизайн нашей карточный игры: как работает игра, как выглядит структура программы и как устроен процесс игры. Проработать поведение программы через тесты.">
<meta property="og:url" content="https://ru.hexlet.io/courses/ddp/lessons/game_design/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="RGvmSKWXGmZVW4LFawER1_7oXqwWfGHLzNnZCPnE2Nyrui1_V-m3BuMYpl1nDuGgPuFzBh5Ln2lxOUNcq8M_sg" />
<script src="/vite/assets/inertia-INZxX8jp.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/preload-helper-BJ4cLWpC.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ahoy-DrlRQ-1D.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/analytics-6pOtQ3OW.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Surface-DL2bpZA-.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/extends-C-EagtpE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/inheritsLoose-BBd-DCVI.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/objectWithoutPropertiesLoose-DRHXDhjp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/index.esm-DAqKOkZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Button-CGPUux8l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/CloseButton-D1euiPao.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Group-BX48WcuU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Loader-BQEY8g6v.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Modal-Cy3HByv7.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/OptionalPortal-1Hza5P2w.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Stack-CtjJzfw4.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Textarea-Ck64llAy.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/DirectionProvider-Dc9zdUke.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/events-DJQOhap0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-reduced-motion-D2owz4wa.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-disclosure-zKtK5W1r.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-hotkeys-Cnc_Rwkb.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/random-id-DOQyszCZ.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/exports-C_MrNx_T.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<link rel="stylesheet" href="/vite/assets/application-BqhCP46M.js" />
<script src="/vite/assets/application-Df9RExpe.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/autocomplete-VMNbxKGl.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/createPopper-C3aM9r1M.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/js.cookie-D1-O8zkX.js" as="script" crossorigin="anonymous"><link rel="stylesheet" href="/vite/assets/application-C8HjmMaq.css" media="screen" />
<script>
window.ym = function(){(ym.a=ym.a||[]).push(arguments)};
window.addEventListener('load', function() {
setTimeout(function() {
ym.l = 1*new Date();
ym(window.gon.ym_counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
// Загружаем скрипт
var k = document.createElement('script');
k.async = 1;
k.src = 'https://mc.yandex.ru/metrika/tag.js';
document.head.appendChild(k);
ym(window.gon.ym_counter, 'getClientID', function(clientID) {
window.ymClientId = clientID;
});
}, 1500);
});
</script>
<!-- Google Tag Manager - deferred -->
<script>
// dataLayer stub сразу — пуши работают до загрузки скрипта
window.dataLayer = window.dataLayer || [];
// Сам скрипт — отложенно после load
window.addEventListener('load', function() {
setTimeout(function() {
dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
var j = document.createElement('script');
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-WK88TH';
document.head.appendChild(j);
}, 1500);
});
</script>
<!-- End Google Tag Manager -->
</head>
<body>
<noscript>
<div>
<img alt="" src="https://mc.yandex.ru/watch/25559621" style="position:absolute; left:-9999px;">
</div>
</noscript>
<header class="sticky-top bg-body">
<nav class="navbar navbar-expand-lg">
<div class="container-xxl">
<a class="navbar-brand" href="/"><img alt="Логотип Хекслета" height="24" src="https://ru.hexlet.io/vite/assets/logo_ru_light-BpiEA1LT.svg" width="96">
</a><button aria-controls="collapsable" aria-expanded="false" aria-label="Меню" class="navbar-toggler border-0 mb-0 mt-1" data-bs-target="#collapsable" data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsable">
<ul class="navbar-nav mb-lg-0 mt-lg-1">
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
Все курсы
<span class="bi bi-chevron-down align-middle ms-1"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item d-flex py-2" href="/courses"><div class="fw-bold me-auto">Все что есть</div>
<div class="text-muted">117</div>
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные категории</b>
</li>
<li>
<a class="dropdown-item py-2" href="/courses_devops">Курсы по DevOps
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_data_analytics">Курсы по аналитике данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_programming">Курсы по программированию
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_testing">Курсы по тестированию
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные курсы</b>
</li>
<li>
<a class="dropdown-item py-2" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/go">Go-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/java">Java-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/python">Python-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/qa-auto-engineer-java">Автоматизатор тестирования на Java
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/data-analytics">Аналитик данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/frontend">Фронтенд-разработчик
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
О Хекслете
<span class="bi bi-chevron-down align-middle"></span>
</button>
<ul class="dropdown-menu bg-body">
<li>
<a class="dropdown-item py-2" href="/pages/about">О нас
</a></li>
<li>
<a class="dropdown-item py-2" href="/blog">Блог
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/hse-research" role="button">Результаты (Исследование)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://career.hexlet.io" role="button">Хекслет Карьера
</span></li>
<li>
<a class="dropdown-item py-2" href="/testimonials">Отзывы студентов
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://t.me/hexlet_help_bot" role="button">Поддержка (В ТГ)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/referal-program/?promo_creative=priglasite-druzei&promo_name=referal-program&promo_position=promo_position&promo_start=010724&promo_type=link" role="button">Реферальная программа
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/certificate" role="button">Подарочные сертификаты
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://hh.ru/employer/4307094" role="button">Вакансии
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://b2b.hexlet.io" data-target="_blank" role="button">Компаниям
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexly.ru/" data-target="_blank" role="button">Колледж
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexlyschool.ru/" data-target="_blank" role="button">Частная школа
</span></li>
</ul>
</li>
<li><a class="nav-link" href="/subscription/new">Подписка</a></li>
</ul>
<ul class="navbar-nav flex-lg-row align-items-lg-center gap-2 ms-auto">
<li>
<a class="nav-link" aria-label="Переключить тему" href="/theme/switch?new_theme=dark"><span aria-hidden="true" class="bi bi-moon"></span>
</a></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="/u/new" role="button"><span>Регистрация</span>
</span></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="https://ru.hexlet.io/session/new" role="button"><span>Вход</span>
</span></li>
</ul>
</div>
</div>
</nav>
</header>
<div class="x-container-xxxl">
</div>
<main class="mb-6 min-vh-100 h-100">
<link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzc2MCwicHVyIjoiYmxvYl9pZCJ9fQ==--9348098e4053d798b6f34bee4ef66947540261e4/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Low%20code%20development-rafiki.png"/><link rel="preload" as="image" href="/vite/assets/development-BVihs_d5.png"/><div id="app" data-page="{"component":"web/courses/lessons/theory_unit","props":{"errors":{},"locale":"ru","language":"ru","httpsHost":"https://ru.hexlet.io","host":"ru.hexlet.io","colorScheme":"light","auth":{"user":{"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26T17:15:08.071Z","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false}},"cloudflareTurnstileSiteKey":"0x4AAAAAAA15KmeFXzd2H0Xo","vkIdClientId":"51586979","yandexIdClientId":"88d071f1d3384eb4bd1deb37910235c7","formAuthToken":"4Gxcb1G1s1OIEayy0AD59Tsj0M5u2H9DYmEba3zaVcwPvZdYo8seMz5SiCrcDwmC-yr9ZGbvgeHfgYE_Lt2yog","topics":[{"id":19272,"title":"Добрый день. Не могу понять на каком этапе у меня добавляется лишняя запись в лог.\n```\n // BEGIN (write your solution here)\n const card = random(cards);\n const cardName = car(card);\n const damage = cdr(card);\n if (order === 1) {\n if (health1 <= 0) {\n const loseMessage = `Игрок ${name1} был убит`;\n return consList(cons(cons(health1, health2), loseMessage), log);\n }\n const nextHealth1 = health1 - damage();\n const message = `Игрок '${name2}' применил '${cardName}'\n против '${name1}' и нанес урон '${damage()}'`;\n const nextLog = consList(cons(cons(nextHealth1, health2), message), log);\n console.log(`logging : ${listToString(nextLog)}`)\n return iter(nextHealth1, name1, health2, name2, order + 1, nextLog);\n } else {\n if (health2 <= 0) {\n const loseMessage = `Игрок ${name2} был убит`;\n return consList(cons(cons(health1, health2), loseMessage), log);\n }\n const nextHealth2 = health2 - damage();\n const message = `Игрок '${name2}' применил '${cardName}'\n против '${name1}' и нанес урон '${damage()}'`;\n const nextLog = consList(cons(cons(health1, nextHealth2), message), log);\n return iter(health1, name1, nextHealth2, name2, order - 1, nextLog);\n }\n // END\n```\nпри проверке получаю:\n```\nexpect(received).toBe(expected) // Object.is equality\n\n Expected: 5\n Received: 6\n\n```\nА console.log при этом выводит нужное кол-во записей:\n```\nconsole.log game.js:19\n logging : (pair: ((-2, 4), Игрок 'Ada' применил 'Костяная кочерга гробницы'\n против 'John' и нанес урон '6'), pair: ((4, 4), Игрок 'Ada' применил 'Костяная кочерга гробницы'\n против 'John' и нанес урон '6'), pair: ((4, 10), Игрок 'Ada' применил 'Костяная кочерга гробницы'\n против 'John' и нанес урон '6'), pair: ((10, 10), Начинаем бой!))\n```","plain_title":"Добрый день. Не могу понять на каком этапе у меня добавляется лишняя запись в лог. // BEGIN (write your solution here) const card = random(cards); const cardName = car(card); const damage = cdr(card); if (order === 1) { if (health1 <= 0) { const loseMessage = `Игрок ${name1} был убит`; return consList(cons(cons(health1, health2), loseMessage), log); } const nextHealth1 = health1 - damage(); const message = `Игрок '${name2}' применил '${cardName}' против '${name1}' и нанес урон '${damage()}'`; const nextLog = consList(cons(cons(nextHealth1, health2), message), log); console.log(`logging : ${listToString(nextLog)}`) return iter(nextHealth1, name1, health2, name2, order + 1, nextLog); } else { if (health2 <= 0) { const loseMessage = `Игрок ${name2} был убит`; return consList(cons(cons(health1, health2), loseMessage), log); } const nextHealth2 = health2 - damage(); const message = `Игрок '${name2}' применил '${cardName}' против '${name1}' и нанес урон '${damage()}'`; const nextLog = consList(cons(cons(health1, nextHealth2), message), log); return iter(health1, name1, nextHealth2, name2, order - 1, nextLog); } // END при проверке получаю: ``` expect(received).toBe(expected) // Object.is equality Expected: 5 Received: 6 А console.log при этом выводит нужное кол-во записей: console.log game.js:19 logging : (pair: ((-2, 4), Игрок 'Ada' применил 'Костяная кочерга гробницы' против 'John' и нанес урон '6'), pair: ((4, 4), Игрок 'Ada' применил 'Костяная кочерга гробницы' против 'John' и нанес урон '6'), pair: ((4, 10), Игрок 'Ada' применил 'Костяная кочерга гробницы' против 'John' и нанес урон '6'), pair: ((10, 10), Начинаем бой!)) ``` ","creator":{"public_name":"Денис Маслакович","id":168626,"is_tutor":false},"comments":[{"creator":{"public_name":"Александр О.","id":61806,"is_tutor":false},"id":40777,"body":"Приветствую!\n\n> Не могу понять на каком этапе у меня добавляется лишняя запись в лог.\n\nЧто-то негусто в вашем примере отладочной печати :) В приведённом коде несколько мест, где добавляется запись в лог. Сделайте больше отладочной печати, чтобы более детально проанализировать состояние программы.","topic_id":19272}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Игровой дизайн: карточный бой","entity_url":null,"active":true}},{"id":11767,"title":"Прикольно...\nРешение учителя меня приятно удивило.\n```\norder === 1 ? 2 : 1\n```\nпрямо в вызове функции.\nВот сижу и думаю: \"И чё я так раньше не делал\".","plain_title":"Прикольно... Решение учителя меня приятно удивило. order === 1 ? 2 : 1 прямо в вызове функции. Вот сижу и думаю: \"И чё я так раньше не делал\". ","creator":{"public_name":"Андрей Никитин","id":144566,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":24406,"body":"В тех местах где ожидается выражение (например аргументы функций), можно подставлять любое выражение, а тернарник это выражение. ","topic_id":11767}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Игровой дизайн: карточный бой","entity_url":null,"active":true}},{"id":19024,"title":"Задачу решил, но не уверен, что понял, что такое нормализация данных, которую Кирилл упоминает в обсуждениях. Что есть нормализация в контексте нашей задачи?","plain_title":"Задачу решил, но не уверен, что понял, что такое нормализация данных, которую Кирилл упоминает в обсуждениях. Что есть нормализация в контексте нашей задачи? ","creator":{"public_name":"Сергей Кулаков","id":181653,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":40186,"body":"Нормализация данных это приведение данных к единому виду независимо от входа. Это позволяет описать алгоритм вычисления единым способом без ифов для всех входных данных.","topic_id":19024}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Игровой дизайн: карточный бой","entity_url":null,"active":true}},{"id":9386,"title":"Есть лишь 1 вопрос, значения в логе должны записываться,как :\nl(l(health1,health2),message), их же не нужны группировать с другими логами?","plain_title":"Есть лишь 1 вопрос, значения в логе должны записываться,как : l(l(health1,health2),message), их же не нужны группировать с другими логами? ","creator":{"public_name":"Дмитрий Ширманов","id":114909,"is_tutor":false},"comments":[{"creator":{"public_name":"Александр О.","id":61806,"is_tutor":false},"id":19159,"body":"Здравствуйте!\n\n> Есть лишь 1 вопрос, значения в логе должны записываться,как : l(l(health1,health2),message)\n\nА почему вы решили, что формат записи лога должен быть именно таким (как вы написали)?\n\n> их же не нужны группировать с другими логами?\n\nДля протоколирования хода игры у вас есть лог, на каждом шаге вы добавляете в этот лог новую запись о результатах шага. Таким образом, лог это типичный список (по мере новых шагов в него, как в список, добавляются новые записи)","topic_id":9386},{"creator":{"public_name":"Дмитрий Ширманов","id":114909,"is_tutor":false},"id":19201,"body":"Спасибо, я уже решил задание.","topic_id":9386}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Игровой дизайн: карточный бой","entity_url":null,"active":true}},{"id":30349,"title":"Добрый день, не могу понять, почему ошибка именно здесь, как будто что-то не так с рандомом. В зависимости от принятого значения переменной order, меняю местами первого и второго игрока.\n\n`// removed`","plain_title":"Добрый день, не могу понять, почему ошибка именно здесь, как будто что-то не так с рандомом. В зависимости от принятого значения переменной order, меняю местами первого и второго игрока. // removed ","creator":{"public_name":"Denis Borneman","id":232491,"is_tutor":false},"comments":[{"creator":{"public_name":"Denis Borneman","id":232491,"is_tutor":false},"id":65771,"body":"**Александр О.**, https://ru.hexlet.io/code_reviews/132816","topic_id":30349},{"creator":{"public_name":"Denis Borneman","id":232491,"is_tutor":false},"id":65786,"body":"**Александр О.**, теперь не уверен, пробовал ставить туда просто число, возвращается значение, но всегда вместо длины лога 5 возвращается 3. ","topic_id":30349},{"creator":{"public_name":"Александр О.","id":61806,"is_tutor":false},"id":65810,"body":"> теперь не уверен\n\nЗдесь вопрос не столько в уверенности как таковой) Вам надо проанализировать как правильно работать с картой и доставать из неё те или иные данные.","topic_id":30349},{"creator":{"public_name":"Александр О.","id":61806,"is_tutor":false},"id":65782,"body":"> сейчас таким образом\n\nА вы уверены, что так делать правильно?","topic_id":30349},{"creator":{"public_name":"Denis Borneman","id":232491,"is_tutor":false},"id":65779,"body":"**Александр О.**, программа при отладочной печати наглухо зависает) но видно, что значение было 10, а в конце становится NaN. Может быть неверно извлекается числовое значение карты урона? cdr(random(cards)) , сейчас таким образом.","topic_id":30349},{"creator":{"public_name":"Александр О.","id":61806,"is_tutor":false},"id":65781,"body":"> но видно, что значение было 10, а в конце становится NaN. Может быть неверно извлекается числовое значение карты урона? \n\nВот, это уже кое-что, конкретика) Сейчас вам надо проработать в этом направлении - проанализировать, почему переменная получает NaN","topic_id":30349},{"creator":{"public_name":"Александр О.","id":61806,"is_tutor":false},"id":65763,"body":"**Denis**, здравствуйте!\n\n[Отправьте свой код на ревью](https://help.hexlet.io/article/40-code-review), а сюда скиньте ссылку на него, чтобы можно было удобно посмотреть и проанализировать код и вывод тестов.","topic_id":30349},{"creator":{"public_name":"Александр О.","id":61806,"is_tutor":false},"id":65774,"body":"Из описания ошибки ` RangeError: Maximum call stack size exceeded` похоже, что у вас не срабатывает терминальное условие (остановки рекурсии). Вы пробовали сделать отладочную печать и отследить состояние переменных - как изменяется во время выполнения программы переменная pHealth2?","topic_id":30349}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Игровой дизайн: карточный бой","entity_url":null,"active":true}},{"id":18750,"title":"И снова всем, привет.\nСогласно алгоритма, \n\n\n> если здоровье игрока (которого атаковали на предыдущем шаге) меньше или равно 0, то обновляем лог и возвращаем наружу.\n> В ином случае, берем рандомную карту, вычисляем урон, записываем новое здоровье, а также добавляем строчку в лог, используя формат:\n> \n> const message = `Игрок '${name1}' применил '${cardName}' против '${name2}' и нанес урон '${damage}'`;\n> \nПодсказка о том, что \n> Формат записи лога cons(cons(health1, health2), message)\n\nскорее является вредным советом. \nИ на мой вопрос про это Кирилл ответил своим коронным хитрым вопросом:\n> Как вы понимаете вот это:\n```Expected: 5\nReceived: 1\n\nexpect(length(log)).toBe(5);\n?\n ```\nИ на пятый день я понял, что лог имеет такой формат consList(cons(cons(health1, health2), message), log)","plain_title":"И снова всем, привет. Согласно алгоритма, если здоровье игрока (которого атаковали на предыдущем шаге) меньше или равно 0, то обновляем лог и возвращаем наружу. В ином случае, берем рандомную карту, вычисляем урон, записываем новое здоровье, а также добавляем строчку в лог, используя формат: const message = Игрок '${name1}' применил '${cardName}' против '${name2}' и нанес урон '${damage}'; Подсказка о том, что Формат записи лога cons(cons(health1, health2), message) скорее является вредным советом. И на мой вопрос про это Кирилл ответил своим коронным хитрым вопросом: Как вы понимаете вот это: ```Expected: 5 Received: 1 expect(length(log)).toBe(5); ? ``` И на пятый день я понял, что лог имеет такой формат consList(cons(cons(health1, health2), message), log) ","creator":{"public_name":"Валерий Рязанский","id":180049,"is_tutor":false},"comments":[{"creator":{"public_name":"Валерий Рязанский","id":180049,"is_tutor":false},"id":39823,"body":"Александр, добрый день.\nДа, спасибо, упражнения выполнил.","topic_id":18750},{"creator":{"public_name":"Александр О.","id":61806,"is_tutor":false},"id":39780,"body":"Пожалуйста, вам удалось справиться с пракикой?","topic_id":18750},{"creator":{"public_name":"Валерий Рязанский","id":180049,"is_tutor":false},"id":39713,"body":"Александр, добрый день.\nСпасибо огромное за пояснение.\nИзвиняюсь, за некорректное утверждение.","topic_id":18750},{"creator":{"public_name":"Александр О.","id":61806,"is_tutor":false},"id":39592,"body":"Добрый день!\n\n> И на пятый день я понял, что лог имеет такой формат consList(cons(cons(health1, health2), message), log)\n\nНекорректное утверждение. Это выражение означает не формат записи лога, это операция добавления к логу новой записи. А формат отдельной записи лога - это то, что добавляется в лог, а именно: `cons(cons(health1, health2), message)`","topic_id":18750}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Игровой дизайн: карточный бой","entity_url":null,"active":true}},{"id":17095,"title":"Теория и практика вводит путаницу.\nВ теории при первом ходе показывает текущий уровень здоровья, а в практике же надо чтоб уже с нанесенным уроном показывал. ","plain_title":"Теория и практика вводит путаницу. В теории при первом ходе показывает текущий уровень здоровья, а в практике же надо чтоб уже с нанесенным уроном показывал. ","creator":{"public_name":"Александр Елахов","id":180236,"is_tutor":false},"comments":[{"creator":{"public_name":"Александр О.","id":61806,"is_tutor":false},"id":36241,"body":"Добрый день!\n\nЭто в теории опечатка, мы её поправим в скором времени: самая первая запись лога должна идти под индексом 0 - в ней отображаются начальные уровни здоровья игроков. В дальнейших логах уже показывается состояние здоровья после соответствуюдих ходов.","topic_id":17095},{"creator":{"public_name":"Pavel Alekseev","id":180146,"is_tutor":false},"id":36284,"body":"Потому что в теории показано то, как работает заключительная игра. Там первый ход - это уведомление о начале игры, которое будет реализовано в следующих уроках (наверно, я пока ещё не дошёл:))","topic_id":17095}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Игровой дизайн: карточный бой","entity_url":null,"active":true}},{"id":8300,"title":"Слишко днищенское решение? Что в этом коде не очень и следовало бы исправить? [код](https://ru.hexlet.io/code_reviews/15067)","plain_title":"Слишко днищенское решение? Что в этом коде не очень и следовало бы исправить? код (https://ru.hexlet.io/code_reviews/15067) ","creator":{"public_name":"Anastasia Panteleeva","id":138123,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":16149,"body":"Ну а еще линтер очень сильно ругается. То что связано с импортами это не ваше, остальное ваше.","topic_id":8300},{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":16148,"body":"Сложно да, почти на каждой строчке возможны разные состояния (отсюда и проверки), что приводит к усложнению понимания раза в два не меньше.","topic_id":8300}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Игровой дизайн: карточный бой","entity_url":null,"active":true}},{"id":4624,"title":"Зачем такие сложности с `cons`, `pairs.cons`, `pairs.car`, `head` если есть банальный `append` для списков? В данном случае он кажется более очевидным для списка лога, но не предложен в импорте.\nВзамен нам предлагается \"спуститься\" по абстракциям ниже, от списка к парам. Ступенька маленькая, но все же.\n\nИли я не проникся, и это связано со структурой лог-сообщения?)","plain_title":"Зачем такие сложности с cons, pairs.cons, pairs.car, head если есть банальный append для списков? В данном случае он кажется более очевидным для списка лога, но не предложен в импорте. Взамен нам предлагается \"спуститься\" по абстракциям ниже, от списка к парам. Ступенька маленькая, но все же. Или я не проникся, и это связано со структурой лог-сообщения?) ","creator":{"public_name":"Denis Smetannikov","id":79938,"is_tutor":false},"comments":[{"creator":{"public_name":"Denis Smetannikov","id":79938,"is_tutor":false},"id":7985,"body":"Спасибо. А то я потянулся к реверсу, но остановился с мыслями о скорости лога.","topic_id":4624},{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":7988,"body":"Да, реверс это частый кейс в конце вычислений при работе со списками. Обычно в соответствующих языках он крайне эффективен.","topic_id":4624},{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":7980,"body":"Насчет списков нужно знать следующую вещь. Правильный способ работы со списком это добавление в голову. Это не только в теории, так, на практике, работают во всех фп языках. Добавление в конце очень дорогая и не естественная операция для списков. А в наших абстракциях `cons` есть не только у пар, но так же и у списков. И для формирования лога, в коде, как раз используется `cons` из списков.","topic_id":4624}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Игровой дизайн: карточный бой","entity_url":null,"active":true}},{"id":16458,"title":"\nЗдраствуйте, решение учителя удивило, не додумался б никогда ((, можете сказать в чем мое решение хуже?","plain_title":"Здраствуйте, решение учителя удивило, не додумался б никогда ((, можете сказать в чем мое решение хуже? ","creator":{"public_name":"Samogray Olexandr Samograi","id":175740,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":34858,"body":"Приложите пожалуйста ссылку на код ревью.","topic_id":16458},{"creator":{"public_name":"Kirill Mokevnin","id":1,"is_tutor":false},"id":34880,"body":"В вашем решении большая цикломатическая сложность.","topic_id":16458},{"creator":{"public_name":"Samogray Olexandr Samograi","id":175740,"is_tutor":false},"id":34869,"body":"https://ru.hexlet.io/code_reviews/52931","topic_id":16458}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Игровой дизайн: карточный бой","entity_url":null,"active":true}}],"lesson":{"exercise":{"id":466,"slug":"js_ddp_game_design_exercise","name":null,"state":"active","kind":"exercise","language":"javascript","locale":"ru","has_web_view":false,"has_test_view":false,"reviewable":true,"readme":"## game.js\n\nДопишите функцию `iter()`, которая является частью ядра игрового движка и описывает в себе логику одного хода.\n\n### Алгоритм\n\n1. Если здоровье игрока, которого атаковали на предыдущем шаге (в примерах этого и следующего пунктов мы предполагаем, что это первый игрок с именем `name1`), меньше или равно 0, то добавляем в лог элемент с сообщением вида `${name1} был убит` и возвращаем лог. Игра закончена.\n\n2. В ином случае, берём рандомную карту, вычисляем урон, записываем новое здоровье, формируем сообщение формата:\n\n ```javascript\n const message = `Игрок '${name1}' применил '${cardName}'\n против '${name2}' и нанес урон '${damage}'`;\n ```\n\n Формируем элемент лога формата `cons(cons(health1, health2), message)` и добавляем его в лог.\n\n3. Повторяем.\n\n### Подсказки\n\n* Параметр `order` в функции `iter()` нужен для определения того, какой игрок сейчас атакует.\n* Используйте функцию `random()` для выбора карты из колоды.\n* Колода карт передаётся в игру через параметр `cards`.\n","prepared_readme":"## game.js\n\nДопишите функцию `iter()`, которая является частью ядра игрового движка и описывает в себе логику одного хода.\n\n### Алгоритм\n\n1. Если здоровье игрока, которого атаковали на предыдущем шаге (в примерах этого и следующего пунктов мы предполагаем, что это первый игрок с именем `name1`), меньше или равно 0, то добавляем в лог элемент с сообщением вида `${name1} был убит` и возвращаем лог. Игра закончена.\n\n2. В ином случае, берём рандомную карту, вычисляем урон, записываем новое здоровье, формируем сообщение формата:\n\n ```javascript\n const message = `Игрок '${name1}' применил '${cardName}'\n против '${name2}' и нанес урон '${damage}'`;\n ```\n\n Формируем элемент лога формата `cons(cons(health1, health2), message)` и добавляем его в лог.\n\n3. Повторяем.\n\n### Подсказки\n\n* Параметр `order` в функции `iter()` нужен для определения того, какой игрок сейчас атакует.\n* Используйте функцию `random()` для выбора карты из колоды.\n* Колода карт передаётся в игру через параметр `cards`.\n","has_solution":true,"entity_name":"Игровой дизайн: карточный бой"},"units":[{"id":1379,"name":"theory","url":"/courses/ddp/lessons/game_design/theory_unit"},{"id":1381,"name":"quiz","url":"/courses/ddp/lessons/game_design/quiz_unit"},{"id":1380,"name":"exercise","url":"/courses/ddp/lessons/game_design/exercise_unit"}],"links":[],"ordered_units":[{"id":1379,"name":"theory","url":"/courses/ddp/lessons/game_design/theory_unit"},{"id":1381,"name":"quiz","url":"/courses/ddp/lessons/game_design/quiz_unit"},{"id":1380,"name":"exercise","url":"/courses/ddp/lessons/game_design/exercise_unit"}],"id":699,"slug":"game_design","state":"approved","name":"Игровой дизайн: карточный бой","course_order":200,"goal":"Продумать игровой дизайн нашей карточный игры: как работает игра, как выглядит структура программы и как устроен процесс игры. Проработать поведение программы через тесты.","self_study":null,"theory_video_provider":"vimeo","theory_video_uid":"172395915","theory":"Вероятно для вас это будет большой сюрприз, но на текущий момент вы уже знаете всё, что нужно для того, чтобы написать эту игру. Предыдущие уроки нам дали достаточно материала для того, чтобы мы написали то, о чем говорили в интро текущего курса, поэтому этот урок посвящен геймдизайну.\nМы будем прорабатывать именно момент того, как работает игра и как она выглядит на программном уровне. И практикой к этому уроку будет уже создание непосредственно самой игры.\n\n## Правила\n\nДавайте еще раз посмотрим на то, как устроена наша игра.\n\n```\nНачинаем бой!\n\nИгрок 'John' применил 'Прохладный чыонг-бонг рыка'\nпротив 'Ada' и нанес урон '3'\n\nИгрок 'Ada' применил 'Воздушный змей клеветы'\nпротив 'John' и нанес урон '1'\n\nИгрок 'John' применил 'Проказливый рубитель крови'\nпротив 'Ada' и нанес урон '2'\n\nAda был убит\n```\n\nИтак, у нас есть начало боя, у нас есть конец боя в котором кто-то умирает и 2 игрока по очереди наносят урон друг другу. При этом игра включает в себя какой-то набор карт, который кстати говоря бесконечен (карты никогда не заканчиваются). Фактически мы просто создаем некие карты в которых описываем какой будет урон и после этого они рандомно применяются к конкретным игрокам для того, чтобы нанести этот урон. Карты – это просто список из которого рандомно выбирается какая-то карта и применяется. Это означает что в следующий раз может быть выбрана та же самая карта. Все зависит от того, как работает алгоритм выбора.\n\n## Test Driven Development\n\nДавайте начнём с тестов и подумаем о том, а как игра может вообще выглядеть и что нужно реализовать в коде для того, чтобы она заработала.\n\n```javascript\nimport { cons } from 'hexlet-pairs'\nimport { l, length } from 'hexlet-pairs-data'\nimport { make } from 'hexlet-card-game'\n\nconst cards = l(\n cons('Костяная кочерга гробницы', () => 6),\n)\nconst game = make(cards)\nconst log = game('John', 'Ada')\n\nassert.equal(length(log), 5)\n```\n\nПервое, что нам нужно – это список карт. Выше видно, что список карт `cards` – это действительно список с точки зрения структур, которые у нас есть.\nКаждый элемент этого списка является парой, которая состоит из 2х элементов: первый элемент пары – это имя карты, второй элемент – это функция, которая внутри себя содержит урон, который будет нанесён.\nОбратите внимание, что этот урон не содержит знака, то есть это не какая-то отрицательная величина, это именно то число, которое будет вычтено из жизни игрока к которому этот урон применяется.\n\nПочему здесь функция, но при этом просто конкретное число? Дело в том, что в будущем мы обязательно расширим нашу функциональность и урон будет не статический, как сейчас (когда карта наносит какую-то конкретную цифру). Например, текущая карта наносит урон 6 очков.\n\n```javascript\ncons('Костяная кочерга гробницы', () => 6)\n```\n\nНо карта может быть более хитрая. Например, она может брать текущее здоровье и на основе него вычислять какой урон нанести. Именно поэтому мы сразу это делаем функцией, потому что это достаточно очевидная вещь, которую в этой игре стоит вводить (тем более так было задумано изначально). Конечно, мы могли бы просто обойтись числом, но нам в конце концов пришлось бы вводить тут функцию.\n\nПосле того, как мы создали список этих карт (карта может быть и одна, это не имеет никакого значения), мы вызываем функцию `make` из нашего пакета `hexlet-card-game`, который мы и будем разрабатывать.\n`make` – это некая функция, которую сейчас мы будем использовать. Она принимает на вход список карт и создаёт игру. После этого мы вызываем `game` и передаем туда имена наших игроков и на этом все, больше ничего делать не надо. Игра идёт автоматически, то есть мы сами ими не управляем.\n\nЕстественно в реальной жизни у нас вызывались бы ходы по очереди, а после этого ожидалось бы какое-то действие от пользователя и мы бы его применяли. В нашем случае это не нужно, поэтому функция `game` проводит автоматическую игру и в конце концов возвращает *log* игры.\n\nЗдесь есть очень важный момент, который к тому же еще и очень интересный. То, что вы видели до этого выглядело, как будто мы просто печатаем на экран то, что происходит внутри игры и `game` мог бы так работать (внутри после каждого хода мы бы печатали что-то на экран). Но если вспомнить наш курс [Основы программирования](https://ru.hexlet.io/courses/introduction_to_programming), то там был очень важный урок посвящённый [чистым функциям](https://ru.hexlet.io/courses/introduction_to_programming/lessons/pure/theory_unit) и важности отсутствия побочных эффектов.\nПечать на экран – это побочный эффект. Это автоматически означает, что вы не можете безопасно запустить 2 раза подряд (как минимум) эту функцию `game`.\nВо-вторых, вы никак не контролируете этот вывод и вообще не можете, например, банально протестировать функцию и узнать что происходит. Конечно, можно технически перехватывать вывод, который идет на экран, но это совершенно неправильный способ тестировать софт. Он очень не надёжный. Это связано с тем, что вам придётся парсить строчки и проверять конкретное содержимое внутри них, что совершенно не хорошо.\nГораздо более правильный подход и способ работы здесь – это не печатать на экран результаты каждого хода, а фактически формировать некий *log*, который представляет из себя список, внутри которого содержится вся необходимая информация связанная с текущим ходом.\n\nВо-первых, это позволяет нам, как минимум, гораздо легче и удобнее анализировать, то что происходит.\nВо-вторых, у нас отсутствуют побочные эффекты и функция всегда возвращает некий чистый результат, то есть она сама по себе является чистой, что очень хорошо. Мы можем её повторно запускать, безопасно анализировать и такой код проще всего тестируется.\n\nНиже мы можем посмотреть первый тест. Самый очевидный и простой – это проверка длины лога, то что он равен 5.\n\n```javascript\nassert.equal(length(log), 5)\n```\n\nНаверно проще данного теста не написать. Ну может быть только проверить, что `log` является списком, но это не нужно, потому что это по интерфейсу и так должно быть очевидно. В любом случае здесь бы упало с ошибкой, если бы `log` не был списком.\nИтак, у нас получилось 5 ходов, давайте же теперь посмотрим какие это ходы.\n\n## Ходы\n\n```javascript\n// step ((health1, health2), message)\n\nconst step1 = get(0, log)\nassert.equal(toString(car(step1)), '(10, 10)')\nconst step2 = get(1, log)\nassert.equal(toString(car(step2)), '(10, 4)')\nconst step3 = get(2, log)\nassert.equal(toString(car(step3)), '(4, 4)')\nconst step4 = get(3, log)\nassert.equal(toString(car(step4)), '(4, -2)')\nconst step5 = get(4, log)\nassert.equal(toString(car(step5)), '(4, -2)')\n```\n\nЗдесь мы видим следующий набор тестов, который тоже должен присутствовать (и он присутствует) в наших тестах, в котором мы извлекаем из лога конкретные шаги (элементы) и проверяем, что происходит на каждом шаге.\n\nПеред тем, как мы посмотрим, непосредственно, как происходит сравнение и что внутри, давайте оценим из чего состоит каждый шаг.\n\n`log` – это набор шагов, который приводит к завершению игры. Каждый шаг – это пара, внутри которой 2 элемента.\n\n```javascript\n// step ((health1, health2), message)\n```\n\nПервый элемент – тоже пара, потому что у нас пока только пары для того, чтобы делать какие-то сложно-составные структуры.\nВторой – это *message*. Message – это как раз то самое сообщение, которое мы видели в самом начале, что кто-то кого-то убил, то есть это просто текстовое сообщение.\nПервый элемент (который пара) содержит здоровье первого игрока и здоровье второго игрока. Это нам нужно как раз для того, чтобы мы могли оценивать “*а правильно ли работает логика нашей программы?*” и мы это используем в тестах.\n\nТеперь давайте посмотрим, как мы это используем. У нас есть функция `get`, которая определена в пакете для работы со списками. Она извлекает элемент по индексу и здесь мы её просим дать нам первый элемент.\n\n```javascript\nconst step1 = get(0, log)\n```\n\nНу и соответственно второй, третий, четвёртый, пятый, то есть как раз все 5 элементов.\n\nА почему их 5? Если мы вспомним был тест, который как раз проверяет, что их 5.\n\n```javascript\nassert.equal(length(log), 5)\n```\n\n Это связано с тем, что текущий уровень жизни у всех мы выставили равным `10`, а `damage` (так называемый урон) у нас равен цифре `6`. И давайте теперь посмотрим, что происходит в таком случае.\n\nВ начале, когда происходит старт игры, мы получаем `message`, который называется **начало игры** и первый элемент в паре, который является парой со здоровьем по умолчанию, то есть это `10 10`. В `log` мы складываем не сам ход, а начальное состояние игры.\n\nКак мы извлекаем здоровье?\n\n```javascript\nassert.equal(toString(car(step1)), '(10, 10)')\n```\n\nИз `step` берем `car` (это первый элемент) и просто превращаем его в строчку, для того, чтобы сравнить и используем `assert.equal`, который проверяет, что левое значение должно совпадать с правым. То есть здесь слева `actual` – то, что пришло на самом деле, а справа `expected` (10, 10) – то, что мы ожидаем. И таким вот образом мы сравниваем каждый элемент.\n\nТеперь мы можем сосредоточиться только на части `expected` (10, 10) и посмотреть, как изменяются жизни в нашей текущей игре.\n\n```javascript\nassert.equal(toString(car(step1)), '(10, 10)')\nassert.equal(toString(car(step2)), '(10, 4)')\nassert.equal(toString(car(step3)), '(4, 4)')\nassert.equal(toString(car(step4)), '(4, -2)')\nassert.equal(toString(car(step5)), '(4, -2)')\n```\n\nСначала у обоих игроков по `10`, после этого применяется урон, который был равен `6` ко второму игроку и соответственно мы получаем `10, 4`. После этого, поскольку карта у нас одна и её урон `6`, она применяется к первому игроку – мы получаем `4, 4`. Затем снова применяется ко второму и у нас получается `4, -2`, это 4-й шаг. На 5 шаге видно, что здесь абсолютно тот же самый вывод, но почему? Потому что 5-й шаг, так же как и 1-й – это специальный шаг, в котором пишется сообщение, что кто-то был убит и здоровье показывается ровно такое какое было на предыдущем шаге, потому что здесь уже никому не наносится урон и мы просто фиксируем некое состояние, в котором закончилась наша игра.\n\n## Свойства\n\nКакими свойствами обладает наш дизайн, то что мы сделали и написали и какими свойствами обладает наш тестовый пакет?\n\n- Интерфейс – одна функция (make)\n- Автоматическая игра\n- Логика покрыта тестами\n\nВо-первых, весь интерфейс – это по сути одна функция `make`, то есть публичный интерфейс, в котором происходит работа, который в свою очередь генерирует функцию `game` и она уже проигрывает нам какую-то игру.\n\nРанее также говорилось, что игра автоматическая, у нас нет никакой возможности манипулировать ходом игры просто потому, что это не нужно. На текущий момент это отвлечёт нас от самой задачи и слишком сильно её усложнит.\n\nНу и как мы заметили логика покрыта тестами. То есть после того, как мы пишем эти тесты мы уже знаем какой у нас будет дизайн, мы легко можем запускать, перезапускать и дописывать наш код и самое главное, что в будущем эти тесты будут нам постоянно помогать. После любого рефакторинга, после любого изменения – мы уже всегда гарантированно сможем проверить, что результат такой, как мы ожидаем.\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":null,"units":[{"id":1378,"name":"theory","url":"/courses/ddp/lessons/intro/theory_unit"}],"links":[],"ordered_units":[{"id":1378,"name":"theory","url":"/courses/ddp/lessons/intro/theory_unit"}],"id":698,"slug":"intro","state":"approved","name":"Введение","course_order":100,"goal":"Познакомиться с курсом и с проектом карточной игры, над которым мы будем работать в течение всего курса, по ходу знакомясь с важными концепциями программирования.","self_study":null,"theory_video_provider":"vimeo","theory_video_uid":"172391980","theory":"<figure>\n <blockquote class=\"blockquote\">\n <p>Я изобрел понятие «объектно-ориентированный», но могу заявить, что не имел в виду C++ при этом.</p>\n </blockquote>\n <figcaption class=\"blockquote-footer\">\n Alan Kay\n </figcaption>\n</figure>\n\nНа текущий момент мы уже достаточно много знаем, как строить модульные программы и теперь пришло время поговорить о том, как строить расширяемые программы. В программировании существует понятие аддитивность, и эта тема будет центральной в нашем курсе.\n**Аддитивность** – это расширение возможности вашей программы без переписывания исходного кода.\n\n## Какие темы будут разобраны?\n\n- Пользовательские типы\n- Динамическая диспетчеризация (по типу)\n- Полиморфизм\n- Передача сообщений\n- Объектно-ориентированное программирование\n\nВо-первых, это пользовательские типы (типы данных, которые мы можем создавать самостоятельно).\nМы поговорим про такое важное понятие, как диспетчеризация и конкретно о динамической диспетчеризации (по типу). Разберём понятие полиморфизма. О нём многие слышали, но очень не многие понимают, что оно по-настоящему обозначает. Эту интересную тему мы разберём в одном из последних уроков. Также мы поговорим про передачу сообщений. И, наконец, придём к пониманию объектно-ориентированного программирования: что это такое, как с ним работать и почему, например, JavaScript считается объектно-ориентированным языком.\n\n## Новый синтаксис\n\n- Свойства\n- Тип Данных: Объект\n- Классы\n\nВ этом курсе появится новый синтаксис, который включает в себя несколько понятий.\nПервое – это **свойства**, которые используются в JavaScript даже для самых базовых вещей. Мы избегали этой темы, чтобы не отвлекаться от модульности. Теперь пришло время начать с ними работать.\n\nВо-вторых, мы разберём тип данных, который называется **объект**. Это не то же самое, что объект в объектно-ориентированном программировании, но иногда названия, которые используются в языках программирования несут свой смысл, и к этому нужно будет просто привыкнуть.\n\nИ последнее, с чем мы познакомимся – это **классы**. Ниже приведён небольшой пример класса, который будет использоваться в нашей программе. Мы подробно разберем его в нашем курсе.\n\n```javascript\nexport default class PercentCard {\n constructor(name, percent) {\n this.name = name\n this.percent = percent\n }\n\n damage(health) {\n return Math.round(health * (this.percent / 100))\n }\n}\n```\n\n## Проект: Карточная игра\n\n\n\nВ этом курсе мы будем работать над своей **карточной игрой**. Она будет без графического интерфейса. Мы будем выводит процесс игры в виде текста.\n\nЭта карточная игра необычная и не такая, как все привыкли видеть, играя в обычные карты. Игра будет про волшебников, в процессе которой происходит нападение друг на друга. Каждая карта описывает атаку, урон юнита и так далее. В нашем случае она будет простая, но на ней будет хорошо видна проблематика, которую мы собираемся отрабатывать.\n\n## Процесс игры\n\nДавайте посмотрим на процесс без кода. Как будет происходить вывод нашей игры:\n\n```\nНачинаем бой!\n\nИгрок 'John' применил 'Прохладный чыонг-бонг рыка'\nпротив 'Ada' и нанес урон '3'\n\nИгрок 'Ada' применил 'Воздушный змей клеветы'\nпротив 'John' и нанес урон '1'\n\nИгрок 'John' применил 'Проказливый рубитель крови'\nпротив 'Ada' и нанес урон '2'\n\nAda был убит\n```\n\nДля карт, которые представляют собой оружие, используются названия позаимствованные из Diablo. В сети существует множество генераторов названий предметов из этой игры. Они довольно прикольные, поэтому будет здорово здесь ими воспользоваться.\n\nГлядя на процесс, очевидно, что игра рассчитана на двух игроков, которые по очереди делают ходы. Сначала один игрок наносит урон, потом второй, затем снова первый и так по кругу. Урон зависит от текущей карты. И она не всегда наносит конкретный урон. Он может зависеть от текущего здоровья (например, снимать 50% здоровья) и других характеристик.\n\nПредположим, что стартовый уровень жизни у каждого игрока 10. А тот, у кого уровень жизни дойдёт до нуля или станет отрицательным, считается убитым. В примере видно, что игрок с именем Ada был убит первым.\n\n## Разработка через тесты\n\nНаш курс будет примером разработки через так называемый Test Driven Development. Это означает, что тесты пишутся до кода.\n\n```\nДизайн кода определяется тестами.\n\nВместо того, чтобы сосредотачиваться на\nвнутреннем представлении, сначала прорабатываются\nварианты использования этого кода.\n```\n\nЗачем это нужно?\n\n **Test Driven Development** – это такая методология, которая существует уже достаточно много лет. Она входит в концепцию экстремального программирования. Её главная фишка, что мы не фокусируемся на том, как будет устроен код внутри. Гораздо важнее, как им пользоваться. Именно дизайн кода (так называемый API), интерфейс, который вы отдаёте другим для использования и является определяющим. Если вы делаете хорошую абстракцию, то внутреннее содержимое не так важно. Вы всегда можете его переписать.\n\nОчень часто бывает так (это касается не только программирования), что люди делают что-то, но пользоваться этим невозможно. Это касается всего в нашей жизни: любого производства, продукта, процесса и так далее. Test Driven Development в первую очередь, заставляет нас думать о дизайне кода, о том, как его использовать, а затем строить внутреннюю структуру.\n\n## Обучение на Хекслете\n\nПовторим аспекты, которые мы используем для того, чтобы обучение было лучше и была проработка правильных вещей.\n\n- Неизменяемость (функциональный стиль)\n- СИКП\n- Обучение через рефакторинг\n- Разработка через тесты\n- Знания, которые не устаревают\n\nМы продолжаем использовать функциональный стиль. При этом постепенно начинаем вводить изменяемость.\n\nКурс также продолжает традицию основ на СИКП.\n\nМы делаем обучение через рефакторинг, то есть мы не рассказываем сразу, как правильно что-то сделать. Чтобы понять достаточно сложные темы по-настоящему, нужно пережить опыт и самостоятельно упереться в сложности. Поэтому уроки будут построены следующим образом: мы делаем какое-то решение, которое у вас (по нашей задумке) будет вызывать вопросы «А почему так?» и будете видеть, что здесь есть какие-то недоработки. В свою очередь следующий урок будет говорить об этом подробно и уже показывать следующий шаг для решения возникших проблем, и так далее. Постепенно мы будем приходить к общему решению, которое исправляет все недостатки предыдущих. Это так называемое обучение через рефакторинг.\n\nТакже напоминаю, что в этом курсе разработка будет происходить через тесты (о чём мы уже с вами говорили).\n\nЗнания текущего курса являются универсальными. Будет совсем чуть-чуть JavaScript специфики, но в основном всё, что здесь рассказывается — это ключ к пониманию, как устроены все современные языки, и какие проблемы они решают.\n"},"id":121,"slug":"ddp","challenges_count":3,"name":"JS: Программирование, управляемое данными","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"На этом курсе вы познакомитесь с программированием, управляемым данными. Вы узнаете, как использовать данные для управления логикой программы. Вы научитесь создавать динамические приложения и работать с классами, узнаете о полиморфизме и его типах, познакомитесь с диспетчеризацией по типу и аддитивностью. Знания из этого курса помогут создавать мощные и гибкие приложения, которые легко адаптируются к изменениям данных.","kind":"additional","updated_at":"2026-01-20T11:53:13.077Z","language":"javascript","duration_cache":39000,"skills":["Делать свое ООП с типами и диспетчеризацией","Использовать инверсию зависимостей для изменения поведения кода без его переписывания","Создавать классы и использовать их в JavaScript","Применять полиморфизм для уменьшения количества условных конструкций и дублирования"],"keywords":["ООП","Динамическая диспетчеризация","Типы данных","Объекты"],"lessons_count":9,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6ODkwOSwicHVyIjoiYmxvYl9pZCJ9fQ==--8d6ae873c203221995a5f2afe08cdb3b594e095f/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJqcGciLCJyZXNpemVfdG9fZmlsbCI6WzYwMCw0MDBdfSwicHVyIjoidmFyaWF0aW9uIn19--39ba06fa99226096df9fc6bb31f84e1d29ea98e9/image.png"},"recommendedLandings":[{"stack":{"id":20,"slug":"js-sicp","title":"СИКП на JS","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"finished","order":4050,"duration_in_months":1},"id":28,"slug":"js-sicp","title":"СИКП на JS","subtitle":"Навык понимать программы на фундаментальном уровне, уверенно проходить собеседования и решать сложные задачи","subtitle_for_lists":"Навык фундаментального программирования","locale":"ru","current":true,"duration_in_months_text":"1 месяц","stack_slug":"js-sicp","price_text":"от 3 900 ₽","duration_text":"1 месяц","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzc2MCwicHVyIjoiYmxvYl9pZCJ9fQ==--9348098e4053d798b6f34bee4ef66947540261e4/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Low%20code%20development-rafiki.png"}],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/ddp/lessons/game_design/theory_unit","version":"0b0c6d4ebbd40fd58630a0dd89cc25544ccdf24e","encryptHistory":false,"clearHistory":false}"><style data-mantine-styles="true">:root, :host{--mantine-font-family: Arial, sans-serif;--mantine-font-family-headings: Arial, sans-serif;--mantine-heading-font-weight: normal;--mantine-radius-default: 0rem;--mantine-primary-color-filled: var(--mantine-color-indigo-filled);--mantine-primary-color-filled-hover: var(--mantine-color-indigo-filled-hover);--mantine-primary-color-light: var(--mantine-color-indigo-light);--mantine-primary-color-light-hover: var(--mantine-color-indigo-light-hover);--mantine-primary-color-light-color: var(--mantine-color-indigo-light-color);--mantine-spacing-xxl: calc(4rem * var(--mantine-scale));--mantine-font-size-xs: 12px;--mantine-font-size-sm: 14px;--mantine-font-size-md: 16px;--mantine-font-size-lg: clamp(16.0000px, calc(15.2727px + 0.2273vw), 18.0000px);--mantine-font-size-xl: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-display-3: clamp(32.0000px, calc(26.1818px + 1.8182vw), 48.0000px);--mantine-font-size-display-2: clamp(36.0000px, calc(25.8182px + 3.1818vw), 64.0000px);--mantine-font-size-display-1: clamp(40.0000px, calc(25.4545px + 4.5455vw), 80.0000px);--mantine-font-size-h1: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-font-size-h2: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-font-size-h3: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-font-size-h4: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-font-size-h5: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-h6: 1rem;--mantine-primary-color-0: var(--mantine-color-indigo-0);--mantine-primary-color-1: var(--mantine-color-indigo-1);--mantine-primary-color-2: var(--mantine-color-indigo-2);--mantine-primary-color-3: var(--mantine-color-indigo-3);--mantine-primary-color-4: var(--mantine-color-indigo-4);--mantine-primary-color-5: var(--mantine-color-indigo-5);--mantine-primary-color-6: var(--mantine-color-indigo-6);--mantine-primary-color-7: var(--mantine-color-indigo-7);--mantine-primary-color-8: var(--mantine-color-indigo-8);--mantine-primary-color-9: var(--mantine-color-indigo-9);--mantine-color-red-0: #ffeaea;--mantine-color-red-1: #fed4d4;--mantine-color-red-2: #f4a7a8;--mantine-color-red-3: #ec7878;--mantine-color-red-4: #e55050;--mantine-color-red-5: #e03131;--mantine-color-red-6: #e02829;--mantine-color-red-7: #c71a1c;--mantine-color-red-8: #b21218;--mantine-color-red-9: #9c0411;--mantine-color-violet-0: #fce9ff;--mantine-color-violet-1: #f1cfff;--mantine-color-violet-2: #e09bff;--mantine-color-violet-3: #d16fff;--mantine-color-violet-4: #be37fe;--mantine-color-violet-5: #b51afe;--mantine-color-violet-6: #b009ff;--mantine-color-violet-7: #9b00e4;--mantine-color-violet-8: #8a00cc;--mantine-color-violet-9: #7800b3;--mantine-color-indigo-0: #edecff;--mantine-color-indigo-1: #d6d5fe;--mantine-color-indigo-2: #aaa9f4;--mantine-color-indigo-3: #7b79eb;--mantine-color-indigo-4: #5451e4;--mantine-color-indigo-5: #3b37e0;--mantine-color-indigo-6: #2d2adf;--mantine-color-indigo-7: #1f1ec7;--mantine-color-indigo-8: #1819b2;--mantine-color-indigo-9: #0c149e;--mantine-color-cyan-0: #dffdff;--mantine-color-cyan-1: #caf5ff;--mantine-color-cyan-2: #99e8ff;--mantine-color-cyan-3: #64daff;--mantine-color-cyan-4: #3ccffe;--mantine-color-cyan-5: #24c8fe;--mantine-color-cyan-6: #00c2ff;--mantine-color-cyan-7: #00ade4;--mantine-color-cyan-8: #009acd;--mantine-color-cyan-9: #0085b5;--mantine-color-green-0: #e9fdec;--mantine-color-green-1: #d7f6dc;--mantine-color-green-2: #b0eab9;--mantine-color-green-3: #86df94;--mantine-color-green-4: #62d574;--mantine-color-green-5: #4ccf5f;--mantine-color-green-6: #3fcc54;--mantine-color-green-7: #2fb344;--mantine-color-green-8: #25a03b;--mantine-color-green-9: #138a2e;--mantine-color-yellow-0: #fff7e2;--mantine-color-yellow-1: #ffeecd;--mantine-color-yellow-2: #ffdc9c;--mantine-color-yellow-3: #ffc966;--mantine-color-yellow-4: #feb93a;--mantine-color-yellow-5: #feae1e;--mantine-color-yellow-6: #ffa90f;--mantine-color-yellow-8: #ca8200;--mantine-color-yellow-9: #af7000;--mantine-h1-font-size: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-h1-font-weight: normal;--mantine-h2-font-size: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-h2-font-weight: normal;--mantine-h3-font-size: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-h3-font-weight: normal;--mantine-h4-font-size: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-h4-font-weight: normal;--mantine-h5-font-size: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-h5-font-weight: normal;--mantine-h6-font-size: 1rem;--mantine-h6-font-weight: normal;}
:root[data-mantine-color-scheme="dark"], :host([data-mantine-color-scheme="dark"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-dark-filled: var(--mantine-color-dark-5);--mantine-color-dark-filled-hover: var(--mantine-color-dark-6);--mantine-color-dark-light: rgba(105, 105, 105, 0.15);--mantine-color-dark-light-hover: rgba(105, 105, 105, 0.2);--mantine-color-dark-light-color: var(--mantine-color-dark-0);--mantine-color-dark-outline: var(--mantine-color-dark-1);--mantine-color-dark-outline-hover: rgba(184, 184, 184, 0.05);--mantine-color-gray-filled: var(--mantine-color-gray-5);--mantine-color-gray-filled-hover: var(--mantine-color-gray-6);--mantine-color-gray-light: rgba(222, 226, 230, 0.15);--mantine-color-gray-light-hover: rgba(222, 226, 230, 0.2);--mantine-color-gray-light-color: var(--mantine-color-gray-0);--mantine-color-gray-outline: var(--mantine-color-gray-1);--mantine-color-gray-outline-hover: rgba(241, 243, 245, 0.05);--mantine-color-red-filled: var(--mantine-color-red-5);--mantine-color-red-filled-hover: var(--mantine-color-red-6);--mantine-color-red-light: rgba(236, 120, 120, 0.15);--mantine-color-red-light-hover: rgba(236, 120, 120, 0.2);--mantine-color-red-light-color: var(--mantine-color-red-0);--mantine-color-red-outline: var(--mantine-color-red-1);--mantine-color-red-outline-hover: rgba(254, 212, 212, 0.05);--mantine-color-pink-filled: var(--mantine-color-pink-5);--mantine-color-pink-filled-hover: var(--mantine-color-pink-6);--mantine-color-pink-light: rgba(250, 162, 193, 0.15);--mantine-color-pink-light-hover: rgba(250, 162, 193, 0.2);--mantine-color-pink-light-color: var(--mantine-color-pink-0);--mantine-color-pink-outline: var(--mantine-color-pink-1);--mantine-color-pink-outline-hover: rgba(255, 222, 235, 0.05);--mantine-color-grape-filled: var(--mantine-color-grape-5);--mantine-color-grape-filled-hover: var(--mantine-color-grape-6);--mantine-color-grape-light: rgba(229, 153, 247, 0.15);--mantine-color-grape-light-hover: rgba(229, 153, 247, 0.2);--mantine-color-grape-light-color: var(--mantine-color-grape-0);--mantine-color-grape-outline: var(--mantine-color-grape-1);--mantine-color-grape-outline-hover: rgba(243, 217, 250, 0.05);--mantine-color-violet-filled: var(--mantine-color-violet-5);--mantine-color-violet-filled-hover: var(--mantine-color-violet-6);--mantine-color-violet-light: rgba(209, 111, 255, 0.15);--mantine-color-violet-light-hover: rgba(209, 111, 255, 0.2);--mantine-color-violet-light-color: var(--mantine-color-violet-0);--mantine-color-violet-outline: var(--mantine-color-violet-1);--mantine-color-violet-outline-hover: rgba(241, 207, 255, 0.05);--mantine-color-indigo-filled: var(--mantine-color-indigo-5);--mantine-color-indigo-filled-hover: var(--mantine-color-indigo-6);--mantine-color-indigo-light: rgba(123, 121, 235, 0.15);--mantine-color-indigo-light-hover: rgba(123, 121, 235, 0.2);--mantine-color-indigo-light-color: var(--mantine-color-indigo-0);--mantine-color-indigo-outline: var(--mantine-color-indigo-1);--mantine-color-indigo-outline-hover: rgba(214, 213, 254, 0.05);--mantine-color-blue-filled: var(--mantine-color-blue-5);--mantine-color-blue-filled-hover: var(--mantine-color-blue-6);--mantine-color-blue-light: rgba(116, 192, 252, 0.15);--mantine-color-blue-light-hover: rgba(116, 192, 252, 0.2);--mantine-color-blue-light-color: var(--mantine-color-blue-0);--mantine-color-blue-outline: var(--mantine-color-blue-1);--mantine-color-blue-outline-hover: rgba(208, 235, 255, 0.05);--mantine-color-cyan-filled: var(--mantine-color-cyan-5);--mantine-color-cyan-filled-hover: var(--mantine-color-cyan-6);--mantine-color-cyan-light: rgba(100, 218, 255, 0.15);--mantine-color-cyan-light-hover: rgba(100, 218, 255, 0.2);--mantine-color-cyan-light-color: var(--mantine-color-cyan-0);--mantine-color-cyan-outline: var(--mantine-color-cyan-1);--mantine-color-cyan-outline-hover: rgba(202, 245, 255, 0.05);--mantine-color-teal-filled: var(--mantine-color-teal-5);--mantine-color-teal-filled-hover: var(--mantine-color-teal-6);--mantine-color-teal-light: rgba(99, 230, 190, 0.15);--mantine-color-teal-light-hover: rgba(99, 230, 190, 0.2);--mantine-color-teal-light-color: var(--mantine-color-teal-0);--mantine-color-teal-outline: var(--mantine-color-teal-1);--mantine-color-teal-outline-hover: rgba(195, 250, 232, 0.05);--mantine-color-green-filled: var(--mantine-color-green-5);--mantine-color-green-filled-hover: var(--mantine-color-green-6);--mantine-color-green-light: rgba(134, 223, 148, 0.15);--mantine-color-green-light-hover: rgba(134, 223, 148, 0.2);--mantine-color-green-light-color: var(--mantine-color-green-0);--mantine-color-green-outline: var(--mantine-color-green-1);--mantine-color-green-outline-hover: rgba(215, 246, 220, 0.05);--mantine-color-lime-filled: var(--mantine-color-lime-5);--mantine-color-lime-filled-hover: var(--mantine-color-lime-6);--mantine-color-lime-light: rgba(192, 235, 117, 0.15);--mantine-color-lime-light-hover: rgba(192, 235, 117, 0.2);--mantine-color-lime-light-color: var(--mantine-color-lime-0);--mantine-color-lime-outline: var(--mantine-color-lime-1);--mantine-color-lime-outline-hover: rgba(233, 250, 200, 0.05);--mantine-color-yellow-filled: var(--mantine-color-yellow-5);--mantine-color-yellow-filled-hover: var(--mantine-color-yellow-6);--mantine-color-yellow-light: rgba(255, 201, 102, 0.15);--mantine-color-yellow-light-hover: rgba(255, 201, 102, 0.2);--mantine-color-yellow-light-color: var(--mantine-color-yellow-0);--mantine-color-yellow-outline: var(--mantine-color-yellow-1);--mantine-color-yellow-outline-hover: rgba(255, 238, 205, 0.05);--mantine-color-orange-filled: var(--mantine-color-orange-5);--mantine-color-orange-filled-hover: var(--mantine-color-orange-6);--mantine-color-orange-light: rgba(255, 192, 120, 0.15);--mantine-color-orange-light-hover: rgba(255, 192, 120, 0.2);--mantine-color-orange-light-color: var(--mantine-color-orange-0);--mantine-color-orange-outline: var(--mantine-color-orange-1);--mantine-color-orange-outline-hover: rgba(255, 232, 204, 0.05);--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-9) 0%, var(--mantine-color-cyan-7) 100%);--app-color-surface: #2e2e2e;}
:root[data-mantine-color-scheme="light"], :host([data-mantine-color-scheme="light"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-red-light: rgba(224, 40, 41, 0.1);--mantine-color-red-light-hover: rgba(224, 40, 41, 0.12);--mantine-color-red-outline-hover: rgba(224, 40, 41, 0.05);--mantine-color-violet-light: rgba(176, 9, 255, 0.1);--mantine-color-violet-light-hover: rgba(176, 9, 255, 0.12);--mantine-color-violet-outline-hover: rgba(176, 9, 255, 0.05);--mantine-color-indigo-light: rgba(45, 42, 223, 0.1);--mantine-color-indigo-light-hover: rgba(45, 42, 223, 0.12);--mantine-color-indigo-outline-hover: rgba(45, 42, 223, 0.05);--mantine-color-cyan-light: rgba(0, 194, 255, 0.1);--mantine-color-cyan-light-hover: rgba(0, 194, 255, 0.12);--mantine-color-cyan-outline-hover: rgba(0, 194, 255, 0.05);--mantine-color-green-light: rgba(63, 204, 84, 0.1);--mantine-color-green-light-hover: rgba(63, 204, 84, 0.12);--mantine-color-green-outline-hover: rgba(63, 204, 84, 0.05);--mantine-color-yellow-light: rgba(255, 169, 15, 0.1);--mantine-color-yellow-light-hover: rgba(255, 169, 15, 0.12);--mantine-color-yellow-outline-hover: rgba(255, 169, 15, 0.05);--app-color-surface: #f1f3f5;--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-5) 100%);}</style><style data-mantine-styles="classes">@media (max-width: 35.99375em) {.mantine-visible-from-xs {display: none !important;}}@media (min-width: 36em) {.mantine-hidden-from-xs {display: none !important;}}@media (max-width: 47.99375em) {.mantine-visible-from-sm {display: none !important;}}@media (min-width: 48em) {.mantine-hidden-from-sm {display: none !important;}}@media (max-width: 61.99375em) {.mantine-visible-from-md {display: none !important;}}@media (min-width: 62em) {.mantine-hidden-from-md {display: none !important;}}@media (max-width: 74.99375em) {.mantine-visible-from-lg {display: none !important;}}@media (min-width: 75em) {.mantine-hidden-from-lg {display: none !important;}}@media (max-width: 87.99375em) {.mantine-visible-from-xl {display: none !important;}}@media (min-width: 88em) {.mantine-hidden-from-xl {display: none !important;}}</style><div style="position:absolute;top:0rem" class=""></div><div style="max-width:var(--container-size-xl);height:100%;min-height:0rem" class=""><style data-mantine-styles="inline">.__m__-_R_5ub_{--grid-gutter:0rem;}</style><div style="height:100%;min-height:0rem" class="m_410352e9 mantine-Grid-root __m__-_R_5ub_"><div class="m_dee7bd2f mantine-Grid-inner" style="height:100%"><style data-mantine-styles="inline">.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:91.66666666666667%;--col-max-width:91.66666666666667%;}@media(min-width: 48em){.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:83.33333333333334%;--col-max-width:83.33333333333334%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem;display:flex" class="m_96bdd299 mantine-Grid-col __m__-_R_rdub_"><style data-mantine-styles="inline">.__m__-_R_6qrdub_{margin-top:0rem;padding-inline:var(--mantine-spacing-xs);width:100%;}@media(min-width: 48em){.__m__-_R_6qrdub_{margin-top:var(--mantine-spacing-xl);width:80%;}}@media(min-width: 62em){.__m__-_R_6qrdub_{padding-inline:var(--mantine-spacing-xl);}}</style><div style="margin-inline:auto;max-width:var(--mantine-breakpoint-xl)" class="__m__-_R_6qrdub_"><div style="color:var(--mantine-color-dimmed)" class="m_4451eb3a mantine-Center-root" data-inline="true"><div style="--ti-size:var(--ti-size-xs);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;margin-inline-end:calc(0.125rem * var(--mantine-scale));color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="xs"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-lock "><path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"></path><path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path><path d="M8 11v-4a4 4 0 1 1 8 0v4"></path></svg></div><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">JS: Программирование, управляемое данными</p></div><h1 style="--title-fw:var(--mantine-h1-font-weight);--title-lh:var(--mantine-h1-line-height);--title-fz:var(--mantine-h1-font-size);margin-bottom:var(--mantine-spacing-xl)" class="m_8a5d1357 mantine-Title-root" data-order="1">Теория: Игровой дизайн: карточный бой</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"Игровой дизайн: карточный бой","inLanguage":"ru","isPartOf":{"@type":"LearningResource","name":"JS: Программирование, управляемое данными"},"isAccessibleForFree":"False","hasPart":{"@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".paywalled"}}</script><div class=""><div style="--alert-color:var(--mantine-color-indigo-light-color);margin-bottom:var(--mantine-spacing-lg);font-size:var(--mantine-font-size-lg)" class="m_66836ed3 mantine-Alert-root" id="mantine-_R_remqrdub_" role="alert" aria-describedby="mantine-_R_remqrdub_-body" aria-labelledby="mantine-_R_remqrdub_-title"><div class="m_a5d60502 mantine-Alert-wrapper"><div class="m_667f2a6a mantine-Alert-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-rocket "><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path><path d="M14 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path></svg></div><div class="m_667c2793 mantine-Alert-body"><div class="m_6a03f287 mantine-Alert-title"><span id="mantine-_R_remqrdub_-title" class="m_698f4f23 mantine-Alert-label">Полный доступ к материалам</span></div><div id="mantine-_R_remqrdub_-body" class="m_7fa78076 mantine-Alert-message"><div style="--group-gap:var(--mantine-spacing-md);--group-align:center;--group-justify:space-between;--group-wrap:wrap" class="m_4081bf90 mantine-Group-root"><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Зарегистрируйтесь и получите доступ к этому и десяткам других курсов</p><a style="--button-height:var(--button-height-xs);--button-padding-x:var(--button-padding-x-xs);--button-fz:var(--mantine-font-size-xs);--button-bg:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-hover:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-color:var(--mantine-color-white);--button-bd:none" class="mantine-focus-auto mantine-active m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root" data-variant="gradient" data-size="xs" href="/u/new"><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">Зарегистрироваться</span></span></a></div></div></div></div></div><div class="paywalled m_d08caa0 mantine-Typography-root"><p>Вероятно для вас это будет большой сюрприз, но на текущий момент вы уже знаете всё, что нужно для того, чтобы написать эту игру. Предыдущие уроки нам дали достаточно материала для того, чтобы мы написали то, о чем говорили в интро текущего курса, поэтому этот урок посвящен геймдизайну.
Мы будем прорабатывать именно момент того, как работает игра и как она выглядит на программном уровне. И практикой к этому уроку будет уже создание непосредственно самой игры.</p>
<h2 id="heading-2-1">Правила</h2>
<p>Давайте еще раз посмотрим на то, как устроена наша игра.</p>
<code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Начинаем бой!
Игрок 'John' применил 'Прохладный чыонг-бонг рыка'
против 'Ada' и нанес урон '3'
Игрок 'Ada' применил 'Воздушный змей клеветы'
против 'John' и нанес урон '1'
Игрок 'John' применил 'Проказливый рубитель крови'
против 'Ada' и нанес урон '2'
Ada был убит</code>
<p>Итак, у нас есть начало боя, у нас есть конец боя в котором кто-то умирает и 2 игрока по очереди наносят урон друг другу. При этом игра включает в себя какой-то набор карт, который кстати говоря бесконечен (карты никогда не заканчиваются). Фактически мы просто создаем некие карты в которых описываем какой будет урон и после этого они рандомно применяются к конкретным игрокам для того, чтобы нанести этот урон. Карты – это просто список из которого рандомно выбирается какая-то карта и применяется. Это означает что в следующий раз может быть выбрана та же самая карта. Все зависит от того, как работает алгоритм выбора.</p>
<h2 id="heading-2-2">Test Driven Development</h2>
<p>Давайте начнём с тестов и подумаем о том, а как игра может вообще выглядеть и что нужно реализовать в коде для того, чтобы она заработала.</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">import { cons } from 'hexlet-pairs'
import { l, length } from 'hexlet-pairs-data'
import { make } from 'hexlet-card-game'
const cards = l(
cons('Костяная кочерга гробницы', () => 6),
)
const game = make(cards)
const log = game('John', 'Ada')
assert.equal(length(log), 5)</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Первое, что нам нужно – это список карт. Выше видно, что список карт <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">cards</code> – это действительно список с точки зрения структур, которые у нас есть.
Каждый элемент этого списка является парой, которая состоит из 2х элементов: первый элемент пары – это имя карты, второй элемент – это функция, которая внутри себя содержит урон, который будет нанесён.
Обратите внимание, что этот урон не содержит знака, то есть это не какая-то отрицательная величина, это именно то число, которое будет вычтено из жизни игрока к которому этот урон применяется.</p>
<p>Почему здесь функция, но при этом просто конкретное число? Дело в том, что в будущем мы обязательно расширим нашу функциональность и урон будет не статический, как сейчас (когда карта наносит какую-то конкретную цифру). Например, текущая карта наносит урон 6 очков.</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">cons('Костяная кочерга гробницы', () => 6)</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Но карта может быть более хитрая. Например, она может брать текущее здоровье и на основе него вычислять какой урон нанести. Именно поэтому мы сразу это делаем функцией, потому что это достаточно очевидная вещь, которую в этой игре стоит вводить (тем более так было задумано изначально). Конечно, мы могли бы просто обойтись числом, но нам в конце концов пришлось бы вводить тут функцию.</p>
<p>После того, как мы создали список этих карт (карта может быть и одна, это не имеет никакого значения), мы вызываем функцию <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">make</code> из нашего пакета <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">hexlet-card-game</code>, который мы и будем разрабатывать.
<code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">make</code> – это некая функция, которую сейчас мы будем использовать. Она принимает на вход список карт и создаёт игру. После этого мы вызываем <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">game</code> и передаем туда имена наших игроков и на этом все, больше ничего делать не надо. Игра идёт автоматически, то есть мы сами ими не управляем.</p>
<p>Естественно в реальной жизни у нас вызывались бы ходы по очереди, а после этого ожидалось бы какое-то действие от пользователя и мы бы его применяли. В нашем случае это не нужно, поэтому функция <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">game</code> проводит автоматическую игру и в конце концов возвращает <em>log</em> игры.</p>
<p>Здесь есть очень важный момент, который к тому же еще и очень интересный. То, что вы видели до этого выглядело, как будто мы просто печатаем на экран то, что происходит внутри игры и <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">game</code> мог бы так работать (внутри после каждого хода мы бы печатали что-то на экран). Но если вспомнить наш курс <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://ru.hexlet.io/courses/introduction_to_programming" rel="noopener noreferrer" target="_blank">Основы программирования</a>, то там был очень важный урок посвящённый <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://ru.hexlet.io/courses/introduction_to_programming/lessons/pure/theory_unit" rel="noopener noreferrer" target="_blank">чистым функциям</a> и важности отсутствия побочных эффектов.
Печать на экран – это побочный эффект. Это автоматически означает, что вы не можете безопасно запустить 2 раза подряд (как минимум) эту функцию <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">game</code>.
Во-вторых, вы никак не контролируете этот вывод и вообще не можете, например, банально протестировать функцию и узнать что происходит. Конечно, можно технически перехватывать вывод, который идет на экран, но это совершенно неправильный способ тестировать софт. Он очень не надёжный. Это связано с тем, что вам придётся парсить строчки и проверять конкретное содержимое внутри них, что совершенно не хорошо.
Гораздо более правильный подход и способ работы здесь – это не печатать на экран результаты каждого хода, а фактически формировать некий <em>log</em>, который представляет из себя список, внутри которого содержится вся необходимая информация связанная с текущим ходом.</p>
<p>Во-первых, это позволяет нам, как минимум, гораздо легче и удобнее анализировать, то что происходит.
Во-вторых, у нас отсутствуют побочные эффекты и функция всегда возвращает некий чистый результат, то есть она сама по себе является чистой, что очень хорошо. Мы можем её повторно запускать, безопасно анализировать и такой код проще всего тестируется.</p>
<p>Ниже мы можем посмотреть первый тест. Самый очевидный и простой – это проверка длины лога, то что он равен 5.</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">assert.equal(length(log), 5)</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Наверно проще данного теста не написать. Ну может быть только проверить, что <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">log</code> является списком, но это не нужно, потому что это по интерфейсу и так должно быть очевидно. В любом случае здесь бы упало с ошибкой, если бы <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">log</code> не был списком.
Итак, у нас получилось 5 ходов, давайте же теперь посмотрим какие это ходы.</p>
<h2 id="heading-2-3">Ходы</h2>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">// step ((health1, health2), message)
const step1 = get(0, log)
assert.equal(toString(car(step1)), '(10, 10)')
const step2 = get(1, log)
assert.equal(toString(car(step2)), '(10, 4)')
const step3 = get(2, log)
assert.equal(toString(car(step3)), '(4, 4)')
const step4 = get(3, log)
assert.equal(toString(car(step4)), '(4, -2)')
const step5 = get(4, log)
assert.equal(toString(car(step5)), '(4, -2)')</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Здесь мы видим следующий набор тестов, который тоже должен присутствовать (и он присутствует) в наших тестах, в котором мы извлекаем из лога конкретные шаги (элементы) и проверяем, что происходит на каждом шаге.</p>
<p>Перед тем, как мы посмотрим, непосредственно, как происходит сравнение и что внутри, давайте оценим из чего состоит каждый шаг.</p>
<p><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">log</code> – это набор шагов, который приводит к завершению игры. Каждый шаг – это пара, внутри которой 2 элемента.</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">// step ((health1, health2), message)</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Первый элемент – тоже пара, потому что у нас пока только пары для того, чтобы делать какие-то сложно-составные структуры.
Второй – это <em>message</em>. Message – это как раз то самое сообщение, которое мы видели в самом начале, что кто-то кого-то убил, то есть это просто текстовое сообщение.
Первый элемент (который пара) содержит здоровье первого игрока и здоровье второго игрока. Это нам нужно как раз для того, чтобы мы могли оценивать “<em>а правильно ли работает логика нашей программы?</em>” и мы это используем в тестах.</p>
<p>Теперь давайте посмотрим, как мы это используем. У нас есть функция <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">get</code>, которая определена в пакете для работы со списками. Она извлекает элемент по индексу и здесь мы её просим дать нам первый элемент.</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">const step1 = get(0, log)</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Ну и соответственно второй, третий, четвёртый, пятый, то есть как раз все 5 элементов.</p>
<p>А почему их 5? Если мы вспомним был тест, который как раз проверяет, что их 5.</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">assert.equal(length(log), 5)</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Это связано с тем, что текущий уровень жизни у всех мы выставили равным <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">10</code>, а <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">damage</code> (так называемый урон) у нас равен цифре <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">6</code>. И давайте теперь посмотрим, что происходит в таком случае.</p>
<p>В начале, когда происходит старт игры, мы получаем <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">message</code>, который называется <strong>начало игры</strong> и первый элемент в паре, который является парой со здоровьем по умолчанию, то есть это <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">10 10</code>. В <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">log</code> мы складываем не сам ход, а начальное состояние игры.</p>
<p>Как мы извлекаем здоровье?</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">assert.equal(toString(car(step1)), '(10, 10)')</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Из <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">step</code> берем <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">car</code> (это первый элемент) и просто превращаем его в строчку, для того, чтобы сравнить и используем <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">assert.equal</code>, который проверяет, что левое значение должно совпадать с правым. То есть здесь слева <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">actual</code> – то, что пришло на самом деле, а справа <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">expected</code> (10, 10) – то, что мы ожидаем. И таким вот образом мы сравниваем каждый элемент.</p>
<p>Теперь мы можем сосредоточиться только на части <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">expected</code> (10, 10) и посмотреть, как изменяются жизни в нашей текущей игре.</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">assert.equal(toString(car(step1)), '(10, 10)')
assert.equal(toString(car(step2)), '(10, 4)')
assert.equal(toString(car(step3)), '(4, 4)')
assert.equal(toString(car(step4)), '(4, -2)')
assert.equal(toString(car(step5)), '(4, -2)')</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Сначала у обоих игроков по <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">10</code>, после этого применяется урон, который был равен <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">6</code> ко второму игроку и соответственно мы получаем <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">10, 4</code>. После этого, поскольку карта у нас одна и её урон <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">6</code>, она применяется к первому игроку – мы получаем <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">4, 4</code>. Затем снова применяется ко второму и у нас получается <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">4, -2</code>, это 4-й шаг. На 5 шаге видно, что здесь абсолютно тот же самый вывод, но почему? Потому что 5-й шаг, так же как и 1-й – это специальный шаг, в котором пишется сообщение, что кто-то был убит и здоровье показывается ровно такое какое было на предыдущем шаге, потому что здесь уже никому не наносится урон и мы просто фиксируем некое состояние, в котором закончилась наша игра.</p>
<h2 id="heading-2-4">Свойства</h2>
<p>Какими свойствами обладает наш дизайн, то что мы сделали и написали и какими свойствами обладает наш тестовый пакет?</p>
<ul>
<li>Интерфейс – одна функция (make)</li>
<li>Автоматическая игра</li>
<li>Логика покрыта тестами</li>
</ul>
<p>Во-первых, весь интерфейс – это по сути одна функция <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">make</code>, то есть публичный интерфейс, в котором происходит работа, который в свою очередь генерирует функцию <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">game</code> и она уже проигрывает нам какую-то игру.</p>
<p>Ранее также говорилось, что игра автоматическая, у нас нет никакой возможности манипулировать ходом игры просто потому, что это не нужно. На текущий момент это отвлечёт нас от самой задачи и слишком сильно её усложнит.</p>
<p>Ну и как мы заметили логика покрыта тестами. То есть после того, как мы пишем эти тесты мы уже знаем какой у нас будет дизайн, мы легко можем запускать, перезапускать и дописывать наш код и самое главное, что в будущем эти тесты будут нам постоянно помогать. После любого рефакторинга, после любого изменения – мы уже всегда гарантированно сможем проверить, что результат такой, как мы ожидаем.</p></div><div style="margin-block:var(--mantine-spacing-xl)" class=""><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md)" class="m_8a5d1357 mantine-Title-root" data-order="2">Рекомендуемые программы</h2><style data-mantine-styles="inline">.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xs);--carousel-slide-size:70%;}@media(min-width: 36em){.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xl);--carousel-slide-size:50%;}}</style><div style="--carousel-control-size:calc(2.5rem * var(--mantine-scale));--carousel-controls-offset:var(--mantine-spacing-sm);margin-bottom:var(--mantine-spacing-lg);padding-block:var(--mantine-spacing-sm);background:var(--app-color-surface)" class="m_17884d0f mantine-Carousel-root responsiveClassName" data-orientation="horizontal" data-include-gap-in-size="true"><div class="m_39bc3463 mantine-Carousel-controls" data-orientation="horizontal"><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="previous" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="next" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(-90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button></div><div class="m_a2dae653 mantine-Carousel-viewport" data-type="media"><div class="m_fcd81474 mantine-Carousel-container __m__-_R_2mremqrdub_" data-orientation="horizontal"><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/js-sicp?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">1 месяц</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">СИКП на JS</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Навык фундаментального программирования</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzc2MCwicHVyIjoiYmxvYl9pZCJ9fQ==--9348098e4053d798b6f34bee4ef66947540261e4/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Low%20code%20development-rafiki.png" alt="СИКП на JS" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 3 900 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md);font-size:var(--mantine-font-size-h3)" class="m_8a5d1357 mantine-Title-root" data-order="2" data-responsive="true">Каталог</h2><p style="margin-bottom:auto" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полный список доступных курсов по разным направлениям</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="/vite/assets/development-BVihs_d5.png" alt="Orientation"/></div></div></div></a></div></div></div></div></div></div></div></div></div><style data-mantine-styles="inline">.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:8.333333333333334%;--col-max-width:8.333333333333334%;}@media(min-width: 48em){.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:16.666666666666668%;--col-max-width:16.666666666666668%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem" class="m_96bdd299 mantine-Grid-col __m__-_R_1bdub_"><div style="margin-inline:var(--mantine-spacing-xs)" class="mantine-visible-from-sm"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-lg);text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/ddp/lessons/game_design/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label"><span style="margin-inline-end:var(--mantine-spacing-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Дальше</span>→</span></span></a><a style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Навигация по теме</span><span class="m_57492dcc mantine-NavLink-description">Теория</span></div><span class="m_690090b5 mantine-NavLink-section" data-position="right"></span></a><div style="margin-block:var(--mantine-spacing-lg)" class="m_3eebeb36 mantine-Divider-root" data-orientation="horizontal" role="separator"></div><div style="margin-block:var(--mantine-spacing-lg)" class=""><div style="justify-content:space-between;margin-bottom:calc(0.1875rem * var(--mantine-scale));color:var(--mantine-color-dimmed);font-size:var(--mantine-font-size-xs)" class="m_8bffd616 mantine-Flex-root __m__-_R_qimrbdub_"><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Завершено</p><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">0 / 9</p></div><div style="--progress-size:var(--progress-size-sm)" class="m_db6d6462 mantine-Progress-root" data-size="sm"><div style="--progress-section-size:0%;--progress-section-color:var(--mantine-color-gray-filled)" class="m_2242eb65 mantine-Progress-section" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="0" aria-valuetext="0%"></div></div></div><button style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root" type="button"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Обсуждения (архив)</span><span class="m_57492dcc mantine-NavLink-description"></span></div></button><div style="--toc-bg:var(--mantine-color-blue-light);--toc-color:var(--mantine-color-blue-light-color);--toc-size:var(--mantine-font-size-sm);--toc-radius:var(--mantine-radius-sm);margin-top:var(--mantine-spacing-xl)" class="m_bcaa9990 mantine-TableOfContents-root" data-variant="light" data-size="sm"></div></div><div class="mantine-hidden-from-sm"><div style="--stack-gap:0rem;--stack-align:stretch;--stack-justify:flex-start" class="m_6d731127 mantine-Stack-root"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-xs);padding:0rem;text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/ddp/lessons/game_design/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">→</span></span></a><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" data-disabled="true" type="button" disabled=""><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></span></button><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto mantine-active m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" type="button"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></span></button></div></div></div></div></div></div></div>
</main>
<footer class="bg-dark fw-light text-light px-3 py-5">
<div class="row small">
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 mb-3">Хекслет</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/about">О нас</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/testimonials">Отзывы</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://b2b.hexlet.io" role="button">Корпоративное обучение</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/blog">Блог</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/qna">Вопросы и ответы</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/glossary">Глоссарий</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://help.hexlet.io" data-target="_blank" role="button">Справка</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" target="_blank" rel="noopener noreferrer" href="/map">Карта сайта</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 fw-normal mb-3">Направления</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_devops">DevOps
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_data_analytics">Аналитика
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_backend_development">Бэкенд
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_programming">Программирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_testing">Тестирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_front_end_dev">Фронтенд
</a></li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Профессии</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/go">Go-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/java">Java-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python">Python-разработчик </a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/data-analytics">Аналитик данных</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/qa-engineer">Инженер по ручному тестированию</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php">РНР-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/frontend">Фронтенд-разработчик</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Навыки</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python-django-developer">Django</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/docker">Docker</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php-laravel-developer">Laravel</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/postman">Postman</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-react-developer">React</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-rest-api">REST API в Node.js</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/spring-boot">Spring Boot</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/typescript">Typescript</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-4 col-md-2">
<div class="fs-4">
<ul class="list-unstyled d-flex">
<li class="me-3">
<a aria-label="Telegram" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://t.me/hexlet_ru"><span class="bi bi-telegram"></span>
</a></li>
<li>
<a aria-label="Youtube" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://www.youtube.com/user/HexletUniversity"><span class="bi bi-youtube"></span>
</a></li>
</ul>
</div>
<div class="mb-2 d-flex flex-column">
<a class="link-light text-decoration-none" rel="nofollow" href="mailto:support@hexlet.io">support@hexlet.io</a>
<a class="link-light text-decoration-none py-2" target="_blank" href="https://t.me/hexlet_help_bot">t.me/hexlet_help_bot</a>
</div>
<ul class="list-unstyled d-flex">
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://hexlet.io/locale/switch?new_locale=en" data-target="_self" role="button"><span class="my-auto">EN</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 opacity-100 external-link" rel="nofollow" data-href="https://ru.hexlet.io/locale/switch?new_locale=ru" data-target="_self" role="button"><span class="my-auto">RU</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://kz.hexlet.io/locale/switch?new_locale=kz" data-target="_self" role="button"><span class="my-auto">KZ</span>
</span></li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<ul class="list-unstyled fs-4">
<li class="mb-3">
<a class="link-light text-decoration-none" href="tel:8%20800%20100%2022%2047">8 800 100 22 47</a>
<span class="d-block opacity-50 small">бесплатно по РФ</span>
</li>
<li>
<a class="link-light text-decoration-none" href="tel:%2B7%20495%20085%2021%2062">+7 495 085 21 62</a>
<span class="d-block opacity-50 small">бесплатно по Москве</span>
</li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<div class="small mb-3">Образовательные услуги оказываются на основании Л035-01298-77/01989008 от 14.03.2025</div>
<ul class="list-unstyled small">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/legal">Правовая информация</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/offer">Оферта</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/license">Лицензия</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/contacts">Контакты</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-12 col-md-4 small">
<div class="mb-2">
<div>ООО «<a href="/" class="text-decoration-none link-light">Хекслет Рус</a>»</div>
<div>108813 г. Москва, вн.тер.г. поселение Московский,</div>
<div>г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3</div>
<div>ОГРН 1217300010476</div>
<div>ИНН 7325174845</div>
</div>
<hr>
<div>АНО ДПО «<a href="/" class="text-decoration-none link-light">Учебный центр «Хекслет</a>»</div>
<div>119331 г. Москва, вн. тер. г. муниципальный округ</div>
<div>Ломоносовский, пр-кт Вернадского, д. 29</div>
<div>ОГРН 1247700712390</div>
<div>ИНН 7736364948</div>
</div>
</div>
</footer>
<div id="root-assistant-offcanvas"></div>
<script src="/vite/assets/assistant-CdBlNCiQ.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/MarkdownBlock-DbyKWoR_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/shiki-V011pkdv.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-XR8Qr8kR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dist-GCHh59xr.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useIsomorphicEffect-HJ6VK0D3.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-KSp6QbZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/classnames-l6ipYlLR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/debounce-jMQ_Cf4f.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"d11015b65d11429ea6b4a2ef37dd7e0b","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>