1. 시작하기 전에
AlphaGo와 AlphaStar의 놀라운 혁신은 머신러닝을 사용하여 초인적인 수준의 게임 에이전트를 빌드할 수 있는 잠재력을 보여주었습니다. 강력한 게임 에이전트를 만드는 데 필요한 기술을 익히기 위해 작은 ML 기반 게임을 만드는 것은 재미있는 연습입니다.
이 Codelab에서는 다음을 사용하여 보드 게임을 빌드하는 방법을 알아봅니다.
- 강화 학습으로 게임 에이전트를 학습시키는 TensorFlow 에이전트
- TensorFlow Serving을 사용하여 모델 서빙
- Flutter로 크로스 플랫폼 보드 게임 앱 만들기
기본 요건
- Flutter 개발 및 Dart에 관한 기본 지식
- 학습 및 배포와 같은 TensorFlow를 활용한 머신러닝에 관한 기본 지식
- Python, 터미널, Docker에 관한 기본 지식
학습할 내용
- TensorFlow Agents를 사용하여 비플레이어 캐릭터 (NPC) 에이전트를 학습시키는 방법
- TensorFlow Serving을 사용하여 학습된 모델을 제공하는 방법
- 크로스 플랫폼 Flutter 보드 게임을 빌드하는 방법
필요한 항목
- Flutter SDK
- Flutter용 Android 및 iOS 설정
- Flutter용 데스크톱 설정
- Flutter용 웹 설정
- Flutter 및 Dart용 Visual Studio Code(VS Code) 설정
- Docker
- Bash
- Python 3.7 이상
2. The Plane Strike Game
이 Codelab에서 빌드하는 게임은 'Battleship' 보드 게임과 유사한 소규모 2인용 보드 게임인 'Plane Strike'입니다. 규칙은 매우 간단합니다.
- 인간 플레이어는 머신러닝으로 학습된 NPC 에이전트와 대결합니다. 사람 플레이어는 에이전트 보드의 셀을 탭하여 게임을 시작할 수 있습니다.
- 게임이 시작되면 인간 플레이어와 에이전트가 각각 자신의 보드에 '비행기' 객체 (아래 애니메이션의 인간 플레이어 보드에 표시된 것처럼 '비행기'를 형성하는 8개의 녹색 셀)를 갖습니다. 이러한 '비행기'는 무작위로 배치되며 보드 소유자에게만 표시되고 상대에게는 숨겨집니다.
- 사람 플레이어와 에이전트는 서로의 보드에서 한 칸씩 번갈아 공격합니다. 사람 플레이어는 에이전트 보드의 셀을 탭할 수 있지만 에이전트는 머신러닝 모델의 예측에 따라 자동으로 선택합니다. 시도한 셀이 '비행기' 셀인 경우 빨간색으로 변하고('명중') 그렇지 않으면 노란색으로 변합니다('실패').
- 빨간색 셀을 먼저 8개 달성하는 사람이 게임에서 승리합니다. 그런 다음 새로운 보드로 게임이 다시 시작됩니다.
다음은 게임의 샘플 게임플레이입니다.

3. Flutter 개발 환경 설정
Flutter 개발의 경우 이 Codelab을 완료하려면 Flutter SDK와 편집기라는 두 가지 소프트웨어가 필요합니다.
다음 기기 중 하나를 사용하여 이 Codelab을 실행할 수 있습니다.
- iOS 시뮬레이터(Xcode 도구 설치 필요)
- Android Emulator(Android 스튜디오 설정 필요)
- 브라우저(디버깅 시 Chrome 필요)
- Windows, Linux 또는 macOS 데스크톱 애플리케이션. 배포에 사용할 플랫폼에서 개발해야 합니다. 따라서 Windows 데스크톱 앱을 개발하려면 적절한 빌드 체인에 액세스할 수 있도록 Windows에서 개발해야 합니다. docs.flutter.dev/desktop에 운영체제별 요구사항이 자세히 설명되어 있습니다.
4. 설정
이 Codelab의 코드를 다운로드하려면 다음 안내를 따르세요.
- 이 Codelab의 GitHub 저장소로 이동합니다.
- Code > Download zip을 클릭하여 이 Codelab의 모든 코드를 다운로드합니다.

