Создание настольной игры с помощью агентов TensorFlow и Flutter.

1. Прежде чем начать

Удивительный прорыв AlphaGo и AlphaStar продемонстрировал потенциал использования машинного обучения для создания игровых агентов сверхчеловеческого уровня. Создание небольшой игры на базе машинного обучения — это увлекательное упражнение, позволяющее приобрести навыки, необходимые для создания мощных игровых агентов.

В этой лаборатории вы узнаете, как создать настольную игру, используя:

  • Агент TensorFlow для обучения игрового агента с помощью обучения с подкреплением
  • TensorFlow служит для обслуживания модели
  • Flutter создаст кроссплатформенное приложение для настольных игр

Предварительные условия

  • Базовые знания разработки Flutter с помощью Dart.
  • Базовые знания машинного обучения с TensorFlow, например, обучение или развертывание.
  • Базовые знания Python, терминалов и Docker.

Что вы узнаете

  • Как обучить агента неигрового персонажа (NPC) с помощью агентов TensorFlow
  • Как обслуживать обученную модель с помощью TensorFlow Serving
  • Как создать кроссплатформенную настольную игру Flutter

Что вам понадобится

2. Игра «Самолетный удар»

Игра, которую вы создадите в этой кодовой лаборатории, называется «Plane Strike» — небольшая настольная игра для двух игроков, напоминающая настольную игру «Морской бой» . Правила очень просты:

  • Игрок-человек играет против агента NPC, обученного машинным обучением. Игрок-человек может начать игру, коснувшись любой ячейки на доске агента.
  • В начале игры у игрока-человека и у агента есть объект «плоскость» (8 зеленых ячеек, образующих «плоскость», как вы можете видеть на доске игрока-человека в анимации ниже) на своих досках; эти «самолеты» располагаются случайным образом и видны только владельцам доски и скрыты для их противников.
  • Игрок-человек и агент по очереди наносят удары по одной клетке доски друг друга. Игрок-человек может коснуться любой ячейки на доске агента, а агент автоматически сделает выбор на основе прогнозов модели машинного обучения. Попытавшаяся ячейка становится красной, если это «плоская» ячейка («попадание»); в противном случае он становится желтым («промахивается»).
  • Тот, кто первым наберет 8 красных клеток, выигрывает игру; затем игра возобновляется с новыми досками.

Вот пример игрового процесса:

77cead530c5a4aff.gif

3. Настройте среду разработки Flutter.

Для разработки Flutter вам понадобятся две части программного обеспечения для выполнения этой лаборатории — Flutter SDK и редактор .

Вы можете запустить кодовую лабораторию, используя любое из этих устройств:

  • Симулятор iOS (требуется установка инструментов Xcode).
  • Эмулятор Android (требуется установка в Android Studio).
  • Браузер (для отладки необходим Chrome).
  • В качестве настольного приложения для Windows , Linux или macOS . Вы должны разрабатывать на платформе, на которой планируете развернуть. Итак, если вы хотите разработать настольное приложение для Windows, вам необходимо разработать его в Windows, чтобы получить доступ к соответствующей цепочке сборки. Существуют требования, специфичные для операционной системы, которые подробно описаны на docs.flutter.dev/desktop .

4. Настройте

Чтобы загрузить код для этой лаборатории кода:

  1. Перейдите в репозиторий GitHub для этой лаборатории кода.
  2. Нажмите «Код» > «Загрузить zip» , чтобы загрузить весь код для этой лаборатории кода.

2cd45599f51fb8a2.png

  1. Разархивируйте загруженный zip-файл, чтобы распаковать codelabs-main со всеми необходимыми ресурсами.

Для этой лаборатории кода вам понадобятся только файлы в подкаталоге tfagents-flutter/ репозитория, который содержит несколько папок:

  • Папки step0 по step6 содержат начальный код, который вы используете для каждого шага этой лаборатории кода.
  • finished папка содержит готовый код готового примера приложения.
  • Каждая папка содержит подпапку backbend , которая включает в себя внутренний код, и подпапку frontend , которая включает в себя код внешнего интерфейса Flutter.

