2010-08-23 GDD 2010 DevQuiz

_ パックマン

私の回答と探索に使用したソースコード。コードはムダに長いし地形の解析を途中まで実装したが探索には使っていない。

探索ロジックはあまり賢くない。たぶんLv2 は同じところをなんども回ってたり。一応工夫した点のメモを残す。

方針

  • パックマンの問題に記述されている仕様に則った動きや、ボードの情報、ある時点でのパックマン/敵/ドット/時刻の情報など探索ロジックによらない部分を共通クラスとして作成する。
  • ロジックは、最短を求める問題なのでまずは幅優先探索を実装する。
  • とはいっても探索済み状態による枝刈りが効かなそうなのに幅優先700 step の探索なんてメモリが足りないだろうから深さ優先の探索を実装する。
  • いずれにしろ完全な探索はリソース(メモリ/時間)食い過ぎなので探索の順序や打ち切り方法を工夫する。
  • うまくいかなかったらシミュレータも作って遊んでみる。よさそうな序盤の動きが見つかったらその続きを探索させる。

結果

  • Lv1 41点
hlllkjllkkhhhkhhkhhhjjjjllkklkllklllj
  • Lv2 190点
kkkllljjjhhhkkkllkkkllljjjh.lljjjlllllhhkkklllllkkkllljjj.hhjjjhhhkkk.ll.kkkhkk
lljjhhhhhhhh.ljjjhhjk.hkkklkkhhhhjjhhkklllkkklhhkkkllljjjlllkkklllllhhjjjhhjjjh
llllkkkllkkkllljjjhhjjjlljjjjjhhhhhhhhhhhjjjhhhkkkllllllllkkkkkhhhhhhhjkllkkkhk
kklhhhhjjjlllhhhhhjj
  • Lv3 545点
hhkkhhlljjlllkklllkllllkkkkkkkklllllllljjjjjhhhllljjjhhhhhllllllllljjjhhhhhhhhh
hhhhhhhkkkkkkkkkkkkkklllljjjjjjjjjjjlllllllllllljjjhhhhhhkkkllkkkkkkkkllllkkkhh
hhhhhhhhhhhhhhhhhhhhhhhjjjhhhhhhhhjjjjjjjjhhhhhhhhhhhhhhhhhhhhhhjjjlllllkkkhkkk
kkkkkhhhhkkklllllllllllllllllllllllllllllljjjlllllllllkkklllllllllljjjhhhhhhjjj
jjjjjhhhhkkhhhhhhhhhjjhhhhhhhhhhhjjjlllllllllllkkkkkkkkkkkhhhhhhhhhhhhhhhhkkkhh
hhhhhhhhjjjjjjjjjjjljjjlllllllllllkkkhhkkkkkkkhhhhhhjjjl

共通ロジック

PacMan.java

反省点

ボードに対する上下左右を UP, DOWN, LEFT, RIGHT として実装を始めてしまったが、進行方向の上下左右を使いたくなったので東西南北に変更したが、一部のメソッドの名前が古いまま。経験値の不足を感じる。

幅優先探索

PacManBfsSolverMain.java のBfsPacManSolver#solve() に探索ロジックを実装。

工夫したところ
  • 状態が持つ情報を減らすため、PacMan の位置に関係なく動く敵(L,R,J)は状態オブジェクトには持たせずMap<Integer/*時刻*/,List<Enemy>/*その時刻における敵リスト*/> からひいてくるようにした。
  • 動かないこと(以下 STAY)の連発は禁止した。また、連続した通路中ではSTAY を2回以上発行しないこととした。
  • 連続した通路中では引き返すのは1回まで。
  • STAYを一切探索しないモードも用意した。

探索順にほぼ意味が無いので、どこを探索しないかに注力。まぁ、結局はたいしたことはしていない...

深さ優先探索

PacManDfsSolverMain.java の DFSPacManSolver#solve() に探索ロジックを記述。

工夫したところ
  • 制限時間とは別に、目標タイムを定めて探索を打ち切った
  • 目標タイム前でも、残りドット数とドットまでの最短距離からクリア出来ない事がわかったら探索を打ち切った。
  • STAYを使う場合は、移動候補に敵との辺りがある場合に限定した(多分ベストな解ではないが...)。
  • 移動の候補はPacMan の現在の向きに応じて決めるようにした
  • この先が行き止まりか、行き止まりまでの間にドットがあるかを素早く判定するた目の情報を持たせた(が、探索には未使用)。

ソースコード

GDD2010Pacman_r141.zip

eclipse のプロジェクトを zip で固めたものを置いておきます。依存ライブラリとして google-collection の 1.0 を使用しています。プロジェクトの設定としては GoogleCollections という名前のユーザライブラリが定義されているとして依存を指定しているので、import する場合はユーザライブラリを定義してください。

一応ここにも貼っておきます(シミュレータはアーカイブの中のみ)。

PacMan.java

/*
 * パックマンのモデルを提供するクラス群。
 *
 * (C) YAMAZAKI Makoto <makoto1975@gmail.com> 2010.
 *
 * 本来なら各クラスを独立したファイルにするところだが、提出用に BFD で探索するクラスと同じファイルに
 * 記述してあった。複数ファイルの提出もOKとのことなので、とりあえず BFS/DFS で共通のものは独立した
 * ファイルに分割した。
 *
 * 依存ライブラリとして Google Collections を使用している。
 *
 * $Id: PackMan.java 135 2010-08-22 10:37:53Z zaki $
 */

package org.zakky.gdd2010.pacman;

import static org.zakky.gdd2010.pacman.Board.Direction.DOWN;
import static org.zakky.gdd2010.pacman.Board.Direction.LEFT;
import static org.zakky.gdd2010.pacman.Board.Direction.RIGHT;
import static org.zakky.gdd2010.pacman.Board.Direction.STAY;
import static org.zakky.gdd2010.pacman.Board.Direction.UP;
import java.awt.Point;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.zakky.gdd2010.pacman.Board.Direction;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

final class Board {
    public static final char WALL = '#';
    public static final char DEADEND = 'd';
    public static final char WALKWAY = 'w';
    public static final char INTERSECTION = 'i';
    public static final char BLANK = ' ';
    public static final char DOT = '.';
    public static final char PACMAN = '@';

    public static final char ENEMY_L = 'L';
    public static final char ENEMY_R = 'R';
    public static final char ENEMY_J = 'J';
    public static final char ENEMY_V = 'V';
    public static final char ENEMY_H = 'H';

    public static final Map<Integer, String> KNOWN_MAPS = ImmutableMap.of(
            Integer.valueOf(-957077586), "Lv1", Integer.valueOf(-1956186425),
            "Lv2", Integer.valueOf(106332079), "Lv3");

    public enum Direction {
        LEFT('h', -1, 0), DOWN('j', 0, 1), UP('k', 0, -1), RIGHT('l', 1, 0), STAY(
                '.', 0, 0);
        public final char literal;
        public final int dx;
        public final int dy;

        Direction(char literal, int dx, int dy) {
            this.literal = literal;
            this.dx = dx;
            this.dy = dy;
        }

        public Direction opposite() {
            return getDirection(0, 0, dx, dy);
        }

        public static Direction fromLiteral(char literal) {
            for (Direction d : values()) {
                if (literal == d.literal) {
                    return d;
                }
            }
            throw new IllegalArgumentException("invalid Direction literal: "
                    + literal);
        }

        public static Direction getDirection(int x, int y, int prevX, int prevY) {
            if (x != prevX) {
                assert y == prevY;
                return (prevX < x) ? RIGHT : LEFT;
            }
            if (y != prevY) {
                assert x == prevX;
                return (prevY < y) ? DOWN : UP;
            }
            return STAY;
        }

        public static List<Direction> filter(ImmutableList<Direction> list,
                Direction... toBeDropped) {
            final List<Direction> result = Lists.newArrayList(list);
            for (Direction d : toBeDropped) {
                result.remove(d);
            }
            return result;
        }
    }

    public static int turnToLeftX(int x, int y, int prevX, int prevY) {
        return x + (y - prevY);
    }

    public static int turnToLeftY(int x, int y, int prevX, int prevY) {
        return y + (prevX - x);
    }

    public static int turnToRightX(int x, int y, int prevX, int prevY) {
        return x + (prevY - y);
    }

    public static int turnToRightY(int x, int y, int prevX, int prevY) {
        return y + (x - prevX);
    }

    public static int goAheadX(int x, int y, int prevX, int prevY) {
        return x * 2 - prevX;
    }

    public static int goAheadY(int x, int y, int prevX, int prevY) {
        return y * 2 - prevY;
    }

