0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

小さな鬼ごっこから始める強化学習① 〜AIが動く世界を作る〜

0
Posted at

demo

WASDで逃げるキャラと、自動追跡する鬼+壁+慣性+捕獲カウントまで実装しました。


はじめに

強化学習で鬼ごっこAIを作ってみたいと思い、シリーズ形式で記録していきます。

始めた理由はシンプルで、

  • 試験勉強中に大きなアプリ開発を始めると止まらなくなりそうだった
  • 勉強しながらでも進捗が「見える」ものを作りたかった
  • 小さいものが成長していくのを見るのが好き

だからまずは「学習を入れられる環境」から作ることにしました。

今回はまだ強化学習は出てきません。
学習を面白くするための土台作りです。


実装環境

  • OS: Windows
  • エディタ: VSCode
  • 言語: Python 3.13
  • ライブラリ: pygame

⚠ Pythonバージョンについて

Python 3.14 では pip install pygame がうまく動作しませんでした。
そのため Python 3.13 を使用しています。

仮想環境を作成して実行しています。

py -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install pygame

今日作るもの

ただの追跡では学習が単純になりすぎるので、
環境側に少し“物理っぽさ”を入れました。

  • 壁(障害物)
  • 慣性っぽい動き(速度が急に変わらない)
  • 斜め移動の正規化
  • 捕まえた回数のカウント表示

① 斜め移動が速くなる問題

WASD入力をそのまま足すと、斜め移動は √2 倍速くなってしまいます。

そこでベクトルを正規化します。

move = Vector2(0, 0)
if keys[pygame.K_d]: move.x += 1
if keys[pygame.K_a]: move.x -= 1
if keys[pygame.K_s]: move.y += 1
if keys[pygame.K_w]: move.y -= 1

if move.length_squared() > 0:
    move = move.normalize()

これで移動方向は保ちつつ、移動速度を一定にできます。


② 慣性っぽい動き

いきなり速度を変更するのではなく、
目標速度に“徐々に追従”させています。

runner_vel += (desired_vel - runner_vel) * (1.0 - pow(2.718281828, -runner_response * dt))
runner += runner_vel * dt

runner_response が大きいほど、入力に対して素早く反応します。
小さいと、重い動きになります。

強化学習にしたときに、単純すぎない挙動になることを期待しています。


③ 壁との衝突処理(円 vs 矩形)

キャラクターは円、壁は矩形で扱っています。

矩形の中で最も近い点(最近点)を求め、
そこから押し出す方式で衝突を解決しています。

def resolve_circle_rect(pos, vel, r, rect):
    nx = max(rect.left, min(pos.x, rect.right))
    ny = max(rect.top, min(pos.y, rect.bottom))
    nearest = Vector2(nx, ny)

    delta = pos - nearest
    d2 = delta.length_squared()
    if d2 >= r * r:
        return pos, vel

    dist = delta.length()
    if dist == 0:
        n = Vector2(0, -1)
        dist = 1e-6
    else:
        n = delta / dist

    pos += n * (r - dist)

    vdot = vel.dot(n)
    if vdot < 0:
        vel -= n * vdot

    return pos, vel

雑に位置を戻すよりも自然な動きになります。


④ 捕まえた回数のカウント

if (runner - chaser).length() < CATCH_DIST:
    caught += 1
    runner, chaser, runner_vel, chaser_vel = reset()

画面左上に表示しています。


最終コード

クリックで展開
import pygame
from pygame.math import Vector2

pygame.init()
WIDTH = 800
HEIGHT = 600
screen = pygame.display.set_mode((WIDTH,HEIGHT))
font = pygame.font.SysFont(None, 28)
clock = pygame.time.Clock()

runner = Vector2(400,300)
runner_R = 10
runner_vel = Vector2(0,0)
runner_max_speed = 460.0
runner_response = 12.0

chaser = Vector2(150,150)
chaser_R = 12
chaser_vel = Vector2(0,0)
chaser_max_speed = 460.0
chaser_response = 10.0

CATCH_DIST = runner_R + chaser_R
caught = 0

walls = [
    pygame.Rect(200, 150, 300, 20),
    pygame.Rect(100, 350, 20, 180),
    pygame.Rect(500, 320, 200, 20),
]

running = True

def resolve_circle_rect(pos, vel, r, rect):
    nx = max(rect.left, min(pos.x, rect.right))
    ny = max(rect.top, min(pos.y, rect.bottom))
    nearest = Vector2(nx, ny)
    delta = pos - nearest
    d2 = delta.length_squared()
    if d2 >= r * r:
        return pos, vel

    dist = delta.length()
    if dist == 0:
        n = Vector2(0, -1)
        dist = 1e-6
    else:
        n = delta / dist

    pos += n * (r - dist)
    vdot = vel.dot(n)
    if vdot < 0:
        vel -= n * vdot

    return pos, vel

def reset():
    runner = Vector2(400,300)
    chaser = Vector2(150,150)
    runner_vel = Vector2(0,0)
    chaser_vel = Vector2(0,0)
    return runner, chaser, runner_vel, chaser_vel

while running:
    dt = clock.tick(60) / 1000.0

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    screen.fill((20,25,25))
    keys = pygame.key.get_pressed()

    move = Vector2(0,0)
    if keys[pygame.K_d]: move.x += 1
    if keys[pygame.K_a]: move.x -= 1
    if keys[pygame.K_s]: move.y += 1
    if keys[pygame.K_w]: move.y -= 1

    if move.length_squared() > 0:
        move = move.normalize()
        desired_vel = move * runner_max_speed
    else:
        desired_vel = Vector2(0,0)

    runner_vel += (desired_vel - runner_vel) * (1.0 - pow(2.718281828, -runner_response * dt))
    runner += runner_vel * dt

    to_runner = runner - chaser
    if to_runner.length_squared() > 0:
        desired_chaser_vel = to_runner.normalize() * chaser_max_speed
    else:
        desired_chaser_vel = Vector2(0,0)

    chaser_vel += (desired_chaser_vel - chaser_vel) * (1.0 - pow(2.718281828, -chaser_response * dt))
    chaser += chaser_vel * dt

    runner.x = max(0,min(runner.x,WIDTH))
    runner.y = max(0,min(runner.y,HEIGHT))
    chaser.x = max(0,min(chaser.x,WIDTH))
    chaser.y = max(0,min(chaser.y,HEIGHT))

    for w in walls:
        runner, runner_vel = resolve_circle_rect(runner, runner_vel, runner_R, w)
        chaser, chaser_vel = resolve_circle_rect(chaser, chaser_vel, chaser_R, w)

    if (runner - chaser).length() < CATCH_DIST:
        caught += 1
        runner, chaser, runner_vel, chaser_vel = reset()

    pygame.draw.circle(screen, (80,140,255), (int(runner.x),int(runner.y)), runner_R)
    pygame.draw.circle(screen, (220,60,60), (int(chaser.x),int(chaser.y)), chaser_R)

    for w in walls:
        pygame.draw.rect(screen, (90,90,90), w)

    text = font.render(f"Caught: {caught}", True, (230,230,230))
    screen.blit(text,(10,10))

    pygame.display.flip()

pygame.quit()

次回

ここまでで「学習させる環境」は完成しました。

ですが、この時点で私は

強化学習について一ミリも知りません。

次回はまず、

  • 強化学習とは何なのか?
  • 教師あり学習と何が違うのか?
  • なぜ鬼ごっこに向いているのか?
  • 「状態」「行動」「報酬」とは何か?

といった基礎から勉強していこうと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?