5. Загрузите зависимости для проекта.

Бэкэнд

Откройте терминал и перейдите в подпапку tfagents-flutter . Запустите следующее:

pip install -r requirements.txt

Внешний интерфейс

  1. В VS Code нажмите «Файл» > «Открыть папку» , а затем выберите папку step0 из исходного кода, который вы скачали ранее.
  2. Откройте файл step0/frontend/lib/main.dart . Если вы увидите диалоговое окно VS Code, предлагающее загрузить необходимые пакеты для начального приложения, нажмите « Получить пакеты» .
  3. Если вы не видите это диалоговое окно, откройте терминал и затем запустите команду flutter pub get в папке step0/frontend .

7ada07c300f166a6.png

6. Шаг 0. Запустите стартовое приложение.

  1. Откройте файл step0/frontend/lib/main.dart в VS Code, убедитесь, что эмулятор Android или симулятор iOS правильно настроен и отображается в строке состояния.

Например, вот что вы видите при использовании Pixel 5 с эмулятором Android:

9767649231898791.png

Вот что вы видите, когда используете iPhone 13 с симулятором iOS:

95529e3a682268b2.png

  1. Нажмите a19a0c68bc4046e6.png Запустите отладку .

Запустите и изучите приложение

Приложение должно запуститься на вашем эмуляторе Android или симуляторе iOS. Пользовательский интерфейс довольно прост. Есть 2 игровые доски; игрок-человек может использовать любую ячейку на доске агента вверху в качестве позиции для удара. Вы научите умного агента автоматически предсказывать, куда нанести удар, основываясь на доске игрока-человека.

Под капотом приложение Flutter отправляет текущую доску игрока-человека на серверную часть, которая запускает модель обучения с подкреплением и возвращает прогнозируемую позицию ячейки для следующего удара. Интерфейс отобразит результат в пользовательском интерфейсе после получения ответа.

734ab3d48a1133e1.png15cba2e741149c95.png

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

7. Шаг 1. Создайте среду Python для агентов TensorFlow Agents.

Основная цель этой лаборатории — разработать агент, который обучается путем взаимодействия со средой. Хотя игра Plane Strike относительно проста и в ней можно вручную создавать правила для агента NPC, вы используете обучение с подкреплением для обучения агента, чтобы вы освоили навыки и могли легко создавать агентов для других игр в будущем.

В стандартной настройке обучения с подкреплением (RL) агент получает наблюдение на каждом временном шаге и выбирает действие. Действие применяется к окружающей среде, и среда возвращает награду и новое наблюдение. Агент обучает политику выбора действий для максимизации суммы вознаграждения, также известной как доход. Играя в игру много-много раз, агент может изучить закономерности и отточить свои навыки, чтобы овладеть игрой. Чтобы сформулировать игру Plane Strike как задачу RL, подумайте о состоянии доски как о наблюдении, о позиции удара как о действии, а о сигнале попадания/промаха как о награде.

bc5da07bc45062f4.png

Для обучения агента NPC вы используете агенты TensorFlow — надежную, масштабируемую и простую в использовании библиотеку обучения с подкреплением для TensorFlow.

TF Agents отлично подходит для обучения с подкреплением, поскольку включает в себя обширный набор лабораторных работ по коду, примеров и обширной документации, которая поможет вам начать работу. Вы можете использовать агенты TF для решения реалистичных и сложных проблем RL с возможностью масштабирования и быстрой разработки новых алгоритмов RL. Вы можете легко переключаться между различными агентами и алгоритмами для экспериментов. Он также хорошо протестирован и прост в настройке.

В OpenAI Gym реализовано множество готовых игровых сред (например, игры Atari), Mujuco и т. д., которые агенты TF могут легко использовать. Но поскольку игра Plane Strike представляет собой полностью пользовательскую игру, вам необходимо сначала реализовать новую среду с нуля.