- 다운로드한 ZIP 파일의 압축을 풀어 필요한 리소스가 모두 포함된
codelabs-main루트 폴더의 압축을 해제합니다.
이 Codelab에서는 저장소의 tfagents-flutter/ 하위 디렉터리에 있는 파일만 필요하며, 여기에는 다음 여러 폴더가 포함됩니다.
step0~step6폴더에는 이 Codelab의 각 단계에서 빌드의 기반이 되는 시작 코드가 포함되어 있습니다.finished폴더에는 완료된 샘플 앱의 완성된 코드가 포함되어 있습니다.- 각 폴더에는 백엔드 코드가 포함된
backbend하위 폴더와 Flutter 프런트엔드 코드가 포함된frontend하위 폴더가 있습니다.
5. 프로젝트의 종속 항목 다운로드
백엔드
터미널을 열고 tfagents-flutter 하위 폴더로 이동합니다. 다음을 실행합니다.
pip install -r requirements.txt
프런트엔드
- VS Code에서 File > Open folder를 클릭한 다음 이전에 다운로드한 소스 코드에서
step0폴더를 선택합니다. step0/frontend/lib/main.dart파일을 엽니다. 시작 앱에 필요한 패키지를 다운로드하라는 VS Code 대화상자가 표시되면 Get packages를 클릭합니다.- 이 대화상자가 표시되지 않으면 터미널을 열고
step0/frontend폴더에서flutter pub get명령어를 실행합니다.

6. 0단계: 시작 앱 실행
- VS Code에서
step0/frontend/lib/main.dart파일을 열고 Android Emulator 또는 iOS 시뮬레이터가 올바르게 설정되어 있고 상태 표시줄에 표시되는지 확인합니다.
예를 들어 Android Emulator로 Pixel 5를 사용하는 경우 다음과 같이 표시됩니다.

iOS 시뮬레이터로 iPhone 13을 사용하는 경우 다음과 같이 표시됩니다.

Start debugging을 클릭합니다.
앱 실행 및 탐색
Android Emulator 또는 iOS 시뮬레이터에서 앱이 실행됩니다. UI는 매우 단순합니다. 게임판은 2개이며, 인간 플레이어는 상단의 에이전트 보드에서 아무 셀이나 공격 위치로 탭할 수 있습니다. 인간 플레이어의 보드를 기반으로 공격할 위치를 자동으로 예측하도록 스마트 에이전트를 학습시킵니다.
내부적으로 Flutter 앱은 인간 플레이어의 현재 보드를 백엔드로 전송합니다. 백엔드는 강화 학습 모델을 실행하고 다음에 칠 예상 셀 위치를 반환합니다. 프런트엔드는 응답을 받은 후 UI에 결과를 표시합니다.

지금 상담사의 보드에서 셀을 클릭해도 앱이 아직 백엔드와 통신할 수 없으므로 아무 일도 일어나지 않습니다.
7. 1단계: TensorFlow Agents Python 환경 만들기
이 Codelab의 기본 목표는 환경과 상호작용하여 학습하는 에이전트를 설계하는 것입니다. Plane Strike 게임은 비교적 간단하며 NPC 에이전트의 규칙을 직접 만들 수 있지만, 강화 학습을 사용하여 에이전트를 학습시켜 기술을 배우고 향후 다른 게임의 에이전트를 쉽게 빌드할 수 있습니다.
표준 강화 학습 (RL) 설정에서 에이전트는 매 시간 단계마다 관찰을 받고 행동을 선택합니다. 행동이 환경에 적용되고 환경은 보상과 새로운 관찰을 반환합니다. 에이전트는 보상(수익이라고도 함)의 합을 극대화하는 작업을 선택하도록 정책을 학습시킵니다. 에이전트는 게임을 여러 번 플레이하여 패턴을 학습하고 기술을 연마하여 게임을 마스터할 수 있습니다. Plane Strike 게임을 RL 문제로 공식화하려면 보드 상태를 관찰, 공격 위치를 행동, 명중/실패 신호를 보상으로 생각하세요.