    public static boolean canTurnToLeft(Board b, int x, int y, int prevX,
            int prevY) {
        return !b.isWall(turnToLeftX(x, y, prevX, prevY),
                turnToLeftY(x, y, prevX, prevY));
    }

    public static boolean canTurnToRight(Board b, int x, int y, int prevX,
            int prevY) {
        return !b.isWall(turnToRightX(x, y, prevX, prevY),
                turnToRightY(x, y, prevX, prevY));
    }

    public static boolean canGoAhead(Board b, int x, int y, int prevX, int prevY) {
        return !b.isWall(goAheadX(x, y, prevX, prevY),
                goAheadY(x, y, prevX, prevY));
    }

    public static boolean canGoBack(Board b, int x, int y, int prevX, int prevY) {
        return !b.isWall(prevX, prevY);
    }

    public static Board buildBoard(BufferedReader reader, boolean useStay)
            throws IOException {
        final int timeLimit = Integer.parseInt(reader.readLine());
        final String[] sizeStrs = reader.readLine().split(Pattern.quote(" "));
        final int width = Integer.parseInt(sizeStrs[0]);
        final int height = Integer.parseInt(sizeStrs[1]);

        final List<char[]> fixedOnBoard =
                Lists.newArrayListWithCapacity(height);
        final List<Enemy> enemies = Lists.newArrayList();
        final Collection<Point> dots = Lists.newArrayList();
        final Point pacman = new Point(-1, -1);

        for (int hIndex = 0; hIndex < height; hIndex++) {
            final String line = reader.readLine();
            if (line == null) {
                throw new IOException("input data is shorter than expected.");
            }
            final char[] row = new char[width];
            for (int wIndex = 0; wIndex < width; wIndex++) {
                final char c = line.charAt(wIndex);
                switch (c) {
                case WALL: // 壁
                    row[wIndex] = WALL;
                    break;
                case BLANK: // 空きマス
                    row[wIndex] = BLANK;
                    break;
                case DOT: // ドット
                    row[wIndex] = BLANK;
                    dots.add(new Point(wIndex + 1, hIndex + 1));
                    break;
                case PACMAN: // 自機
                    row[wIndex] = BLANK;
                    if (0 < pacman.x || 0 < pacman.y) {
                        // Pac-Man が2つ
                        throw new RuntimeException(
                                "duplicate Pac-Man found on the board");
                    }
                    pacman.x = wIndex + 1;
                    pacman.y = hIndex + 1;
                    break;
                case ENEMY_V:
                    row[wIndex] = BLANK;
                    enemies.add(new EnemyV(wIndex + 1, hIndex + 1));
                    break;
                case ENEMY_H:
                    row[wIndex] = BLANK;
                    enemies.add(new EnemyH(wIndex + 1, hIndex + 1));
                    break;
                case ENEMY_L:
                    row[wIndex] = BLANK;
                    enemies.add(new EnemyL(wIndex + 1, hIndex + 1));
                    break;
                case ENEMY_R:
                    row[wIndex] = BLANK;
                    enemies.add(new EnemyR(wIndex + 1, hIndex + 1));
                    break;
                case ENEMY_J:
                    row[wIndex] = BLANK;
                    enemies.add(new EnemyJ(wIndex + 1, hIndex + 1));
                    break;
                default:
                    throw new RuntimeException("unexpected char '" + c
                            + "' at x: " + (wIndex + 1) + ", y: "
                            + (hIndex + 1));
                }
            }
            fixedOnBoard.add(row);
        }

        // データの確認と、処理のための前準備

        // Pac-Man が存在すること
        if (pacman.x <= 0 || pacman.y <= 0) {
            throw new RuntimeException("Pac-Man not found on the board");
        }
        // 周囲が壁であることの確認と、空白の分類
        for (int hIndex = 0; hIndex < height; hIndex++) {
            final char[] row = fixedOnBoard.get(hIndex);
            // 最初と最後の行は全てが壁であること
            if (hIndex == 0 || hIndex == height - 1) {
                for (char edge : row) {
                    if (edge != WALL) {
                        throw new RuntimeException(
                                "edge of the board must be wall: y: "
                                        + (hIndex + 1));
                    }
                }
                continue;
            }

            // 最初と最後の行以外は両端が壁であること
            if (row[0] != WALL || row[row.length - 1] != WALL) {
                throw new RuntimeException(
                        "edge of the board must be wall: y: " + (hIndex + 1));
            }
            for (int wIndex = 1; wIndex < width - 1; wIndex++) {
                if (isWall(fixedOnBoard, wIndex + 1, hIndex + 1)) {
                    continue;
                } else if (isWalkWay(fixedOnBoard, wIndex + 1, hIndex + 1)) {
                    row[wIndex] = WALKWAY;
                } else if (isIntersection(fixedOnBoard, wIndex + 1, hIndex + 1)) {
                    row[wIndex] = INTERSECTION;
                } else if (isDeadEnd(fixedOnBoard, wIndex + 1, hIndex + 1)) {
                    row[wIndex] = DEADEND;
                } else {
                    throw new RuntimeException("unknown object on board ("
                            + (wIndex + 1) + ", " + (hIndex + 1) + ").");
                }
            }
        }
        // TODO そのうち全てのドットに到達可能であることを確認してみる

        return new Board(timeLimit, fixedOnBoard, pacman.x, pacman.y, dots,
                enemies, useStay);
    }

    private final int timeLimit_;
    private final int initialPacmanX_;
    private final int initialPacmanY_;
    private final Collection<Point> initialDots_; // Point は mutable なのでそのうち自前に。
    private final ImmutableList<Enemy> initialEnemies_;
    private final ImmutableList<Enemy> initialIntelligentEnemies_;
    private final ImmutableList<Enemy> initialNonIntelligentEnemies_;
    private ImmutableList<ImmutableList<Enemy>> nonIntelligentEnemiesCache_ =
            null;
    private final List<char[]> fixedOnBoard_;
    private final boolean useStay_;

    private final ImmutableList<ImmutableList<CellDetail>> cellDetails_;

    private Board(int timeLimit, List<char[]> wallsOnBoard, int initPacmanX,
            int initPacmanY, Collection<Point> initDots,
            List<Enemy> initEnemies, boolean useStay) {
        timeLimit_ = timeLimit;
        fixedOnBoard_ = Collections.unmodifiableList(wallsOnBoard);
        initialPacmanX_ = initPacmanX;
        initialPacmanY_ = initPacmanY;
        initialDots_ = initDots;
        initialIntelligentEnemies_ = toIntelligentEnemiesOnly(initEnemies);
        initialNonIntelligentEnemies_ =
                toNonIntelligentEnemiesOnly(initEnemies);
        initialEnemies_ = ImmutableList.copyOf(initEnemies);
        useStay_ = useStay;

        final ImmutableList.Builder<ImmutableList<CellDetail>> rows =
                ImmutableList.builder();
        for (int y = 1; y <= wallsOnBoard.size(); y++) {
            final ImmutableList.Builder<CellDetail> columns =
                    ImmutableList.builder();
            for (int x = 1; x <= wallsOnBoard.get(y - 1).length; x++) {
                columns.add(new CellDetail(this, x, y, useStay));
            }
            rows.add(columns.build());
        }
        cellDetails_ = rows.build();

        // 詳細情報さらにセットする
        for (int y = 1; y <= wallsOnBoard.size(); y++) {
            for (int x = 1; x <= wallsOnBoard.get(y - 1).length; x++) {
                if (isDeadEnd(x, y)) {
                    final ImmutableList.Builder<Direction> wayToDeadEnd =
                            ImmutableList.builder();
                    final ImmutableList<Direction> openWays =
                            getDetail(x, y).getOpenWays();

                    Direction toCurrent =
                            Direction.filter(openWays, STAY).get(0);
                    int currentX = x + toCurrent.dx;
                    int currentY = y + toCurrent.dy;

                    while (isWalkWay(currentX, currentY)) {
                        wayToDeadEnd.add(toCurrent.opposite());
                        final CellDetail detail = getDetail(currentX, currentY);
                        detail.setWayToDeadEnd(wayToDeadEnd.build());
                        toCurrent =
                                Direction.filter(detail.getOpenWays(), STAY,
                                        toCurrent.opposite()).get(0);
                        currentX += toCurrent.dx;
                        currentY += toCurrent.dy;
                    }
                }
            }
        }
    }

    public boolean useStay() {
        return useStay_;
    }

    public CellDetail getDetail(int x, int y) {
        return cellDetails_.get(y - 1).get(x - 1);
    }

    public static class CellDetail {
        private final char type_;
        private final ImmutableList<Direction> openWays_;
        private ImmutableList<Direction> wayToDeadEnd_ = null;