Чтобы реализовать среду Python Agents TF, вам необходимо реализовать следующие методы:

class YourGameEnv(py_environment.PyEnvironment):

  def __init__(self):
    """Initialize environment."""


  def action_spec(self):
    """Return action_spec."""


  def observation_spec(self):
    """Return observation_spec."""


  def _reset(self):
    """Return initial_time_step."""


  def _step(self, action):
    """Apply action and return new time_step."""

Наиболее важной из них является функция _step() , которая выполняет действие и возвращает новый объект time_step . В случае с игрой Plane Strike у вас есть игровое поле; когда появляется новая позиция для удара, в зависимости от состояния игрового поля окружающая среда определяет:

  • Как дальше должно выглядеть игровое поле (должна ли ячейка изменить свой цвет на красный или желтый, учитывая местоположение скрытой плоскости?)
  • Какую награду должен получить игрок за эту позицию (награда за попадание или штраф за промах?)
  • Если игра прекратится (кто-нибудь выиграл?)
  • Добавьте следующий код в функцию _step() в файл _planestrike_py_environment.py :
if self._hit_count == self._plane_size:
    self._episode_ended = True
    return self.reset()

if self._strike_count + 1 == self._max_steps:
    self.reset()
    return ts.termination(
        np.array(self._visible_board, dtype=np.float32), UNFINISHED_GAME_REWARD
    )

self._strike_count += 1
action_x = action // self._board_size
action_y = action % self._board_size
# Hit
if self._hidden_board[action_x][action_y] == HIDDEN_BOARD_CELL_OCCUPIED:
    # Non-repeat move
    if self._visible_board[action_x][action_y] == VISIBLE_BOARD_CELL_UNTRIED:
        self._hit_count += 1
        self._visible_board[action_x][action_y] = VISIBLE_BOARD_CELL_HIT
        # Successful strike
        if self._hit_count == self._plane_size:
            # Game finished
            self._episode_ended = True
            return ts.termination(
                np.array(self._visible_board, dtype=np.float32),
                FINISHED_GAME_REWARD,
            )
        else:
            self._episode_ended = False
            return ts.transition(
                np.array(self._visible_board, dtype=np.float32),
                HIT_REWARD,
                self._discount,
            )
    # Repeat strike
    else:
        self._episode_ended = False
        return ts.transition(
            np.array(self._visible_board, dtype=np.float32),
            REPEAT_STRIKE_REWARD,
            self._discount,
        )
# Miss
else:
    # Unsuccessful strike
    self._episode_ended = False
    self._visible_board[action_x][action_y] = VISIBLE_BOARD_CELL_MISS
    return ts.transition(
        np.array(self._visible_board, dtype=np.float32),
        MISS_REWARD,
        self._discount,

8. Шаг 2. Обучите игровой агент с помощью агентов TensorFlow.

Имея среду TF Agents, вы можете обучать игрового агента. Для этой лаборатории кода вы используете агент REINFORCE. REINFORCE — это алгоритм градиента политики в RL. Его основная идея состоит в том, чтобы корректировать параметры нейронной сети политики на основе сигналов вознаграждения, собранных во время игры, чтобы сеть политики могла максимизировать отдачу в будущих играх.

  • Во-первых, вам необходимо создать экземпляры сред обучения и оценки. Добавьте этот код в функцию train_agent() в файле step2/backend/training.py :
train_py_env = planestrike_py_environment.PlaneStrikePyEnvironment(
    board_size=BOARD_SIZE, discount=DISCOUNT, max_steps=BOARD_SIZE**2
)
eval_py_env = planestrike_py_environment.PlaneStrikePyEnvironment(
    board_size=BOARD_SIZE, discount=DISCOUNT, max_steps=BOARD_SIZE**2
)

train_env = tf_py_environment.TFPyEnvironment(train_py_env)
eval_env = tf_py_environment.TFPyEnvironment(eval_py_env)
  • Далее вам нужно создать агент обучения с подкреплением, который будет обучен. В этой лаборатории кода вы используете агент REINFORCE, который является агентом на основе политик. Добавьте этот код прямо под приведенным выше кодом:
actor_net = tfa.networks.Sequential(
    [
        tfa.keras_layers.InnerReshape([BOARD_SIZE, BOARD_SIZE], [BOARD_SIZE**2]),
        tf.keras.layers.Dense(FC_LAYER_PARAMS, activation="relu"),
        tf.keras.layers.Dense(BOARD_SIZE**2),
        tf.keras.layers.Lambda(lambda t: tfp.distributions.Categorical(logits=t)),
    ],
    input_spec=train_py_env.observation_spec(),
)

optimizer = tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE)

