Como criar um jogo de tabuleiro com agentes do TensorFlow e Flutter

1. Antes de começar

A incrível inovação do AlphaGo e na AlphaStar demonstrou o potencial de usar aprendizado de máquina para criar agentes de jogos de níveis sobre-humanos. Criar um pequeno jogo com tecnologia de ML é um exercício divertido para aprender as habilidades necessárias para criar agentes de jogo poderosos.

Neste codelab, você vai aprender a criar um jogo de tabuleiro usando:

  • Agente do TensorFlow para treinar um agente de jogo com aprendizado por reforço
  • TensorFlow Serving para disponibilizar o modelo
  • Flutter para criar um app de jogo de tabuleiro multiplataforma

Pré-requisitos

  • Conhecimento básico de desenvolvimento do Flutter com Dart
  • Conhecimento básico de machine learning com o TensorFlow, como treinamento ou implantação
  • Conhecimento básico de Python, terminais e Docker

O que você vai aprender

  • Como treinar um agente de personagem não jogador (NPC) usando agentes do TensorFlow
  • Como disponibilizar o modelo treinado usando o TensorFlow Serving
  • Como criar um jogo de tabuleiro multiplataforma do Flutter

O que é necessário

2. Jogo The Plane Strike

O jogo criado neste codelab é chamado de Plane Strike, um pequeno jogo de tabuleiro para duas pessoas semelhante ao Battleship. As regras são muito simples:

  • O jogador humano joga contra um agente NPC treinado por machine learning. O jogador humano pode iniciar o jogo tocando em qualquer célula no tabuleiro do agente.
  • No começo do jogo, o jogador humano e o agente têm um "plano" (oito células verdes que formam um "plano", como você pode ver no tabuleiro do jogador humano na animação abaixo) nos seus próprios tabuleiros; esses "aviões" são colocados aleatoriamente e visíveis apenas para os donos do tabuleiro e escondidos para seus oponentes.
  • O jogador humano e o agente se revezam para atacar uma célula do tabuleiro um do outro. O jogador humano pode tocar em qualquer célula no quadro do agente, enquanto o agente faz a escolha automaticamente com base na previsão de um modelo de machine learning. A célula desejada fica vermelha se for um "plano" célula (‘hit'); caso contrário, ele fica amarelo ("perder").
  • Vence o jogo quem atingir 8 células vermelhas primeiro. o jogo é reiniciado com novos tabuleiros.

Aqui está um exemplo de jogabilidade:

77cead530c5a4aff.gif

3. Configurar o ambiente de desenvolvimento do Flutter

Para desenvolver o Flutter, você precisa de dois softwares para concluir este codelab: o SDK do Flutter e um editor (links em inglês).

É possível executar o codelab usando qualquer um destes dispositivos:

  • O simulador para iOS, que exige a instalação de ferramentas do Xcode.
  • O Android Emulator, que requer configuração no Android Studio.
  • Um navegador (o Chrome é necessário para depuração).
  • Como um aplicativo para computador Windows, Linux ou macOS. Você precisa desenvolver na plataforma em que planeja implantar. Portanto, se quiser desenvolver um app para um computador Windows, você terá que desenvolver no Windows para acessar a cadeia de builds adequada. Há requisitos específicos de cada sistema operacional que são abordados em detalhes em docs.flutter.dev/desktop.

4. Começar a configuração

Para fazer o download do código para este codelab, faça o seguinte:

  1. Navegue até o repositório do GitHub deste codelab.
  2. Clique em Code > Download zip para fazer o download de todo o código para este codelab.

2cd45599f51fb8a2.png

  1. Descompacte o arquivo ZIP salvo para acessar uma pasta raiz codelabs-main com todos os recursos necessários.

Para este codelab, você só precisa dos arquivos no subdiretório tfagents-flutter/ do repositório, que contém várias pastas:

  • As pastas de step0 a step6 contêm o código inicial baseado em cada etapa deste codelab.
  • A pasta finished contém o código concluído do app de exemplo finalizado.
  • Cada pasta contém uma subpasta backbend, que inclui o código de back-end, e uma subpasta frontend, que inclui o código de front-end do Flutter.

5. Fazer o download das dependências do projeto

Back-end

Abra o terminal e acesse a subpasta tfagents-flutter. Execute o comando a seguir:

pip install -r requirements.txt

Front-end

  1. No VS Code, clique em File > Abra a pasta e selecione a pasta step0 no código-fonte que você salvou antes.
  2. Abra step0/frontend/lib/main.dart. Se aparecer uma caixa de diálogo do VS Code solicitando o download dos pacotes necessários para o app inicial, clique em Get packages.
  3. Se a caixa de diálogo não aparecer, abra o terminal e execute o comando flutter pub get na pasta step0/frontend.

7ada07c300f166a6.png

6. Etapa 0: executar o app inicial

  1. Abra o arquivo step0/frontend/lib/main.dart no VS Code, verifique se o Android Emulator ou o simulador de iOS está configurado corretamente e aparece na barra de status.

Por exemplo, veja o que você verá ao usar o Pixel 5 com o Android Emulator:

9767649231898791.png

Veja o que você verá ao usar o iPhone 13 com o iOS Simulator:

95529e3a682268b2.png

  1. Clique em a19a0c68bc4046e6.png Iniciar depuração.

Executar e explorar o app

O app será iniciado no Android Emulator ou no iOS Simulator. A interface é bem direta. Há dois tabuleiros de jogos: um jogador humano pode tocar em qualquer célula no tabuleiro do agente na parte superior como uma posição de ataque. Você vai treinar um agente inteligente para prever automaticamente onde atacar com base no tabuleiro humano.

Internamente, o app Flutter envia o tabuleiro atual do jogador humano para o back-end, que executa um modelo de aprendizado por reforço e retorna uma posição de célula prevista para atacar em seguida. O front-end vai exibir o resultado na interface depois de receber a resposta.

734ab3d48a1133e1.png 15cba2e741149c95.png

Se você clicar em qualquer célula no quadro do agente agora, nada acontecerá porque o app ainda não pode se comunicar com o back-end.

7. Etapa 1: criar um ambiente Python do TensorFlow Agents

O principal objetivo deste codelab é projetar um agente que aprenda interagindo com um ambiente. Embora o jogo Plane Strike seja relativamente simples e seja possível criar regras para o agente NPC, você usa o aprendizado por reforço para treinar o agente e aprender as habilidades dele e criar facilmente agentes para outros jogos no futuro.

Na configuração padrão do aprendizado por reforço (RL, na sigla em inglês), o agente recebe uma observação em cada etapa de tempo e escolhe uma ação. A ação é aplicada ao ambiente, que retorna uma recompensa e uma nova observação. O agente treina uma política para escolher ações e maximizar a soma das recompensas, também conhecidas como devoluções. Ao jogar muitas vezes, o agente consegue aprender os padrões e aprimorar as habilidades para dominar o jogo. Para formular o jogo Plane Strike como um problema de RL, pense no estado do tabuleiro como a observação, uma posição de ataque como a ação e o sinal de acertar/agarrar como a recompensa.

bc5da07bc45062f4.png

Para treinar o agente NPC, use o TensorFlow Agents, uma biblioteca de aprendizado por reforço confiável, escalonável e fácil de usar.

O TF Agents é ótimo para o aprendizado por reforço porque vem com um extenso conjunto de codelabs, exemplos e documentação extensiva para você começar. É possível usar o TF Agents para resolver problemas realistas e complexos de aprendizado por reforço com escalonabilidade e desenvolver novos algoritmos de aprendizado de máquina rapidamente. Alterne facilmente entre diferentes agentes e algoritmos para a experimentação. Ele também é bem testado e fácil de configurar.

Existem muitos ambientes de jogos pré-criados no OpenAI Gym (por exemplo, jogos de Atari), Mujuco e outros, que os agentes do TF podem usar facilmente. Mas como o Plane Strike é totalmente personalizado, primeiro você precisa implementar um novo ambiente do zero.

Para implementar um ambiente Python do TF Agents, é preciso implementar os seguintes métodos:

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."""

A mais importante é a função _step(), que realiza uma ação e retorna um novo objeto time_step. No caso do jogo Plane Strike, você tem um tabuleiro, quando uma nova posição de ataque é aplicada, com base na condição do tabuleiro do jogo, o ambiente identifica:

  • Como deve ser a aparência do tabuleiro do jogo (a célula deve mudar de cor para vermelho ou amarelo, considerando a localização oculta do plano?)
  • Qual recompensa o jogador deve receber por essa posição (recebimento de recompensa ou erro de penalidade?)
  • O jogo deve ser encerrado (alguém ganhou?)
  • Adicione o seguinte código à função _step() ao arquivo _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. Etapa 2: treinar o agente do jogo com os agentes do TensorFlow

Com o ambiente do TF Agents implantado, é possível treinar o agente de jogo. Neste codelab, você vai usar um agente REINFORCE. REINFORCE é um algoritmo de gradiente de política em RL. A ideia básica é ajustar os parâmetros da rede neural de política com base nos sinais de recompensa coletados durante o jogo, para que a rede possa maximizar o retorno em jogadas futuras.

  • Primeiro você precisa instanciar os ambientes de treinamento e avaliação. Adicione este código à função train_agent() no arquivo 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)
  • Em seguida, crie um agente de aprendizado por reforço que será treinado. Neste codelab, você vai usar o agente REINFORCE, que é um agente baseado em políticas. Adicione este código logo abaixo do código acima:
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,
)
  • Por fim, treine o agente no loop de treinamento. No loop, primeiro você coleta alguns episódios de jogadas em um buffer e depois treina o agente com os dados armazenados em buffer. Adicione este código à função train_agent() no arquivo 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()
  • Agora você pode começar o treinamento. No terminal, acesse a pasta step2/backend no computador e execute:
python training.py

O treinamento leva de 8 a 12 horas para ser concluído, dependendo das configurações de hardware. Você não precisa concluir todo o treinamento por conta própria, já que um modelo pré-treinado é fornecido em step3. Enquanto isso, você pode monitorar o progresso com o TensorBoard. Abra um novo terminal, acesse a pasta step2/backend no computador e execute:

tensorboard --logdir tf_agents_log/

tf_agents_log é a pasta que contém o registro de treinamento. Veja abaixo um exemplo de execução de treinamento:

33e12e2b387c063a.png 8488632ccf43348a.png

Observe que a duração média do episódio diminui e o retorno médio aumenta à medida que o treinamento avança. Intuitivamente, é possível entender que, se o agente for mais inteligente e fizer previsões melhores, a duração do jogo ficará menor e o agente coletará mais recompensas. Isso faz sentido porque o agente quer terminar o jogo em menos etapas para minimizar os descontos pesados em recompensas nas etapas posteriores.

Após a conclusão do treinamento, o modelo treinado é exportado para a pasta policy_model.

9. Etapa 3: implantar o modelo treinado com o TensorFlow Serving

Agora que você treinou o agente de jogo, é possível implantá-lo com o TensorFlow Serving.

  • No seu terminal, acesse a pasta step3/backend no computador e inicie o TensorFlow Serving com o 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

Primeiro, o Docker faz o download automático da imagem do TensorFlow Serving em um minuto. Depois disso, o TensorFlow Serving será iniciado. O registro será semelhante a este snippet de código:

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 ...

Você pode enviar um exemplo de solicitação ao endpoint para verificar se está funcionando conforme o esperado:

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

O endpoint vai retornar uma posição prevista 45, que é (5, 5) no centro do tabuleiro (para os curiosos, você pode tentar descobrir por que o centro do tabuleiro é um bom palpite para a primeira posição de golpes).

{
    "predictions": [45]
}

Pronto! Você criou um back-end para prever a próxima posição de aviso do agente NPC.

10. Etapa 4: criar o app do Flutter para Android e iOS

O back-end está pronto. Você pode começar a enviar solicitações a ele para extrair previsões de posição do aviso do app Flutter.

  • Primeiro, defina uma classe que encapsule as entradas a serem enviadas. Adicione este código ao arquivo 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;
  }
}

Agora é possível enviar a solicitação ao TensorFlow Serving para fazer previsões.

  • Adicione este código à função predict() no arquivo 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');
}

Quando o app receber a resposta do back-end, atualize a interface do jogo para refletir o progresso dele.

  • Adicione este código à função _gridItemTapped() no arquivo 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(() {});

Executar

  1. Clique em a19a0c68bc4046e6.png Start debugging e aguarde o app carregar.
  2. Toque em qualquer célula no tabuleiro do agente para iniciar o jogo.

852942d0de299c1f.png 6ae3601470c8e33a.png

11. Etapa 5: ativar o app do Flutter para as plataformas para computador

Além do Android e iOS, o Flutter também oferece suporte a plataformas de computador, incluindo Linux, Mac e Windows.

Linux

  1. Verifique se o dispositivo de destino está definido como 86cba523de82b4f9.png na barra de status do VSCode.
  2. Clique em a19a0c68bc4046e6.png Start debugging e aguarde o app carregar.
  3. Clique em qualquer célula no tabuleiro do agente para iniciar o jogo.

48594c7c0a589733.png

Mac

  1. No Mac, você precisa configurar os direitos apropriados, já que o app vai enviar solicitações HTTP para o back-end. Consulte Direitos e aplicativos sandbox para mais detalhes.

Adicione este código a step4/frontend/macOS/Runner/DebugProfile.entitlements e step4/frontend/macOS/Runner/Release.entitlements, respectivamente:

<key>com.apple.security.network.client</key>
<true/>
  1. Verifique se o dispositivo de destino está definido como eb4b0b5563824138.png na barra de status do VSCode.
  2. Clique em a19a0c68bc4046e6.png Start debugging e aguarde o app carregar.
  3. Clique em qualquer célula no tabuleiro do agente para iniciar o jogo.

55a5de3674194e89.png

Windows

  1. Verifique se o dispositivo de destino está definido como 9587be1bb375bc0f.png na barra de status do VSCode.
  2. Clique em a19a0c68bc4046e6.png Start debugging e aguarde o app carregar.
  3. Clique em qualquer célula no tabuleiro do agente para iniciar o jogo.

41d9f87d84c5e755.png

12. Etapa 6: ativar o app do Flutter para a plataforma da Web

Mais uma coisa que você pode fazer é adicionar suporte da Web ao app Flutter. Por padrão, a plataforma da Web é ativada automaticamente para apps Flutter, portanto, tudo o que você precisa fazer é iniciá-la.

  1. Verifique se o dispositivo de destino está definido como 71db93efa928d15d.png na barra de status do VSCode.
  2. Clique em a19a0c68bc4046e6.png Start debugging e aguarde o app carregar no navegador Chrome.
  3. Clique em qualquer célula no tabuleiro do agente para iniciar o jogo.

fae7490304e28dfe.png

13. Parabéns

Você criou um app de jogo de tabuleiro com um agente com tecnologia de ML para jogar contra um usuário humano.

Saiba mais