        CellDetail(Board board, int x, int y, boolean useStay) {
            type_ = Board.getType(board.fixedOnBoard_, x, y);
            if (type_ == WALL) {
                openWays_ = ImmutableList.of();
            } else {
                final ImmutableList.Builder<Direction> builder =
                        ImmutableList.builder();
                for (Direction dir : Direction.values()) {
                    if (!board.canGo(x, y, dir)) {
                        continue;
                    }
                    builder.add(dir);
                }
                openWays_ = builder.build();
            }
        }

        void setWayToDeadEnd(ImmutableList<Direction> way) {
            if (type_ == INTERSECTION) {
                throw new RuntimeException(
                        "can't set way to deadend on INTERSECTION");
            }
            wayToDeadEnd_ = way;
        }

        public ImmutableList<Direction> getWayToDeadEnd() {
            return wayToDeadEnd_;
        }

        public ImmutableList<Direction> getOpenWays() {
            return openWays_;
        }
    }

    private static ImmutableList<Enemy> toIntelligentEnemiesOnly(
            List<Enemy> allEnemies) {
        final ImmutableList.Builder<Enemy> intelligents =
                ImmutableList.builder();
        for (Enemy enemy : allEnemies) {
            if (!enemy.isIntelligent()) {
                continue;
            }
            intelligents.add(enemy);
        }
        return intelligents.build();
    }

    private static ImmutableList<Enemy> toNonIntelligentEnemiesOnly(
            List<Enemy> allEnemies) {
        final ImmutableList.Builder<Enemy> nonIntelligents =
                ImmutableList.builder();
        for (Enemy enemy : allEnemies) {
            if (enemy.isIntelligent()) {
                continue;
            }
            nonIntelligents.add(enemy);
        }
        return nonIntelligents.build();
    }

    public static ImmutableList<Enemy> updateEnemies(Board board,
            Iterable<Enemy> enemies, int myX, int myY) {
        final ImmutableList.Builder<Enemy> newEnemies = ImmutableList.builder();
        for (Enemy enemy : enemies) {
            final Enemy updated = enemy.getNext(board, myX, myY);
            newEnemies.add(updated);
        }
        return newEnemies.build();
    }

    private static ImmutableList<ImmutableList<Enemy>> buildNonIntelligentEnemiesCache(
            Board board, int limit) {
        ImmutableList<Enemy> prevNonIntelligents =
                board.getInitialNonIntelligentEnemies();

        final ImmutableList.Builder<ImmutableList<Enemy>> builder =
                ImmutableList.builder();
        for (int time = 0; time <= limit; time++) {
            // non intelligent 限定なので、自機の位置は適当
            prevNonIntelligents =
                    updateEnemies(board, prevNonIntelligents, 1, 1);
            builder.add(prevNonIntelligents);
        }
        return builder.build();
    }

    public int getWidth() {
        return fixedOnBoard_.get(0).length;
    }

    public int getHeight() {
        return fixedOnBoard_.size();
    }

    public int getTimeLimit() {
        return timeLimit_;
    }

    public int getInitialPacmanX() {
        return initialPacmanX_;
    }

    public int getInitialPacmanY() {
        return initialPacmanY_;
    }

    public Collection<Point> getInitialDots() {
        return initialDots_;
    }

    public ImmutableList<Enemy> getInitialEnemies() {
        return initialEnemies_;
    }

    public ImmutableList<Enemy> getInitialIntelligentEnemies() {
        return initialIntelligentEnemies_;
    }

    public ImmutableList<Enemy> getInitialNonIntelligentEnemies() {
        return initialNonIntelligentEnemies_;
    }

    public boolean isWall(int x, int y) {
        return isWall(fixedOnBoard_, x, y);
    }

    public static boolean isSafe(Iterable<Enemy> currentEnemies, int currentX,
            int currentY, int previousX, int previousY) {
        for (Enemy enemy : currentEnemies) {
            // 重なるのはアウト
            if (enemy.x() == currentX && enemy.y() == currentY) {
                return false;
            }
            // 入れ替わるのもアウト
            if (enemy.x() == previousX && enemy.y() == previousY
                    && enemy.prevX() == currentX && enemy.prevY() == currentY) {
                return false;
            }
        }
        return true;
    }

    public boolean canGo(int x, int y, Direction d) {
        switch (d) {
        case LEFT:
            return canGoW(fixedOnBoard_, x, y);
        case RIGHT:
            return canGoE(fixedOnBoard_, x, y);
        case UP:
            return canGoN(fixedOnBoard_, x, y);
        case DOWN:
            return canGoS(fixedOnBoard_, x, y);
        case STAY:
            return true;
        default:
            throw new RuntimeException("inknown direction: " + d);
        }
    }

    public boolean canGoW(int x, int y) {
        return canGoW(fixedOnBoard_, x, y);
    }

    public boolean canGoE(int x, int y) {
        return canGoE(fixedOnBoard_, x, y);
    }

    public boolean canGoN(int x, int y) {
        return canGoN(fixedOnBoard_, x, y);
    }

    public boolean canGoS(int x, int y) {
        return canGoS(fixedOnBoard_, x, y);
    }

    public boolean isDeadEnd(int x, int y) {
        return isDeadEnd(fixedOnBoard_, x, y);
    }

    public boolean isWalkWay(int x, int y) {
        return isWalkWay(fixedOnBoard_, x, y);
    }

    public boolean isIntersection(int x, int y) {
        return isIntersection(fixedOnBoard_, x, y);
    }

    public int getScore(Collection<Point> dots, int time) {
        final int timeScore =
                dots.isEmpty() ? Math.max(0, getTimeLimit() - time) : 0;
        final int dotScore = getInitialDots().size() - dots.size();
        return timeScore + dotScore;
    }

    /*
     * 指定された時刻での移動が完了した状態の敵リストを返します。
     * 返すのは自機の位置に関係しない敵(LRJ)のみです。
     */
    public ImmutableList<Enemy> getNonIntelligentEnemies(int time) {
        if (nonIntelligentEnemiesCache_ == null) {
            nonIntelligentEnemiesCache_ =
                    buildNonIntelligentEnemiesCache(this, getTimeLimit());
        }

        if (time < 0 || getTimeLimit() < time) {
            throw new IllegalArgumentException("invalid time: " + time);
        }
        return nonIntelligentEnemiesCache_.get(time);
    }

    private static boolean canGoW(List<char[]> fixedOnBoard, int x, int y) {
        if (isWall(fixedOnBoard, x, y)) {
            // 現在地が壁ならどこにもいけない
            return false;
        }
        return !isWall(fixedOnBoard, x + LEFT.dx, y + LEFT.dy);
    }

    private static boolean canGoE(List<char[]> fixedOnBoard, int x, int y) {
        if (isWall(fixedOnBoard, x, y)) {
            // 現在地が壁ならどこにもいけない
            return false;
        }
        return !isWall(fixedOnBoard, x + RIGHT.dx, y + RIGHT.dy);
    }

    private static boolean canGoN(List<char[]> fixedOnBoard, int x, int y) {
        if (isWall(fixedOnBoard, x, y)) {
            // 現在地が壁ならどこにもいけない
            return false;
        }
        return !isWall(fixedOnBoard, x + UP.dx, y + UP.dy);
    }

    private static boolean canGoS(List<char[]> fixedOnBoard, int x, int y) {
        if (isWall(fixedOnBoard, x, y)) {
            // 現在地が壁ならどこにもいけない
            return false;
        }
        return !isWall(fixedOnBoard, x + DOWN.dx, y + DOWN.dy);
    }

    private static final int countNexts(List<char[]> fixedOnBoard, int x, int y) {
        int nexts = 0;
        if (canGoS(fixedOnBoard, x, y)) {
            nexts++;
        }
        if (canGoW(fixedOnBoard, x, y)) {
            nexts++;
        }
        if (canGoN(fixedOnBoard, x, y)) {
            nexts++;
        }
        if (canGoE(fixedOnBoard, x, y)) {
            nexts++;
        }
        return nexts;
    }

    private static boolean isDeadEnd(List<char[]> fixedOnBoard, int x, int y) {
        final char type = getType(fixedOnBoard, x, y);
        if (type == DEADEND) {
            return true;
        }
        if (type == WALKWAY || type == INTERSECTION
                || isWall(fixedOnBoard, x, y)) {
            return false;
        }

        final int nexts = countNexts(fixedOnBoard, x, y);
        return nexts <= 1;
    }

    private static boolean isWalkWay(List<char[]> fixedOnBoard, int x, int y) {
        final char type = getType(fixedOnBoard, x, y);
        if (type == WALKWAY) {
            return true;
        }
        if (type == INTERSECTION || type == DEADEND
                || isWall(fixedOnBoard, x, y)) {
            return false;
        }

        final int nexts = countNexts(fixedOnBoard, x, y);
        return nexts == 2;
    }