train_step_counter = tf.Variable(0)

tf_agent = reinforce_agent.ReinforceAgent(
    train_env.time_step_spec(),
    train_env.action_spec(),
    actor_network=actor_net,
    optimizer=optimizer,
    normalize_returns=True,
    train_step_counter=train_step_counter,
)
  • Наконец, обучите агента в цикле обучения. В цикле вы сначала собираете в буфер несколько эпизодов игрового процесса, а затем обучаете агента с помощью буферизованных данных. Добавьте этот код в функцию train_agent() в файле step2/backend/training.py :
# Collect a few episodes using collect_policy and save to the replay buffer.
collect_episode(
    train_py_env,
    collect_policy,
    COLLECT_EPISODES_PER_ITERATION,
    replay_buffer_observer,
)

# Use data from the buffer and update the agent's network.
iterator = iter(replay_buffer.as_dataset(sample_batch_size=1))
trajectories, _ = next(iterator)
tf_agent.train(experience=trajectories)
replay_buffer.clear()
  • Теперь вы можете приступить к обучению. В своем терминале перейдите в папку step2/backend на своем компьютере и запустите:
python training.py

Завершение обучения занимает 8–12 часов, в зависимости от конфигурации вашего оборудования (вам не обязательно завершать все обучение самостоятельно, поскольку на step3 предоставляется предварительно обученная модель). Тем временем вы можете следить за прогрессом с помощью TensorBoard . Откройте новый терминал, перейдите в папку step2/backend на вашем компьютере и запустите:

tensorboard --logdir tf_agents_log/

tf_agents_log — это папка, содержащая журнал обучения. Пример тренировочного запуска выглядит следующим образом:

33e12e2b387c063a.png8488632ccf43348a.png

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

После завершения обучения обученная модель экспортируется в папку policy_model .

9. Шаг 3. Разверните обученную модель с помощью TensorFlow Serving.

Теперь, когда вы обучили игровой агент, вы можете развернуть его с помощью TensorFlow Serving.

  • В своем терминале перейдите в папку step3/backend на своем компьютере и запустите TensorFlow Serving с Docker:
docker run -t --rm -p 8501:8501 -p 8500:8500 -v "$(pwd)/backend/policy_model:/models/policy_model" -e MODEL_NAME=policy_model tensorflow/serving

Docker сначала автоматически загружает образ TensorFlow Serving, что занимает минуту. После этого должно начаться обслуживание TensorFlow. Журнал должен выглядеть следующим образом:

2022-05-30 02:38:54.147771: I tensorflow_serving/model_servers/server.cc:89] Building single TensorFlow model file config:  model_name: policy_model model_base_path: /models/policy_model
2022-05-30 02:38:54.148222: I tensorflow_serving/model_servers/server_core.cc:465] Adding/updating models.
2022-05-30 02:38:54.148273: I tensorflow_serving/model_servers/server_core.cc:591]  (Re-)adding model: policy_model
2022-05-30 02:38:54.262684: I tensorflow_serving/core/basic_manager.cc:740] Successfully reserved resources to load servable {name: policy_model version: 123}
2022-05-30 02:38:54.262768: I tensorflow_serving/core/loader_harness.cc:66] Approving load for servable version {name: policy_model version: 123}
2022-05-30 02:38:54.262787: I tensorflow_serving/core/loader_harness.cc:74] Loading servable version {name: policy_model version: 123}
2022-05-30 02:38:54.265010: I external/org_tensorflow/tensorflow/cc/saved_model/reader.cc:38] Reading SavedModel from: /models/policy_model/123
2022-05-30 02:38:54.277811: I external/org_tensorflow/tensorflow/cc/saved_model/reader.cc:90] Reading meta graph with tags { serve }
2022-05-30 02:38:54.278116: I external/org_tensorflow/tensorflow/cc/saved_model/reader.cc:132] Reading SavedModel debug info (if present) from: /models/policy_model/123
2022-05-30 02:38:54.280229: I external/org_tensorflow/tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2022-05-30 02:38:54.332352: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:206] Restoring SavedModel bundle.
2022-05-30 02:38:54.337000: I external/org_tensorflow/tensorflow/core/platform/profile_utils/cpu_utils.cc:114] CPU Frequency: 2193480000 Hz
2022-05-30 02:38:54.402803: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:190] Running initialization op on SavedModel bundle at path: /models/policy_model/123
2022-05-30 02:38:54.410707: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:277] SavedModel load for tags { serve }; Status: success: OK. Took 145695 microseconds.
2022-05-30 02:38:54.412726: I tensorflow_serving/servables/tensorflow/saved_model_warmup_util.cc:59] No warmup data file found at /models/policy_model/123/assets.extra/tf_serving_warmup_requests
2022-05-30 02:38:54.417277: I tensorflow_serving/core/loader_harness.cc:87] Successfully loaded servable version {name: policy_model version: 123}
2022-05-30 02:38:54.419846: I tensorflow_serving/model_servers/server_core.cc:486] Finished adding/updating models
2022-05-30 02:38:54.420066: I tensorflow_serving/model_servers/server.cc:367] Profiler service is enabled
2022-05-30 02:38:54.428339: I tensorflow_serving/model_servers/server.cc:393] Running gRPC ModelServer at 0.0.0.0:8500 ...
[warn] getaddrinfo: address family for nodename not supported
2022-05-30 02:38:54.431620: I tensorflow_serving/model_servers/server.cc:414] Exporting HTTP/REST API at:localhost:8501 ...
[evhttp_server.cc : 245] NET_LOG: Entering the event loop ...

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

curl -d '{"signature_name":"action","instances":[{"0/discount":0.0,"0/observation":[[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,     0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]],"0/reward":0.0,"0/step_type":0}]}'     -X POST http://localhost:8501/v1/models/policy_model:predict

Конечная точка вернет прогнозируемую позицию 45 , то есть (5, 5) в центре доски (любопытные могут попытаться выяснить, почему центр доски является хорошим предположением для позиции первого удара).

{
    "predictions": [45]
}

Вот и все! Вы успешно создали серверную часть для прогнозирования позиции следующего удара для агента NPC.

10. Шаг 4. Создайте приложение Flutter для Android и iOS.

Бэкэнд готов. Вы можете начать отправлять ему запросы для получения прогнозов позиции удара из приложения Flutter.

  • Во-первых, вам нужно определить класс, который упаковывает входные данные для отправки. Добавьте этот код в файл step4/frontend/lib/game_agent.dart :
class Inputs {
  final List<double> _boardState;
  Inputs(this._boardState);

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = <String, dynamic>{};
    data['0/discount'] = [0.0];
    data['0/observation'] = [_boardState];
    data['0/reward'] = [0.0];
    data['0/step_type'] = [0];
    return data;
  }
}

Теперь вы можете отправить запрос в TensorFlow Serving, чтобы сделать прогнозы.

  • Добавьте этот код в функцию predict() в файле step4/frontend/lib/game_agent.dart :
var flattenedBoardState = boardState.expand((i) => i).toList();
final response = await http.post(
  Uri.parse('http://$server:8501/v1/models/policy_model:predict'),
  body: jsonEncode(<String, dynamic>{
    'signature_name': 'action',
    'instances': [Inputs(flattenedBoardState)]
  }),
);