NPC 에이전트를 학습시키기 위해 TensorFlow용으로 안정적이고 확장 가능하며 사용하기 쉬운 강화 학습 라이브러리인 TensorFlow Agents를 활용합니다.
TF Agents는 시작하는 데 도움이 되는 광범위한 Codelab, 예시, 광범위한 문서가 제공되므로 강화 학습에 적합합니다. TF Agents를 사용하면 확장성을 갖춘 현실적이고 복잡한 강화 학습 문제를 해결하고 새로운 강화 학습 알고리즘을 빠르게 개발할 수 있습니다. 실험을 위해 다양한 에이전트와 알고리즘 간에 쉽게 전환할 수 있습니다. 또한 테스트를 거쳤으며 구성하기 쉽습니다.
OpenAI Gym (예: 아타리 게임), Mujuco 등에 구현된 사전 빌드 게임 환경이 많이 있으며 TF Agents에서 이를 쉽게 활용할 수 있습니다. 하지만 Plane Strike 게임은 완전한 맞춤 게임이므로 먼저 처음부터 새로운 환경을 구현해야 합니다.
TF Agents Python 환경을 구현하려면 다음 메서드를 구현해야 합니다.
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."""
가장 중요한 것은 작업을 가져와 새 time_step 객체를 반환하는 _step() 함수입니다. Plane Strike 게임의 경우 게임판이 있습니다. 새로운 공격 위치가 들어오면 환경은 게임판 상태를 기반으로 다음을 파악합니다.
- 숨겨진 비행기 위치를 고려할 때 셀의 색상을 빨간색이나 노란색으로 변경해야 하는 등 게임판이 다음에 어떻게 표시되어야 하는지
- 플레이어가 해당 위치에 대해 어떤 보상을 받아야 하나요 (적중 보상 또는 미적중 페널티)?
- 게임이 종료되어야 하나요 (누군가 이겼나요)?
- 다음 코드를
_planestrike_py_environment.py파일의_step()함수에 추가합니다.
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 Agents로 게임 에이전트 학습시키기
TF Agents 환경이 준비되면 게임 에이전트를 학습시킬 수 있습니다. 이 Codelab에서는 REINFORCE 에이전트를 사용합니다. REINFORCE는 RL의 정책 경사 알고리즘입니다. 기본 아이디어는 게임 플레이 중에 수집된 보상 신호를 기반으로 정책 신경망 매개변수를 조정하여 정책 네트워크가 향후 플레이에서 수익을 극대화할 수 있도록 하는 것입니다.
- 먼저 학습 및 평가 환경을 인스턴스화해야 합니다.
step2/backend/training.py파일의train_agent()함수에 다음 코드를 추가합니다.
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)
- 다음으로 학습할 강화 학습 에이전트를 만들어야 합니다. 이 Codelab에서는 정책 기반 에이전트인 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,
)
- 마지막으로 학습 루프에서 에이전트를 학습시킵니다. 루프에서 먼저 게임 플레이 에피소드를 버퍼에 몇 개 수집한 다음 버퍼링된 데이터로 에이전트를 학습시킵니다.
step2/backend/training.py파일의train_agent()함수에 다음 코드를 추가합니다.
# 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은 학습 로그가 포함된 폴더입니다. 샘플 학습 실행은 다음과 같습니다.
학습이 진행됨에 따라 평균 에피소드 길이가 감소하고 평균 수익이 증가하는 것을 확인할 수 있습니다. 상담사가 더 똑똑하고 더 나은 예측을 하면 게임 시간이 짧아지고 상담사가 더 많은 보상을 획득한다는 것을 직관적으로 이해할 수 있습니다. 이는 에이전트가 후반 단계에서 큰 보상 할인을 최소화하기 위해 더 적은 단계로 게임을 완료하려고 하기 때문에 적절합니다.
학습이 완료되면 학습된 모델이 policy_model 폴더로 내보내집니다.
9. 3단계: TensorFlow Serving으로 학습된 모델 배포
이제 게임 에이전트를 학습시켰으므로 TensorFlow Serving을 사용하여 배포할 수 있습니다.
- 터미널에서 컴퓨터의
step3/backend폴더로 이동하여 Docker로 TensorFlow Serving을 시작합니다.
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 Serving이 시작됩니다. 로그는 다음 코드 스니펫과 같아야 합니다.
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단계: Android 및 iOS용 Flutter 앱 만들기
백엔드가 준비되었습니다. 이제 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에 요청을 보내 예측을 수행할 수 있습니다.
step4/frontend/lib/game_agent.dart파일의predict()함수에 다음 코드를 추가합니다.
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');
}
앱이 백엔드로부터 응답을 받으면 게임 진행 상황을 반영하도록 게임 UI를 업데이트합니다.
step4/frontend/lib/main.dart파일의_gridItemTapped()함수에 다음 코드를 추가합니다.
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(() {});
실행하기
Start debugging을 클릭하고 앱이 로드될 때까지 기다립니다.- 상담사의 보드에서 셀을 탭하여 게임을 시작합니다.

11. 5단계: 데스크톱 플랫폼용 Flutter 앱 사용 설정
Flutter는 Android 및 iOS 외에도 Linux, Mac, Windows를 비롯한 데스크톱 플랫폼을 지원합니다.
Linux
- VSCode의 상태 표시줄에서 대상 기기가
로 설정되어 있는지 확인합니다.
Start debugging을 클릭하고 앱이 로드될 때까지 기다립니다.- 에이전트 보드의 셀을 클릭하여 게임을 시작합니다.

Mac
- Mac의 경우 앱이 HTTP 요청을 백엔드로 전송하기 때문에 적절한 사용 권한을 설정해야 합니다. 자세한 내용은 사용 권한 및 앱 샌드박스를 참고하세요.
step4/frontend/macOS/Runner/DebugProfile.entitlements 및 step4/frontend/macOS/Runner/Release.entitlements에 각각 다음 코드를 추가합니다.
<key>com.apple.security.network.client</key>
<true/>
- VSCode의 상태 표시줄에서 대상 기기가
로 설정되어 있는지 확인합니다.
Start debugging을 클릭하고 앱이 로드될 때까지 기다립니다.- 에이전트 보드의 셀을 클릭하여 게임을 시작합니다.

Windows
- VSCode의 상태 표시줄에서 대상 기기가
로 설정되어 있는지 확인합니다.
Start debugging을 클릭하고 앱이 로드될 때까지 기다립니다.- 에이전트 보드의 셀을 클릭하여 게임을 시작합니다.

12. 6단계: 웹 플랫폼용 Flutter 앱 사용 설정
Flutter 앱에 웹 지원을 추가할 수도 있습니다. 기본적으로 웹 플랫폼은 Flutter 앱에 자동으로 사용 설정되므로 실행하기만 하면 됩니다.
- VSCode의 상태 표시줄에서 대상 기기가
로 설정되어 있는지 확인합니다.
Start debugging을 클릭하고 앱이 Chrome 브라우저에 로드될 때까지 기다립니다.- 에이전트 보드의 셀을 클릭하여 게임을 시작합니다.

13. 축하합니다
ML 기반 에이전트가 인간 플레이어와 대결하는 보드 게임 앱을 빌드했습니다.