    private static boolean isIntersection(List<char[]> fixedOnBoard, int x,
            int y) {
        final char type = getType(fixedOnBoard, x, y);
        if (type == INTERSECTION) {
            return true;
        }
        if (type == WALKWAY || type == DEADEND || isWall(fixedOnBoard, x, y)) {
            return false;
        }

        final int nexts = countNexts(fixedOnBoard, x, y);
        return 3 <= nexts;
    }

    private static boolean isOutOfRange(List<char[]> fixedOnBoard, int x, int y) {
        return (x <= 0 || y <= 0 || fixedOnBoard.get(0).length <= x || fixedOnBoard.size() <= y);
    }

    private static boolean isWall(List<char[]> fixedOnBoard, int x, int y) {
        if (isOutOfRange(fixedOnBoard, x, y)) {
            return true;
        }
        return getType(fixedOnBoard, x, y) == WALL;
    }

    private static char getType(List<char[]> fixedOnBoard, int x, int y) {
        return fixedOnBoard.get(y - 1)[x - 1];
    }

    public boolean existsDot(int x, int y, Iterable<Point> dots) {
        final Point target = new Point(x, y);
        for (Point dot : dots) {
            if (dot.equals(target)) {
                return true;
            }
        }
        return false;
    }

    private transient String stringRep_ = null;

    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        // timeLimit は同値判定に使用しません。

        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }

        if (hashCode() != obj.hashCode()) {
            return false;
        }

        final Board other = (Board) obj;

        return toString().equals(other.toString());
    }

    public String toString() {
        String stringRep = stringRep_;
        if (stringRep == null) {
            stringRep = toString(this).intern();
            stringRep_ = stringRep;
        }
        return stringRep;
    }

    public static String toString(Board b) {
        final int width = b.getWidth();
        final int height = b.getHeight();
        final ImmutableList<Enemy> enemies = b.getInitialEnemies();
        final Collection<Point> dots = b.getInitialDots();
        final int pacmanX = b.getInitialPacmanX();
        final int pacmanY = b.getInitialPacmanY();
        boolean detailOfPacmanWritten = false;
        final Iterator<Enemy> enemiesToPrintDetails = enemies.iterator();
        final StringBuilder whole = new StringBuilder();
        final StringBuilder line = new StringBuilder();
        for (int y = 1; y <= height; y++) {
            line.delete(0, line.length());
            // write wall
            for (int x = 1; x <= width; x++) {
                line.append(b.isWall(x, y) ? WALL : BLANK);
            }
            // write dots
            for (Point dot : dots) {
                if (dot.y != y) {
                    continue;
                }
                line.setCharAt(dot.x - 1, DOT);
            }
            // write enemies
            for (Enemy enemy : enemies) {
                if (enemy.y() != y) {
                    continue;
                }
                line.setCharAt(enemy.x() - 1, enemy.getName());
            }
            // write PacMan
            if (pacmanY == y) {
                line.setCharAt(pacmanX - 1, PACMAN);
            }

            // PacMan と 敵の詳細を表示
            if (!detailOfPacmanWritten) {
                detailOfPacmanWritten = true;
                line.append("   " + Board.PACMAN + " at (" + pacmanX + ", "
                        + pacmanY + ")");

            } else if (enemiesToPrintDetails.hasNext()) {
                line.append("   ").append(
                        enemiesToPrintDetails.next().toString());
            }
            line.append("\r\n");
            whole.append(line);
        }

        while (enemiesToPrintDetails.hasNext()) {
            line.delete(0, line.length());
            for (int i = 0; i < width; i++) {
                line.append(' ');
            }
            line.append("   ").append(enemiesToPrintDetails.next().toString());
            line.append("\r\n");
            whole.append(line);
        }
        return whole.toString();
    }

    public static void printInitialBoard(Board b) {
        printInitialBoard(b, false);
    }

    public static void printInitialBoard(Board b, boolean printNext) {
        Board.printBoard(b, 0, b.getInitialDots(), b.getInitialEnemies(),
                b.getInitialPacmanX(), b.getInitialPacmanY(), new char[0],
                printNext);
    }

    public static void printBoard(Board b, BoardState state, boolean writeNext) {
        final List<Enemy> nonIntelligentEnemies =
                state.getTime() == 0 ? b.getInitialNonIntelligentEnemies()
                        : b.getNonIntelligentEnemies(state.getTime() - 1);
        printBoard(
                b,
                state.getTime(),
                state.getDots(),
                Iterables.concat(nonIntelligentEnemies,
                        state.getIntelligentEnemies()), state.getPacmanX(),
                state.getPacmanY(), state.getHistory(), false);
    }

    public static void printBoard(Board b, BoardState state) {
        printBoard(b, state, false);
    }

    public static void printBoard(Board b, int time, Collection<Point> dots,
            Iterable<Enemy> enemies, int pacmanX, int pacmanY, char[] history,
            boolean writeNext) {
        final PrintStream out = System.out;

        Iterable<Enemy> nextEnemies =
                (!writeNext) ? null : Board.updateEnemies(b, enemies, pacmanX,
                        pacmanY);
        out.println();
        out.print("time: ");
        out.println(time);
        out.print("remaining dots: ");
        out.println(dots.size());
        out.print("score: ");
        out.println(b.getScore(dots, time));
        out.print("history: ");
        out.println(history);
        final int width = b.getWidth();
        final int height = b.getHeight();
        boolean detailOfPacmanWritten = false;
        final Iterator<Enemy> enemiesToPrintDetails = enemies.iterator();
        final StringBuilder sb = new StringBuilder();
        for (int y = 1; y <= height; y++) {
            sb.delete(0, sb.length());

            final int offsetForNext = b.getWidth() + 1/* one space */;

            writeWalls(sb, b, y);
            if (writeNext) {
                sb.append(' ');
                writeWalls(sb, b, y);
            }

            writeDots(sb, b, y, dots, 0);
            if (writeNext) {
                sb.append(' ');
                writeDots(sb, b, y, dots, offsetForNext);
            }

            writeEnemies(sb, b, y, enemies, 0);
            if (writeNext) {
                sb.append(' ');
                writeEnemies(sb, b, y, nextEnemies, offsetForNext);
            }

            // write PacMan
            writePacMan(sb, b, y, pacmanX, pacmanY, 0);
            if (writeNext) {
                sb.append(' ');
                writePacMan(sb, b, y, pacmanX, pacmanY, offsetForNext);
            }

            // PacMan と 敵の詳細を表示
            if (!detailOfPacmanWritten) {
                detailOfPacmanWritten = true;
                sb.append("   " + Board.PACMAN + " at (" + pacmanX + ", "
                        + pacmanY + ")");
            } else if (enemiesToPrintDetails.hasNext()) {
                sb.append("   ").append(enemiesToPrintDetails.next().toString());
            }

            out.println(sb.toString());
        }

        while (enemiesToPrintDetails.hasNext()) {
            sb.delete(0, sb.length());
            for (int i = 0; i < width + (!writeNext ? 0 : 2 + width); i++) {
                sb.append(' ');
            }
            sb.append("   ").append(enemiesToPrintDetails.next().toString());
            out.println(sb.toString());
        }
    }

    private static void writeWalls(StringBuilder sb, Board b, int y) {
        for (int x = 1; x <= b.getWidth(); x++) {
            sb.append(b.isWall(x, y) ? WALL : BLANK);
        }
    }

    private static void writeDots(StringBuilder sb, Board b, int y,
            Collection<Point> dots, int offset) {
        for (Point dot : dots) {
            if (dot.y != y) {
                continue;
            }
            sb.setCharAt(offset + dot.x - 1, DOT);
        }
    }

    private static void writeEnemies(StringBuilder sb, Board b, int y,
            Iterable<Enemy> enemies, int offset) {
        for (Enemy enemy : enemies) {
            if (enemy.y() != y) {
                continue;
            }
            sb.setCharAt(offset + enemy.x() - 1, enemy.getName());
        }
    }

    private static void writePacMan(StringBuilder sb, Board b, int y,
            int pacmanX, int pacmanY, int offset) {
        if (pacmanY == y) {
            sb.setCharAt(offset + pacmanX - 1, PACMAN);
        }
    }
}

abstract class Enemy {
    private final int currX_;
    private final int currY_;

    private final int prevX_;
    private final int prevY_;

    public Enemy(final int x, final int y) {
        if (x <= 0 || y <= 0) {
            throw new IllegalArgumentException("invalid coord. x: " + x
                    + ", y: " + y);
        }
        currX_ = x;
        currY_ = y;
        prevX_ = -1;
        prevY_ = -1;
    }