if (response.statusCode == 200) {
  var output = List<int>.from(
      jsonDecode(response.body)['predictions'] as List<dynamic>);
  return output[0];
} else {
  throw Exception('Error response');
}

Как только приложение получает ответ от серверной части, вы обновляете пользовательский интерфейс игры, чтобы он отражал ход игры.

  • Добавьте этот код в функцию _gridItemTapped() в файле step4/frontend/lib/main.dart :
int agentAction =
    await _policyGradientAgent.predict(_playerVisibleBoardState);
_agentActionX = agentAction ~/ _boardSize;
_agentActionY = agentAction % _boardSize;
if (_playerHiddenBoardState[_agentActionX][_agentActionY] ==
    hiddenBoardCellOccupied) {
  // Non-repeat move
  if (_playerVisibleBoardState[_agentActionX][_agentActionY] ==
      visibleBoardCellUntried) {
    _agentHitCount++;
  }
  _playerVisibleBoardState[_agentActionX][_agentActionY] =
      visibleBoardCellHit;
} else {
  _playerVisibleBoardState[_agentActionX][_agentActionY] =
      visibleBoardCellMiss;
}
setState(() {});

Запусти это

  1. Нажмите a19a0c68bc4046e6.png Запустите отладку и дождитесь загрузки приложения.
  2. Коснитесь любой ячейки на доске агента, чтобы начать игру.

852942d0de299c1f.png6ae3601470c8e33a.png

11. Шаг 5. Включите приложение Flutter для настольных платформ.

Помимо Android и iOS, Flutter также поддерживает настольные платформы, включая Linux, Mac и Windows.

Линукс

  1. Убедитесь, что на целевом устройстве установлено 86cba523de82b4f9.png в строке состояния VSCode.
  2. Нажмите a19a0c68bc4046e6.png Запустите отладку и дождитесь загрузки приложения.
  3. Нажмите любую ячейку на доске агента, чтобы начать игру.

48594c7c0a589733.png

Мак

  1. Для Mac вам необходимо настроить соответствующие права, поскольку приложение будет отправлять HTTP-запросы на серверную часть. Дополнительную информацию см. в разделах «Права» и «Песочница приложения» .

Добавьте этот код в step4/frontend/macOS/Runner/DebugProfile.entitlements и step4/frontend/macOS/Runner/Release.entitlements соответственно:

<key>com.apple.security.network.client</key>
<true/>
  1. Убедитесь, что на целевом устройстве установлено значение eb4b0b5563824138.png в строке состояния VSCode.
  2. Нажмите a19a0c68bc4046e6.png Запустите отладку и дождитесь загрузки приложения.
  3. Нажмите любую ячейку на доске агента, чтобы начать игру.

55a5de3674194e89.png

Окна

  1. Убедитесь, что на целевом устройстве установлено значение 9587be1bb375bc0f.png в строке состояния VSCode.
  2. Нажмите a19a0c68bc4046e6.png Запустите отладку и дождитесь загрузки приложения.
  3. Нажмите любую ячейку на доске агента, чтобы начать игру.

41d9f87d84c5e755.png

12. Шаг 6. Включите приложение Flutter для веб-платформы.

Еще одна вещь, которую вы можете сделать, — это добавить веб-поддержку в приложение Flutter. По умолчанию веб-платформа автоматически включается для приложений Flutter, поэтому все, что вам нужно сделать, это запустить ее.

  1. Убедитесь, что на целевом устройстве установлено 71db93efa928d15d.png в строке состояния VSCode.
  2. Нажмите a19a0c68bc4046e6.png Запустите отладку и дождитесь загрузки приложения в браузере Chrome.
  3. Нажмите любую ячейку на доске агента, чтобы начать игру.

fae7490304e28dfe.png

13. Поздравления

Вы создали приложение для настольной игры с агентом на базе машинного обучения, чтобы играть против игрока-человека!

Узнать больше