DATE : 2010/08/30 (Mon)
Google Developer Day 2010のDevQuizで出題された「PAC-MAN」の入力2までを解いた際に使用したソースコードです。言語にはPythonを使用しました。標準出力からマップデータを入れると、標準出力に経路を出力するスクリプトです。
本スクリプトでは、A*アルゴリズムを使ってパックマンの動きを探索しました。しかしただ単に探索したのでは探索範囲が爆発的に膨れあがってしまうため、敵と衝突するような動きは避けるようにしました。また、次の交差点まで移動するような経路も隣接する経路にくわえ、次の交差点まで移動する経路はスコアが半分になるようにしました。本スクリプトでは、スコアが少ないほど良いスコアであるため、次の交差点まで一気に移動する経路が優先的に選ばれます。こうすることで、探索の高速化を図りました。
スコアは、「(残りのドット数)×1000 + (消費した時間)」とし、挟み撃ちに合うなどして敵と衝突すると、「(残りのドット数)×1000」の部分を残り時間と乗算しました。
本スクリプトに至るまでには、紆余曲折がかなりありました。まずはじめは、反復深化深さ優先探索を実装してみたのですが、答えがなかなか返ってこなかったため断念しました。次にA*アルゴリズムを実装したのですが、スコアにさまざまなヒューリスティクスを加えてみたものの、入力1のみしか解けず入力2は答えが返ってこない状況となりました。加えるヒューリスティクスのネタも尽きたところで、一発奮起して遺伝的アルゴリズムを実装してみました。ところが、あともう少しでドットを食べきれるところで探索が終了してしまいました。
しかし、A*アルゴリズム版から遺伝的アルゴリズム版へ至る間に、探索の効率化やスコア計算の簡略化を行っていました。遺伝的アルゴリズムを実装する前までは敵と衝突するような経路も探索候補に入れていました。ところが遺伝的アルゴリズムでは効率的な遺伝子の設計が求められます。そこで、敵と衝突するような経路は探索範囲から外すようにしました。スコア計算についても、処理に時間のかかるヒューリスティクスはやめて、単純な計算で済むような方法としました。
このような改良を、なかばやけくそでA*アルゴリズム版にも施してみました。すると、入力2も解けてしまったのです。同じようにして入力3も解けないものかとスクリプトを実行してみたのですが、残念ながらDevQuizの締め切りまでに回答は出ませんでした。
探索に関して新しいアイディアを出すには、別の方法で実装してみるというのも十分に有効だと感じました。
#!/usr/bin/env python
# coding: UTF-8
import bisect
import math
import sys
# 左上隅の座標
START_X = 1
START_Y = 1
MAX_SCORE = sys.maxint
MIN_SCORE = -sys.maxint - 1
class Wall:
u"""
壁を表す。
"""
def __str__(self):
return u'#'
class Dot:
u"""
ドットを表す。
"""
def __str__(self):
return u'.'
# 壁のインスタンス。マップ内で同一のインスタンスを使い回す。
WALL = Wall()
# ドットのインスタンス。マップ内で同一のインスタンスを使い回す。
DOT = Dot()
# マップの評価結果
# 何も起こらなかった。
NOTHING = 0
# 敵と衝突した。
CRASH = 1
# ドットをとった。
ATE_DOT = 2
# 時間内にすべてのドットをとった。
CLEAR = 3
# 時間切れになった。
TIME_OVER = 4
class Character:
u"""
敵やパックマンを表すベースクラス。
"""
def __init__(self, x, y):
self.position = (x, y)
self.history = []
def set_position(self, x, y):
u"""
現在の位置を設定する。
"""
self.history.append(self.position)
self.position = (x, y)
def undo(self):
u"""
1つ前の状態に戻す。
"""
self.position = self.history.pop()
class Packman(Character):
u"""
パックマンを表す。
"""
def __init__(self, x, y):
Character.__init__(self, x, y)
def __str__(self):
return u'@'
class Enemy(Character):
u"""
敵を表すベースクラス。
"""
def __init__(self, x, y):
Character.__init__(self, x, y)
self.id = u'%s,%s' % (x, y)
def get_first_next(self, neighborhoods):
u"""
時刻t = 0での移動先を返す。
"""
x, y = self.position
if (x, y + 1) in neighborhoods:
return (x, y + 1)
elif (x - 1, y) in neighborhoods:
return (x - 1, y)
elif (x, y - 1) in neighborhoods:
return (x, y - 1)
elif (x + 1, y) in neighborhoods:
return (x + 1, y)
def get_next(self, neighborhoods):
u"""
次の移動先を返す。
"""
if len(self.history) == 0:
return self.get_first_next(neighborhoods)
else:
if len(neighborhoods) == 1:
return neighborhoods[0]
elif len(neighborhoods) == 2:
for a_neighborhood in neighborhoods:
if self.history[-1] != a_neighborhood:
return a_neighborhood
else:
return self.get_next_on_crossing(neighborhoods)
def get_differ_to_packman(self):
u"""
敵から見たパックマンとの距離を返す。
"""
return (self.packman.position[0] - self.position[0],
self.packman.position[1] - self.position[1])
def get_state(self):
u"""
現在の状態を文字列として表現する。
"""
state = u''
if 0 < len(self.history):
state += u'(%s, %s)->' % (self.history[-1][0], self.history[-1][0])
state += u'(%s, %s) ' % (self.position[0], self.position[1])
return state
class EnemyV(Enemy):
u"""
敵Vを表す。
"""
def __init__(self, x, y):
Enemy.__init__(self, x, y)
self.packman = None
def get_next_on_crossing(self, neighborhoods):
u"""
交差点での次の移動先を返す。
"""
dx, dy = self.get_differ_to_packman()
if dy != 0:
candidate = (self.position[0], self.position[1] + (dy / abs(dy)))
if candidate in neighborhoods:
return candidate
if dx != 0:
candidate = (self.position[0] + (dx / abs(dx)), self.position[1])
if candidate in neighborhoods:
return candidate
return self.get_first_next(neighborhoods)
def __str__(self):
return u'V'
class EnemyH(Enemy):
u"""
敵Hを表す。
"""
def __init__(self, x, y):
Enemy.__init__(self, x, y)
self.packman = None
def get_next_on_crossing(self, neighborhoods):
u"""
交差点での次の移動先を返す。
"""
dx, dy = self.get_differ_to_packman()
if dx != 0:
candidate = (self.position[0] + (dx / abs(dx)), self.position[1])
if candidate in neighborhoods:
return candidate
if dy != 0:
candidate = (self.position[0], self.position[1] + (dy / abs(dy)))
if candidate in neighborhoods:
return candidate
return self.get_first_next(neighborhoods)
def __str__(self):
return u'H'
class EnemyLRActionBase:
u"""
敵L,Rの動作を共通化したベースクラス。
"""
def __init__(self, history, search):
self.history = history
self.search = search
def get_next_on_crossing(self, position, neighborhoods):
u"""
交差点での次の移動先を返す。
"""
previous_position = self.history[-1]
previous_diff = (previous_position[0] - position[0],
previous_position[1] - position[1])
start = self.search.index(previous_diff) + 1
for index in range(start, start + len(self.search)):
next_diff = self.search[index % len(self.search)]
candidate = (position[0] + next_diff[0], position[1] + next_diff[1])
if candidate in neighborhoods:
return candidate
class EnemyLAction(EnemyLRActionBase):
u"""
敵Lの動作を表す。
"""
SEARCH = [(0, -1), (1, 0), (0, 1), (-1, 0)]
def __init__(self, history):
EnemyLRActionBase.__init__(self, history, EnemyLAction.SEARCH)
class EnemyL(Enemy):
u"""
敵Lを表す。
"""
def __init__(self, x, y):
Enemy.__init__(self, x, y)
self.action = EnemyLAction(self.history)
def get_next_on_crossing(self, neighborhoods):
u"""
交差点での次の移動先を返す。
"""
return self.action.get_next_on_crossing(self.position, neighborhoods)
def __str__(self):
return u'L'
class EnemyRAction(EnemyLRActionBase):
u"""
敵Rの動作を表す。
"""
SEARCH = [(0, 1), (1, 0), (0, -1), (-1, 0)]
def __init__(self, history):
EnemyLRActionBase.__init__(self, history, EnemyRAction.SEARCH)
class EnemyR(Enemy):
u"""
敵Rを表す。
"""
def __init__(self, x, y):
Enemy.__init__(self, x, y)
self.action = EnemyRAction(self.history)
def get_next_on_crossing(self, neighborhoods):
u"""
交差点での次の移動先を返す。
"""
return self.action.get_next_on_crossing(self.position, neighborhoods)
def __str__(self):
return u'R'
class EnemyJ(Enemy):
u"""
敵Jを表す。
"""
def __init__(self, x, y, field):
Enemy.__init__(self, x, y)
self.action_r = EnemyRAction(self.history)
self.action_l = EnemyLAction(self.history)
self.current_action = self.action_l
self.action_history = []
self.field = field
def set_position(self, x, y):
Enemy.set_position(self, x, y)
self.action_history.append(self.current_action)
previous_position = self.history[-1]
if 3 <= self.field.get_neighborhoods_count(
previous_position[0], previous_position[1]):
self.change_action()
def change_action(self):
if self.current_action == self.action_r:
self.current_action = self.action_l
else:
self.current_action = self.action_r
def get_next(self, neighborhoods):
self.on_crossing = False
return Enemy.get_next(self, neighborhoods)
def get_next_on_crossing(self, neighborhoods):
u"""
交差点での次の移動先を返す。
"""
return self.current_action.get_next_on_crossing(self.position, neighborhoods)
def undo(self):
Enemy.undo(self)
self.current_action = self.action_history.pop()
def get_state(self):
return Enemy.get_state(self) + str(self.current_action)
def __str__(self):
return u'J'
class Field:
u"""
現在のマップの状態を表す。
"""
def __init__(self, field_file = None):
if field_file:
self.make_field(field_file)
else:
pass
def get_neighborhoods_count(self, x, y):
u"""
進入可能な通路の数を返す。
"""
neighborhood_count = 0
if WALL not in self.field[y - 1][x]:
neighborhood_count += 1
if WALL not in self.field[y + 1][x]:
neighborhood_count += 1
if WALL not in self.field[y][x + 1]:
neighborhood_count += 1
if WALL not in self.field[y][x - 1]:
neighborhood_count += 1
return neighborhood_count
def get_fast_way_one(self, candidate, neighborhood):
u"""
隣接する交差点までの1経路を返す。
"""
forward_x = neighborhood[0] - candidate[0]
forward_y = neighborhood[1] - candidate[1]
previous_x = candidate[0]
previous_y = candidate[1]
current_x = candidate[0] + forward_x
current_y = candidate[1] + forward_y
fast_way = [(previous_x, previous_y), (current_x, current_y)]
while True:
neighborhoods = self.get_neighborhoods_on_position(current_x, current_y)
if 3 <= len(neighborhoods):
return fast_way
elif (current_x + forward_x, current_y + forward_y) not in neighborhoods:
list = [a_neighborhood for a_neighborhood in neighborhoods
if a_neighborhood != (previous_x, previous_y)]
if len(list) == 0:
return None
next = list[0]
if len(next) == 0:
return fast_way
forward_x = next[0] - current_x
forward_y = next[1] - current_y
previous_x = current_x
previous_y = current_y
current_x = current_x + forward_x
current_y = current_y + forward_y
fast_way.append((current_x, current_y))
def get_fast_ways(self, candidate):
u"""
隣接する交差点までの全経路を返す。
"""
neighborhoods = [a_neighborhood for a_neighborhood
in self.get_neighborhoods_on_position(candidate[0], candidate[1])
if a_neighborhood != self.packman.position]
fast_ways = []
for a_neighborhood in neighborhoods:
fast_way = (self.get_fast_way_one(candidate, a_neighborhood))
if fast_way:
fast_ways.append(fast_way)
return fast_ways
def get_neighborhoods_on_position(self, x, y):
u"""
隣接する通路の座標を返す。
"""
neighborhoods = []
if WALL not in self.field[y - 1][x]:
neighborhoods.append((x, y - 1))
if WALL not in self.field[y + 1][x]:
neighborhoods.append((x, y + 1))
if WALL not in self.field[y][x + 1]:
neighborhoods.append((x + 1, y))
if WALL not in self.field[y][x - 1]:
neighborhoods.append((x - 1, y))
return neighborhoods
def get_neighborhoods(self, character):
u"""
指定のキャラクターから見た、隣接する通路の座標を返す。
"""
return self.get_neighborhoods_on_position(
character.position[0], character.position[1])
def get_next_candidates(self):
u"""
次に移動可能な座標を返す。
"""
candidates = self.get_neighborhoods(self.packman)
candidates.append(self.packman.position)
for an_enemy in self.enemies:
enemy_next = an_enemy.get_next(self.get_neighborhoods(an_enemy))
for a_candidate in candidates[:]:
if an_enemy.position == a_candidate or enemy_next == a_candidate:
candidates.remove(a_candidate)
return candidates
def move_character(self, character, x, y):
u"""
キャラクターを指定の座標に動かす。
"""
self.field[character.position[1]][character.position[0]].remove(character)
self.field[y][x].add(character)
character.set_position(x, y)
def undo_character(self, character):
u"""
指定のキャラクターを1つ前の時刻の状態に戻す。
"""
self.field[character.position[1]][character.position[0]].remove(character)
character.undo()
self.field[character.position[1]][character.position[0]].add(character)
def is_crashed(self):
u"""
パックマンが敵と衝突したか否かを返す。
"""
if 0 < len(self.field[self.packman.position[1]][self.packman.position[0]] &
self.enemies_on_field):
return True
if 2 <= len(self.packman.history):
packman_previous_position = self.packman.history[-1]
candidate_enemies = \
self.field[packman_previous_position[1]][packman_previous_position[0]] & \
self.enemies_on_field
for a_candidate_enemy in candidate_enemies:
if a_candidate_enemy.history[-1] == self.packman.position:
return True
return False
def evalute(self):
u"""
現在のフィールドの状態について評価を行う。
"""
if self.time < 0:
self.dot_ate_history.append(None)
return TIME_OVER
if self.is_crashed():
self.dot_ate_history.append(None)
return CRASH
if DOT in self.field[self.packman.position[1]][self.packman.position[0]]:
self.field[self.packman.position[1]][self.packman.position[0]].remove(DOT)
self.dot_count -= 1
self.dot_ate_history.append(self.packman.position)
if self.dot_count == 0:
return CLEAR
else:
return ATE_DOT
else:
self.dot_ate_history.append(None)
return NOTHING
def next(self, x, y):
u"""
指定の座標にパックマンを移動させる。
"""
for an_enemy in self.enemies:
next_x, next_y = an_enemy.get_next(self.get_neighborhoods(an_enemy))
self.move_character(an_enemy, next_x, next_y)
self.move_character(self.packman, x, y)
self.time -= 1
return self.evalute()
def undo(self):
u"""
1つ前の時刻の状態にフィールドを戻す。
"""
for an_enemy in self.enemies:
self.undo_character(an_enemy)
self.undo_character(self.packman)
previous_dot = self.dot_ate_history.pop()
if previous_dot:
self.field[previous_dot[1]][previous_dot[0]].add(DOT)
self.dot_count += 1
self.time += 1
def undo_all(self):
u"""
初期状態に戻す。
"""
for i in range(0, len(self.packman.history)):
self.undo()
def set_packman_to_enemies(self):
u"""
各EnemyオブジェクトにPackmanオブジェクトを設定する。
"""
for an_enemy in self.enemies:
an_enemy.packman = self.packman
def make_field(self, file):
u"""
フィールドを作成する。
"""
lines = file.readlines()
# 1行目: 時間
self.time = int(lines[0])
# 2行目: <幅> <高さ>
self.width, self.height = [int(a_number) + 1 for a_number in lines[1].split()]
# マスク処理での境界値処理を省略するために、一回り大きなフィールドを作成する。
self.field = []
for row_index in range(0, self.height + 1):
current_row = []
self.field.append(current_row)
for column_index in range(0, self.width + 1):
current_row.append(set())
# フィールドの作成
self.dot_count = 0
self.enemies = []
field_source = lines[2:]
y = START_Y
for a_row in field_source:
x = START_X
for a_column in a_row:
self.field[y][x].clear()
element = self.get_element(a_column, x, y)
if element:
self.field[y][x].add(element)
if isinstance(element, Dot):
self.dot_count += 1
elif isinstance(element, Packman):
self.packman = element
elif isinstance(element, Enemy):
self.enemies.append(element)
x += 1
y += 1
self.set_packman_to_enemies()
self.packman_on_field = frozenset([self.packman])
self.enemies_on_field = frozenset(self.enemies)
self.dot_on_field = frozenset([DOT])
self.dot_ate_history = []
self.initial_time = self.time
self.initial_dot_count = self.dot_count
def get_element(self, element_character, x, y):
u"""
テキスト表現のマップの指定の要素からオブジェクトを生成する。
"""
if element_character == u'#':
return WALL
elif element_character == u' ':
return None
elif element_character == u'.':
return DOT
elif element_character == u'@':
return Packman(x, y)
elif element_character == u'V':
return EnemyV(x, y)
elif element_character == u'H':
return EnemyH(x, y)
elif element_character == u'L':
return EnemyL(x, y)
elif element_character == u'R':
return EnemyR(x, y)
elif element_character == u'J':
return EnemyJ(x, y, self)
def __str__(self):
map = u''
for y in range(START_Y, self.height):
for x in range(START_X, self.width):
elements = self.field[y][x]
if WALL in elements:
map += str(WALL)
elif self.packman in elements:
if 0 < len(elements - self.packman_on_field):
map += u'x'
else:
map += str(self.packman)
elif 0 < len(elements & self.enemies_on_field):
enemies = list(elements & self.enemies_on_field)
map += str(enemies[0])
elif DOT in elements:
map += str(DOT)
else:
map += u' '
map += u'\n'
return map
class Node:
u"""
フィールド上のある場面を表す。
"""
def __init__(self, current_field, result):
if 2 <= len(current_field.packman.history):
self.route = current_field.packman.history[1:]
self.route.append(current_field.packman.position)
elif 1 == len(current_field.packman.history):
self.route = [current_field.packman.position]
else:
self.route = []
self.is_fast = False
self.position = current_field.packman.position
self.score = self.calculate_score(current_field, result)
self.original_score = self.score
self.enemy_state = u''
for an_enemy in current_field.enemies:
self.enemy_state += an_enemy.get_state()
self.string = u'%s,%s,%s' % (
str(len(self.route)), str(self.position), self.enemy_state)
def __str__(self):
return self.string
def calculate_score(self, current_field, result):
u"""
スコアを計算する。
"""
if result == TIME_OVER:
return MAX_SCORE
if current_field.time <= current_field.dot_count:
return MAX_SCORE
dot_score = (current_field.dot_count * 1000)
if result == CRASH:
dot_score *= current_field.time
score = len(self.route) + dot_score
return score
def set_fast(self):
u"""
高速移動(次の交差点まで移動)するノードか否かを設定する。
"""
self.is_fast = True
self.score /= 2
def setup_field(self, field):
u"""
Fieldオブジェクトを、本ノードが表す状態にする。
"""
if self.route == field.packman.history[1:]:
return
field.undo_all()
for a_next in self.route:
field.next(a_next[0], a_next[1])
def __cmp__(x, y):
return x.score - y.score
class ClosedNode():
u"""
調査が終わったノードを表す。
bisectモジュールは、__cmp__関数を用いるため、
クローズリストでbisectモジュールを使用するには本クラスが必要。
"""
def __init__(self, node):
self.node = node
self.score = node.score
self.original_score = node.original_score
def __cmp__(x, y):
return cmp(str(x.node), str(y.node))
def find_node_index(node, list):
u"""
指定のリストからノードを探し、そのインデックスを返す。
"""
i = bisect.bisect_left(list, node)
if i < len(list) and list[i] == node:
return i
else:
return -1
def insert_node(node, list):
u"""
指定のリストにノードを挿入する。
"""
bisect.insort(list, node)
def pop_node(node, list):
u"""
指定のリストから指定のノードを取り出す。
"""
index = find_node_index(node, list)
if 0 <= index:
list.pop(index)
else:
return None
def find_index_from_close_list(node, close_list):
u"""
クローズリストからノードを探し、そのインデックスを返す。
"""
return find_node_index(ClosedNode(node), close_list)
def find_index_from_open_list(node, open_list):
u"""
オープンリストからノードを探し、そのインデックスを返す。
"""
for index in range(0,len(open_list)):
if str(node) == str(open_list[index]):
return index
else:
return -1
def decode_start_to_next(start, next):
u"""
1回分の移動を表現する文字を返す。
"""
diff_x = next[0] - start[0]
diff_y = next[1] - start[1]
if diff_x == -1:
return u'h'
elif diff_y == 1:
return u'j'
elif diff_y == -1:
return u'k'
elif diff_x == 1:
return u'l'
elif diff_x == 0 and diff_y == 0:
return u'.'
else:
return u'?'
def decode_route(start, route):
u"""
指定の経路を表現する文字列を返す。
"""
result = u''
for next in route:
result += decode_start_to_next(start, next)
start = next
return result
def a_search(field):
u"""
経路を探索する。
"""
first_node = Node(field, NOTHING)
start_position = field.packman.position
open_list = [first_node]
open_list_strs = [str(first_node)]
close_list = []
min_dot_count = field.initial_dot_count
while 0 < len(open_list):
current_node = open_list.pop(0)
insert_node(ClosedNode(current_node), close_list)
current_node.setup_field(field)
if field.dot_count < min_dot_count:
min_dot_count = field.dot_count
print field
print u'%s/%s' % ((field.initial_dot_count - field.dot_count),
field.initial_dot_count)
print decode_route(
start_position, field.packman.history[1:] + [field.packman.position])
if field.dot_count == 0:
return current_node.route
candidates_1x = field.get_next_candidates()
candidates_nx = []
for a_candidate_1x in candidates_1x:
candidates_nx.extend(field.get_fast_ways(a_candidate_1x))
for a_candidate_nx in candidates_nx:
next_count = 0
result = NOTHING
created_nodes = []
for a_next in a_candidate_nx:
result = field.next(a_next[0], a_next[1])
next_count += 1
if result == CRASH or result == TIME_OVER:
break
else:
node = Node(field, result)
if node.score != MAX_SCORE:
created_nodes.append(node)
if result == CLEAR:
break
if 0 < len(created_nodes):
created_nodes[-1].set_fast()
for a_node in created_nodes:
if pop_node(str(a_node), open_list_strs):
open_list_index = find_index_from_open_list(a_node, open_list)
if 0 <= open_list_index:
if a_node.original_score < \
open_list[open_list_index].original_score:
insert_node(ClosedNode(open_list[open_list_index]),
close_list)
del open_list[open_list_index]
insert_node(a_node, open_list)
insert_node(str(a_node), open_list_strs)
continue
close_list_index = find_index_from_close_list(a_node, close_list)
if 0 <= close_list_index:
if a_node.original_score < \
close_list[close_list_index].original_score:
del close_list[close_list_index]
insert_node(a_node, open_list)
insert_node(str(a_node), open_list_strs)
continue
insert_node(a_node, open_list)
insert_node(str(a_node), open_list_strs)
for i in range(0, next_count):
field.undo()
else:
print u'失敗'
def print_result(route, field):
u"""
結果を標準出力に出力する。
"""
decoded_route = decode_route(field.packman.position, route)
print decoded_route
print u'-- initial --'
print str(field)
index = 0
for a_next in route:
field.next(a_next[0], a_next[1])
print u'-- %s --' % (decoded_route[index])
print str(field)
index += 1
print u'%s steps.' % (len(decoded_route))
print decoded_route
def main():
initial_field = Field(sys.stdin)
route = a_search(initial_field)
initial_field.undo_all()
print_result(route, initial_field)
if __name__ == '__main__':
main()