    protected Enemy(final int x, final int y, final Enemy prev) {
        if (x <= 0 || y <= 0) {
            throw new IllegalArgumentException("invalid coord. x: " + x
                    + ", y: " + y);
        }
        currX_ = x;
        currY_ = y;
        prevX_ = prev.x();
        prevY_ = prev.y();
    }

    public abstract char getName();

    public final Enemy getNext(Board b, int myX, int myY) {
        if (isInitialPosition()) {
            return firstMove(b);
        }

        final int x = x();
        final int y = y();

        if (b.isWalkWay(x, y)) {
            if (!fromN() && b.canGoN(x, y)) {
                return goN(b);
            } else if (!fromS() && b.canGoS(x, y)) {
                return goS(b);
            } else if (!fromW() && b.canGoW(x, y)) {
                return goW(b);
            } else {
                assert !fromE() && b.canGoE(x(), y());
                return goE(b);
            }
        }

        if (b.isDeadEnd(x, y)) {
            if (b.canGoN(x, y)) {
                return goN(b);
            } else if (b.canGoS(x, y)) {
                return goS(b);
            } else if (b.canGoW(x, y)) {
                return goW(b);
            } else {
                assert b.canGoE(x(), y());
                return goE(b);
            }
        }

        if (b.isWall(x, y)) {
            throw new RuntimeException(getName()
                    + " is on the wall. current: (" + x + ", " + y + ")");
        }

        assert b.isIntersection(x, y);
        return getNextAtIntersection(b, myX, myY);
    }

    protected abstract Enemy getNextAtIntersection(Board b, int myX, int myY);

    protected abstract Enemy createNextEnemy(Board b, int nextX, int nextY);

    protected abstract boolean isIntelligent();

    public final int x() {
        return currX_;
    }

    public final int y() {
        return currY_;
    }

    public final int prevX() {
        return prevX_;
    }

    public final int prevY() {
        return prevY_;
    }

    public final boolean isInitialPosition() {
        return prevX_ <= 0 || prevY_ <= 0;
    }

    protected final Enemy firstMove(Board b) {
        final int x = x();
        final int y = y();
        if (b.canGoS(x, y)) {
            return goS(b);
        } else if (b.canGoW(x, y)) {
            return goW(b);
        } else if (b.canGoN(x, y)) {
            return goN(b);
        } else if (b.canGoE(x, y)) {
            return goE(b);
        } else {
            throw new RuntimeException("can't move from (" + x + "," + y + ")");
        }
    }

    public final boolean fromW() {
        return prevX_ - currX_ < 0;
    }

    public final boolean fromE() {
        return 0 < prevX_ - currX_;
    }

    public final boolean fromN() {
        return prevY_ - currY_ < 0;
    }

    public final boolean fromS() {
        return 0 < prevY_ - currY_;
    }

    protected final Enemy goW(Board b) {
        final int x = x();
        final int y = y();
        if (!b.canGoW(x, y)) {
            throw new RuntimeException("can't go to left. current: (" + x
                    + ", " + y + ")");
        }
        return createNextEnemy(b, x - 1, y);
    }

    protected final Enemy goE(Board b) {
        final int x = x();
        final int y = y();
        if (!b.canGoE(x, y)) {
            throw new RuntimeException("can't go to right. current: (" + x
                    + ", " + y + ")");
        }
        return createNextEnemy(b, x + 1, y);
    }

    protected final Enemy goN(Board b) {
        final int x = x();
        final int y = y();
        if (!b.canGoN(x, y)) {
            throw new RuntimeException("can't go to up. current: (" + x + ", "
                    + y + ")");
        }
        return createNextEnemy(b, x, y - 1);
    }

    protected final Enemy goS(Board b) {
        final int x = x();
        final int y = y();
        if (!b.canGoS(x, y)) {
            throw new RuntimeException("can't go to down. current: (" + x
                    + ", " + y + ")");
        }
        return createNextEnemy(b, x, y + 1);
    }

    @Override
    public String toString() {
        return "" + getName() + " at (" + x() + ", " + y() + ") to "
                + Direction.getDirection(x(), y(), prevX(), prevY());
    }
}

final class EnemyV extends Enemy {

    public EnemyV(int x, int y) {
        super(x, y);
    }

    EnemyV(int x, int y, Enemy prev) {
        super(x, y, prev);
    }

    @Override
    public char getName() {
        return Board.ENEMY_V;
    }

    @Override
    protected Enemy getNextAtIntersection(Board b, int myX, int myY) {
        final int x = x();
        final int y = y();
        final int dx = myX - x();
        final int dy = myY - y();

        if (dy != 0) {
            if (0 < dy && b.canGoS(x, y)) {
                return goS(b);
            } else if (dy < 0 && b.canGoN(x, y)) {
                return goN(b);
            }
        }
        if (dx != 0) {
            if (0 < dx && b.canGoE(x, y)) {
                return goE(b);
            } else if (dx < 0 && b.canGoW(x, y)) {
                return goW(b);
            }
        }

        // 上記の条件に当てはまらなければ、最初と同じ移動
        return firstMove(b);
    }

    @Override
    protected EnemyV createNextEnemy(Board b, int nextX, int nextY) {
        return new EnemyV(nextX, nextY, this);
    }

    @Override
    protected boolean isIntelligent() {
        return true;
    }
}

final class EnemyH extends Enemy {

    public EnemyH(int x, int y) {
        super(x, y);
    }

    EnemyH(int x, int y, Enemy prev) {
        super(x, y, prev);
    }

    @Override
    public char getName() {
        return Board.ENEMY_H;
    }

    @Override
    protected Enemy getNextAtIntersection(Board b, int myX, int myY) {
        final int x = x();
        final int y = y();
        final int dx = myX - x();
        final int dy = myY - y();

        if (dx != 0) {
            if (0 < dx && b.canGoE(x, y)) {
                return goE(b);
            } else if (dx < 0 && b.canGoW(x, y)) {
                return goW(b);
            }
        }
        if (dy != 0) {
            if (0 < dy && b.canGoS(x, y)) {
                return goS(b);
            } else if (dy < 0 && b.canGoN(x, y)) {
                return goN(b);
            }
        }

        // 上記の条件に当てはまらなければ、最初と同じ移動
        return firstMove(b);
    }

    @Override
    protected EnemyH createNextEnemy(Board b, int nextX, int nextY) {
        return new EnemyH(nextX, nextY, this);
    }

    @Override
    protected boolean isIntelligent() {
        return true;
    }
}

final class EnemyL extends Enemy {

    public EnemyL(int x, int y) {
        super(x, y);
    }

    EnemyL(int x, int y, Enemy prev) {
        super(x, y, prev);
    }

    @Override
    public char getName() {
        return Board.ENEMY_L;
    }

    @Override
    protected Enemy getNextAtIntersection(Board b, int myX, int myY) {
        return getNextAtIntersection(b, this);
    }

    static Enemy getNextAtIntersection(Board b, Enemy prev) {
        final int x = prev.x();
        final int y = prev.y();
        final int prevX = prev.prevX();
        final int prevY = prev.prevY();

        if (Board.canTurnToLeft(b, x, y, prevX, prevY)) {
            return prev.createNextEnemy(b,
                    Board.turnToLeftX(x, y, prevX, prevY),
                    Board.turnToLeftY(x, y, prevX, prevY));
        } else if (Board.canGoAhead(b, x, y, prevX, prevY)) {
            return prev.createNextEnemy(b, Board.goAheadX(x, y, prevX, prevY),
                    Board.goAheadY(x, y, prevX, prevY));
        } else {
            assert Board.canTurnToRight(b, x, y, prevX, prevY);
            return prev.createNextEnemy(b,
                    Board.turnToRightX(x, y, prevX, prevY),
                    Board.turnToRightY(x, y, prevX, prevY));
        }
    }

    @Override
    protected EnemyL createNextEnemy(Board b, int nextX, int nextY) {
        return new EnemyL(nextX, nextY, this);
    }

    @Override
    protected boolean isIntelligent() {
        return false;
    }
}

final class EnemyR extends Enemy {

    public EnemyR(int x, int y) {
        super(x, y);
    }

    private EnemyR(int x, int y, Enemy prev) {
        super(x, y, prev);
    }

    @Override
    public char getName() {
        return Board.ENEMY_R;
    }

    @Override
    protected Enemy getNextAtIntersection(Board b, int myX, int myY) {
        return getNextAtIntersection(b, this);
    }

    static Enemy getNextAtIntersection(Board b, Enemy prev) {
        final int x = prev.x();
        final int y = prev.y();
        final int prevX = prev.prevX();
        final int prevY = prev.prevY();

        if (Board.canTurnToRight(b, x, y, prevX, prevY)) {
            return prev.createNextEnemy(b,
                    Board.turnToRightX(x, y, prevX, prevY),
                    Board.turnToRightY(x, y, prevX, prevY));
        } else if (Board.canGoAhead(b, x, y, prevX, prevY)) {
            return prev.createNextEnemy(b, Board.goAheadX(x, y, prevX, prevY),
                    Board.goAheadY(x, y, prevX, prevY));
        } else {
            assert Board.canTurnToLeft(b, x, y, prevX, prevY);
            return prev.createNextEnemy(b,
                    Board.turnToLeftX(x, y, prevX, prevY),
                    Board.turnToLeftY(x, y, prevX, prevY));
        }
    }

    @Override
    protected EnemyR createNextEnemy(Board b, int nextX, int nextY) {
        return new EnemyR(nextX, nextY, this);
    }

    @Override
    protected boolean isIntelligent() {
        return false;
    }
}

class EnemyJ extends Enemy {
    private final boolean actAsL_;

    public EnemyJ(int x, int y) {
        super(x, y);
        actAsL_ = false; // 最初の交差点進入でL なので、最初は R にしておく
    }

    EnemyJ(int x, int y, EnemyJ prev, Board b) {
        super(x, y, prev);
        // 交差点に入ったら LR の入れ替え
        actAsL_ = b.isIntersection(x, y) ? !prev.actAsL() : prev.actAsL();
    }

    @Override
    public final char getName() {
        return Board.ENEMY_J;
    }

    private boolean actAsL() {
        return actAsL_;
    }

    @Override
    protected Enemy getNextAtIntersection(Board b, int myX, int myY) {
        if (actAsL_) {
            return EnemyL.getNextAtIntersection(b, this);
        } else {
            return EnemyR.getNextAtIntersection(b, this);
        }
    }

    @Override
    protected EnemyJ createNextEnemy(Board b, int nextX, int nextY) {
        return new EnemyJ(nextX, nextY, this, b);
    }

    @Override
    protected boolean isIntelligent() {
        return false;
    }

    @Override
    public String toString() {
        return super.toString() + " act as "
                + (actAsL_ ? Board.ENEMY_L : Board.ENEMY_R);
    }
}

class BoardState {
    private final int pacmanX_;
    private final int pacmanY_;
    private final Collection<Point> dots_;
    private final ImmutableList<Enemy> intelligentEnemies_;
    private final int time_;
    private final char[] history_;

    private final boolean backUsedOnThisWalkWay_;
    private final boolean stayUsedOnThisWalkWay_;

    public BoardState(Board board) {
        pacmanX_ = board.getInitialPacmanX();
        pacmanY_ = board.getInitialPacmanY();
        dots_ = board.getInitialDots();
        intelligentEnemies_ = board.getInitialIntelligentEnemies();
        time_ = 0;
        history_ = new char[0];
        stayUsedOnThisWalkWay_ = false;
        backUsedOnThisWalkWay_ = false;
    }

    public BoardState(int pacmanX, int pacmanY, Collection<Point> dots,
            ImmutableList<Enemy> intelligentEnemies, BoardState prevState,
            boolean backUsedOnThisWalkWay, boolean stayUsedOnThisWalkWay) {
        pacmanX_ = pacmanX;
        pacmanY_ = pacmanY;
        dots_ = dots;
        intelligentEnemies_ = intelligentEnemies;
        time_ = prevState.getTime() + 1;
        final char[] prevHistory = prevState.getHistory();
        history_ = new char[prevHistory.length + 1];
        System.arraycopy(prevHistory, 0, history_, 0, prevHistory.length);
        history_[prevHistory.length] =
                Direction.getDirection(pacmanX, pacmanY,
                        prevState.getPacmanX(), prevState.getPacmanY()).literal;
        backUsedOnThisWalkWay_ = backUsedOnThisWalkWay;
        stayUsedOnThisWalkWay_ = stayUsedOnThisWalkWay;
    }

    public final int getPacmanX() {
        return pacmanX_;
    }

    public final int getPacmanY() {
        return pacmanY_;
    }

    public final Collection<Point> getDots() {
        return dots_;
    }

    public final int getEatenDotCount(Board b) {
        return b.getInitialDots().size() - getDots().size();
    }

    public final ImmutableList<Enemy> getIntelligentEnemies() {
        return intelligentEnemies_;
    }

    public final int getTime() {
        return time_;
    }

    public final int getScore(Board b) {
        return b.getScore(getDots(), getTime());
    }

    public final char[] getHistory() {
        return history_;
    }

    public final boolean backUsedOnThisWalkWay() {
        return backUsedOnThisWalkWay_;
    }

    public final boolean stayUsedOnThisWalkWay() {
        return stayUsedOnThisWalkWay_;
    }

    public boolean isAlive() {
        return true;
    }

    public final boolean isCleared() {
        return isAlive() && dots_.isEmpty();
    }

    public final BoardState move(Board board, Direction direction,
            ImmutableList<Enemy> nextIntelligentEnemies, boolean isBack) {
        final int x = getPacmanX() + direction.dx;
        final int y = getPacmanY() + direction.dy;
        if (board.getTimeLimit() <= getTime()
                || !Board.isSafe(Iterables.concat(
                        board.getNonIntelligentEnemies(getTime()),
                        nextIntelligentEnemies), x, y, getPacmanX(),
                        getPacmanY())) {
            return new GameOverBoardState(x, y, getDots(),
                    nextIntelligentEnemies, this);
        }
        final Collection<Point> currentDots = Lists.newArrayList(getDots());
        currentDots.remove(new Point(x, y));

        final BoardState newState =
                new BoardState(
                        x,
                        y,
                        // 変更がなければドットのリストを共有する
                        (getDots().size() == currentDots.size()) ? getDots()
                                : currentDots,
                        nextIntelligentEnemies,
                        this,
                        board.isWalkWay(x, y)
                                && board.isWalkWay(getPacmanX(), getPacmanY())
                                && (isBack || backUsedOnThisWalkWay()),
                        board.isWalkWay(x, y)
                                && board.isWalkWay(getPacmanX(), getPacmanY())
                                && (direction == STAY || stayUsedOnThisWalkWay()));
        return newState;
    }

    public static BoardState batchMove(BoardState state, Board board,
            char[] route) {
        if (route == null) {
            return state;
        }
        for (char commandChar : route) {
            final Direction nextCommand = Direction.fromLiteral(commandChar);

            final int prevTime = state.getTime();
            state = doCommand(state, board, nextCommand);
            if (state.getTime() == prevTime) {
                // 進まなかったということはエラーなので、できたところまでを返す。
                return state;
            }

            if (!state.isAlive()) {
                System.out.println("");
                System.out.println("GameOver!");
                return state;
            }
            if (state.isCleared()) {
                System.out.println("");
                System.out.println("Cleared!");
                return state;
            }
        }
        return state;
    }

    private static BoardState doCommand(BoardState state, Board board,
            Direction nextCommand) {
        if (!board.canGo(state.getPacmanX(), state.getPacmanY(), nextCommand)) {
            System.out.println("can't move to " + nextCommand);
            return state;
        }

        final ImmutableList<Enemy> nextIntelligentEnemies =
                Board.updateEnemies(board, state.getIntelligentEnemies(),
                        state.getPacmanX(), state.getPacmanY());
        state =
                state.move(board, nextCommand, nextIntelligentEnemies, false/*厳密には違うが*/);
        Board.printBoard(board, state, true);
        return state;
    }
}

/**
 * GameOver を表す特別な {@link BoardState}。
 *
 * {@link BoardState} は大量にインスタンスが作られる可能性があるため、フラグを持たせると
 * メモリが無駄に消費される。GameOver なインスタンスは殆ど作られないため、サブクラスとして
 * 表現してみた。
 */
final class GameOverBoardState extends BoardState {

    public GameOverBoardState(int pacmanX, int pacmanY, Collection<Point> dots,
            ImmutableList<Enemy> intelligentEnemies, BoardState prevState) {
        super(pacmanX, pacmanY, dots, intelligentEnemies, prevState, true, true);
    }

    @Override
    public boolean isAlive() {
        return false;
    }
}

PacManBfsSolverMain.java

/*
 * PacMan を幅優先探索で解くプログラム。状態数が多くなるので、せいぜい Lv1 程度が限度。
 *
 * (C) YAMAZAKI Makoto <makoto1975@gmail.com> 2010.
 *
 * 戻りを制限しないとメモリはたくさん必要。 main メソッド中の backOnWalkWayTimeLimit を
 * Integer.MAX_VALE にすると無制限になるが、その場合は -Xmx4096m とかでないと解が求まらない。
 * とりあえず制限時間の 20% にしているが、根拠なし。 Lv1 の問題であれば、 t=6 以降は戻り不要。
 *
 * 単純に全ての動きを探索するとさすがに状態が多すぎるので、戻る操作は通路内で2回以上行えないように
 * 制限している。
 * その場にとどまる操作を使用するかどうかを選択できるようにしているが、使用する場合であっても2回続けて
 * 使用するパターンは探索から除外している。
 *
 * 依存ライブラリとして Google Collections を使用している。
 *
 * $Id: PacManBfsSolverMain.java 134 2010-08-22 10:35:46Z zaki $
 */
package org.zakky.gdd2010.pacman;

import static org.zakky.gdd2010.pacman.Board.Direction.STAY;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Deque;
import java.util.List;
import org.zakky.gdd2010.pacman.Board.Direction;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

final class BFSPacManSolver {
    private final Board board_;
    private final char[] staticRoute_;
    private final Deque<BoardState> queue_ = Lists.newLinkedList();
    private final int backOnWalkWayTimeLimit_;

    public BFSPacManSolver(Board board, int backOnWalkWayTimeLimit,
            char[] staticRoute) {
        assert board != null;
        board_ = board;
        backOnWalkWayTimeLimit_ = backOnWalkWayTimeLimit;
        staticRoute_ = staticRoute;
    }

    public BoardState solve() throws Exception {
        final BoardState initialState = new BoardState(board_);

        // まずは最初の状態をベストとしておく。
        BoardState bestState =
                BoardState.batchMove(initialState, board_, staticRoute_);
        int bestScore = bestState.getScore(board_);
        // 最初は新しい時刻と判定されるように -1 しておく。
        int prevTime = bestState.getTime() - 1;

        enqueue(bestState);

        while (hasMoreState()) {
            final BoardState state = dequeue();
            if (state.getTime() != prevTime) {
                // 新しい時刻に入った
                prevTime = state.getTime();
                System.out.println("searching " + state.getTime()
                        + " step path. state count in queue is "
                        + (queue_.size() + 1));
            }

            // 直前の PacMan の位置
            final int prevX = state.getPacmanX();
            final int prevY = state.getPacmanY();
            final ImmutableList<Enemy> nextIntelligentEnemies =
                    Board.updateEnemies(board_, state.getIntelligentEnemies(),
                            prevX, prevY);
            // 次の行動の候補を列挙し、GameOver にならなければキューに入れる

            final char[] history = state.getHistory();
            final Direction backward =
                    history.length == 0 ? null
                            : Direction.fromLiteral(
                                    state.getHistory()[state.getHistory().length - 1]).opposite();

            final List<Direction> candidates = Lists.newArrayList();
            for (Direction dir : Direction.values()) {
                if (dir == STAY) {
                    if (!board_.useStay()) {
                        continue;
                    }
                    if (backward == STAY) {
                        // 前回の移動も STAY だったら候補から除外
                        // TODO 連続 STAY は除外しているが、本当にこれで最適解といえるか証明する
                        continue;
                    }
                    if (state.stayUsedOnThisWalkWay()) {
                        continue;
                    }
                    candidates.add(dir);
                    continue;
                }
                if (!board_.canGo(prevX, prevY, dir)) {
                    continue;
                }
                if (board_.isWalkWay(prevX, prevY) && dir == backward) {
                    if (state.backUsedOnThisWalkWay()
                            || backOnWalkWayTimeLimit_ < state.getTime()) {
                        // FIXME 戻らないとGameOver の場合はもどるようにする
                        continue;
                    }
                }
                candidates.add(dir);
            }

            for (Direction to : candidates) {
                final BoardState newState =
                        state.move(board_, to, nextIntelligentEnemies,
                                backward != STAY && backward == to);
                if (newState.isAlive()) {
                    final int score = newState.getScore(board_);
                    if (bestScore < score) {
                        bestState = newState;
                        bestScore = score;
                        // 記録更新
                        Board.printBoard(board_, newState);
                        if (newState.getDots().isEmpty()) {
                            return bestState;
                        }
                    }
                    enqueue(newState);
                }
            }
        }
        return bestState;
    }

    private void enqueue(BoardState state) {
        queue_.offer(state);
    }

    private boolean hasMoreState() {
        return !queue_.isEmpty();
    }

    private BoardState dequeue() {
        return queue_.remove();
    }
}

public class PacManBfsSolverMain {
    public static void main(String[] args) throws Exception {
        final boolean useStay = true;
        final Board board =
                Board.buildBoard(new BufferedReader(new InputStreamReader(
                        System.in, "UTF-8")), useStay);

        final String mapName =
                Board.KNOWN_MAPS.get(Integer.valueOf(board.hashCode()));
        if (mapName != null) {
            System.out.println("mapname: " + mapName);
        }
        System.out.println("timelimit: " + board.getTimeLimit());
        System.out.println("width: " + board.getWidth());
        System.out.println("height: " + board.getHeight());
        Board.printInitialBoard(board);

        final int backOnWalkWayTimeLimit = Integer.MAX_VALUE; /* Lv1 なら戻る制限なしでも解ける */
        final BoardState answer =
                new BFSPacManSolver(
                        board,
                        backOnWalkWayTimeLimit,
                        null).solve();
        System.out.println("");
        if (answer.isCleared()) {
            System.out.println("Game Cleared (score=" + answer.getScore(board)
                    + ").");
        } else {
            System.out.println("Gema Over(score=" + answer.getScore(board)
                    + ").");
        }
        System.out.println(answer.getEatenDotCount(board) + " dots eaten in "
                + answer.getTime() + " steps.");
        System.out.println(answer.getHistory());
    }
}

PacManDfsSolverMain.java

/*
 * PacMan を深さ優先で探索するための Main クラス。
 *
 * (C) YAMAZAKI Makoto <makoto1975@gmail.com> 2010.
 *
 * 依存ライブラリとして Google Collections を使用している。
 *
 * Lv2
 * 以下のところまでこのプログラムで求め、Lv1 で使用した幅優先探索プログラムで残りを解いた。
 * kkkllljjjhhhkkkllkkkllljjjh.lljjjlllllhhkkklllllkkkllljjj.hhjjjhhhkkk.ll.
 * kkkhkklljjhhhhhhhh.ljjjhhjk.hkkklkkhhhhjjhhkklllkkklhhkkkllljjjlllkkkllll
 * lhhjjjhhjjjhllllkkkllkkkllljjjhhjjjlljjjjjhhhhhhhhhhhjjjhhhkkkllllllllkkk
 * kkhhhhhhhjkllkkkhkkklhhhhjjj
 *
 * $Id: PacManDfsSolverMain.java 132 2010-08-22 10:05:51Z zaki $
 */
package org.zakky.gdd2010.pacman;

import static org.zakky.gdd2010.pacman.Board.Direction.STAY;
import java.awt.Point;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import org.zakky.gdd2010.pacman.Board.Direction;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

final class DFSPacManSolver {
    private final boolean debug_;

    private final Board board_;
    private final char[] staticRoute_;
    private final int backOnWalkWayTimeLimit_;
    private int cutOffTime_;

    private BoardState bestState_;
    private int bestScore_;

    private int callCountOffindBest_ = 0;

    public DFSPacManSolver(Board board, int backOnWalkWayTimeLimit,
            char[] staticRoute, int cutOffTime, boolean debug) {
        assert board != null;
        board_ = board;
        backOnWalkWayTimeLimit_ = backOnWalkWayTimeLimit;
        cutOffTime_ = Math.min(cutOffTime, board.getTimeLimit());
        staticRoute_ = staticRoute;
        debug_ = debug;
    }

    public BoardState solve() throws Exception {
        final BoardState initialState = new BoardState(board_);

        // まずは最初の状態をベストとしておく。
        bestState_ = BoardState.batchMove(initialState, board_, staticRoute_);
        bestScore_ = bestState_.getScore(board_);

        return findBest(bestState_);
    }

    private BoardState findBest(BoardState state) {
        callCountOffindBest_++;
        if ((callCountOffindBest_ & 0xfffff) == 0) {
            System.out.print(callCountOffindBest_);
            System.out.print(": ");
            System.out.println(state.getHistory());
            if ((callCountOffindBest_ & 0xffffff) == 0) {
                Board.printBoard(board_, bestState_);
                System.out.println("current best");
                System.out.print("current cut-off time: ");
                System.out.println(cutOffTime_);
            }
        }
        if (debug_) {
            Board.printBoard(board_, state);
        }
        if (state.isCleared() || !state.isAlive()) {
            return state;
        }
        if (cutOffTime_ - state.getDots().size()
                + getShortestTimeToNearestDot(state) < state.getTime()) {
            return new GameOverBoardState(state.getPacmanX(),
                    state.getPacmanY(), state.getDots(),
                    state.getIntelligentEnemies(), state);
        }

        final char[] history = state.getHistory();
        final Direction last =
                history.length == 0 ? null
                        : Direction.fromLiteral(state.getHistory()[state.getHistory().length - 1]);
        final Direction forward =
                (last != STAY) ? last
                        : history.length == 0 ? null
                                : Direction.fromLiteral(state.getHistory()[state.getHistory().length - 2]);
        final Direction backward = forward == null ? null : forward.opposite();

        LinkedList<Direction> candidates = Lists.newLinkedList();
        for (Direction dir : allDirsBySearchOrder(forward)) {

            if (dir == Direction.STAY) {
                if (!board_.useStay()) {
                    continue;
                }
                if (last == STAY) {
                    // 前回の移動も STAY だったら候補から除外
                    // TODO 連続 STAY は除外しているが、本当にこれで最適解といえるか証明する
                    continue;
                }
                if (state.stayUsedOnThisWalkWay()) {
                    continue;
                }
            }
            if (!board_.canGo(state.getPacmanX(), state.getPacmanY(), dir)) {
                continue;
            }
            if (board_.isWalkWay(state.getPacmanX(), state.getPacmanY())
                    && dir == backward) {
                if (state.backUsedOnThisWalkWay()
                        || backOnWalkWayTimeLimit_ < state.getTime()) {
                    // FIXME 戻らないとGameOver の場合はもどるようにする
                    continue;
                }
            }
            candidates.add(dir);
        }

        final int pacmanX = state.getPacmanX();
        final int pacmanY = state.getPacmanY();
        final ImmutableList<Enemy> nextIntelligentEnemies =
                Board.updateEnemies(board_, state.getIntelligentEnemies(),
                        pacmanX, pacmanY);

        // 敵との当りがない場合は STAY を探索候補から除外する
        {
            boolean stayIncluded = false;
            boolean unsafeCandidatesIncluded = false;
            final Iterable<Enemy> nextEnemies =
                    Iterables.concat(nextIntelligentEnemies,
                            board_.getNonIntelligentEnemies(state.getTime()));
            for (final Direction d : candidates) {
                if (d == STAY) {
                    stayIncluded = true;
                    continue;
                }
                if (!Board.isSafe(nextEnemies, pacmanX + d.dx, pacmanY + d.dy,
                        pacmanX, pacmanY)) {
                    unsafeCandidatesIncluded = true;
                    break;
                }
            }
            if (stayIncluded && !unsafeCandidatesIncluded) {
                candidates.remove(STAY);
            }
        }

        candidates = reorderByPriority(candidates, state);

        BoardState currentBestState = state;
        int currentBestScore = currentBestState.getScore(board_);

        if (debug_) {
            System.out.println("candidates:  " + candidates);
            System.out.println("Press any Key to continue.");
            try {
                System.in.read();
            } catch (IOException e) {
                // assert true;
            }
        }
        for (Direction candidate : candidates) {
            final BoardState nextState =
                    state.move(board_, candidate, nextIntelligentEnemies, false/*とりあえず*/);
            final BoardState result = findBest(nextState);
            final int score = result.getScore(board_);
            if (currentBestScore < score) {
                currentBestState = result;
                currentBestScore = score;
                if (bestScore_ < score) {
                    bestState_ = result;
                    bestScore_ = score;
                    Board.printBoard(board_, state);
                    System.out.println("new record!");
                    if (bestState_.isCleared()) {
                        cutOffTime_ = bestState_.getTime();
                    }
                    System.out.print("current cut-off time: ");
                    System.out.println(cutOffTime_);
                }
            }
        }
        return currentBestState;
    }

    private int getShortestTimeToNearestDot(BoardState state) {
        final int pacmanX = state.getPacmanX();
        final int pacmanY = state.getPacmanY();
        final Collection<Point> dots = state.getDots();
        if (dots.isEmpty()) {
            return 0;
        }
        int shortestTime = Integer.MAX_VALUE;
        for (Point dot : dots) {
            // とりあえず壁は無視する
            final int requiredTime =
                    Math.abs(dot.x - pacmanX) + Math.abs(dot.y - pacmanY);
            if (requiredTime < shortestTime) {
                shortestTime = requiredTime;
            }
        }
        return shortestTime;
    }

    private static final ImmutableList<Direction> VALUES_FOR_UNKNOWN =
            ImmutableList.of(Direction.STAY, Direction.UP, Direction.RIGHT,
                    Direction.LEFT, Direction.DOWN);
    private static final ImmutableList<Direction> VALUES_FOR_UP =
            ImmutableList.of(Direction.STAY, Direction.RIGHT, Direction.UP,
                    Direction.LEFT, Direction.DOWN);
    private static final ImmutableList<Direction> VALUES_FOR_RIGHT =
            ImmutableList.of(Direction.STAY, Direction.DOWN, Direction.RIGHT,
                    Direction.UP, Direction.LEFT);
    private static final ImmutableList<Direction> VALUES_FOR_DOWN =
            ImmutableList.of(Direction.STAY, Direction.LEFT, Direction.DOWN,
                    Direction.RIGHT, Direction.UP);
    private static final ImmutableList<Direction> VALUES_FOR_LEFT =
            ImmutableList.of(Direction.STAY, Direction.UP, Direction.LEFT,
                    Direction.DOWN, Direction.RIGHT);

    private ImmutableList<Direction> allDirsBySearchOrder(Direction heading) {
        if (heading == null) {
            return VALUES_FOR_UNKNOWN;
        }
        // 進行方向に対して STAY、右、正面、左、後ろの順
        switch (heading) {
        case UP:
            return VALUES_FOR_UP;
        case RIGHT:
            return VALUES_FOR_RIGHT;
        case DOWN:
            return VALUES_FOR_DOWN;
        case LEFT:
            return VALUES_FOR_LEFT;
        case STAY:
            /* fall-through */
        default:
            throw new IllegalArgumentException("invalid head direction: "
                    + heading);
        }
    }

    private LinkedList<Direction> reorderByPriority(
            LinkedList<Direction> candidates, BoardState state) {
        List<Direction> noDot = null;
        final ListIterator<Direction> it = candidates.listIterator();
        while (it.hasNext()) {
            final Direction candidate = it.next();
            final int nextX = state.getPacmanX() + candidate.dx;
            final int nextY = state.getPacmanY() + candidate.dy;

            if (!board_.existsDot(nextX, nextY, state.getDots())) {
                if (noDot == null) {
                    noDot = Lists.newArrayListWithCapacity(candidates.size());
                }
                it.remove();
                noDot.add(candidate);
            }
        }

        if (noDot != null) {
            candidates.addAll(noDot);
        }
        // 探索の優先順を決定する
        return candidates;
    }
}

public class PacManDfsSolverMain {
    private static final ImmutableMap<String, String> STATIC_ROUTES =
            ImmutableMap.of("Lv1", "", "Lv2", "", "Lv3",
                    "hhkkhhlljjlllkklllkllllkkkkkkkklllllllljjjjjhhhllljjj");
    private static final ImmutableMap<String, Integer> CUT_OFF_TIMES =
            ImmutableMap.of("Lv1", Integer.valueOf(37), "Lv2",
                    Integer.valueOf(221), "Lv3", Integer.valueOf(451));

    public static void main(String[] args) throws Exception {
        final boolean useStay = false;
        final boolean debug = false;
        final BufferedReader input =
                new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
        final Board board = Board.buildBoard(input, useStay);
        final String mapName =
                Board.KNOWN_MAPS.get(Integer.valueOf(board.hashCode()));
        System.out.println("mapname: "
                + (mapName == null ? "unknown" : mapName));

        Board.printInitialBoard(board, true);

        final String staticRoute =
                mapName == null ? null : STATIC_ROUTES.get(mapName);
        final Integer cutOffTime =
                mapName == null ? null : CUT_OFF_TIMES.get(mapName);

        final BoardState answer =
                new DFSPacManSolver(board, Integer.MAX_VALUE,
                        staticRoute == null ? null : staticRoute.toCharArray(),
                        cutOffTime == null ? Integer.MAX_VALUE
                                : cutOffTime.intValue(), debug).solve();
        System.out.println("");
        if (answer.isCleared()) {
            System.out.println("Game Cleared (score=" + answer.getScore(board)
                    + ").");
        } else {
            System.out.println("Gema Over(score=" + answer.getScore(board)
                    + ").");
        }
        System.out.println(answer.getEatenDotCount(board) + " dots eaten in "
                + answer.getTime() + " steps.");
        System.out.println(answer.getHistory());
    }
}

«前の日記(2010-03-01) 最新