argius note

プログラミング関連

ミニゲーム?「迷いの森」

今朝は早くに目が覚めたので、珍しくちょっと勉強なんかしたりして、ちょっとだけ早く家を出ました。
おとといの暑さとは打って変わって、秋らしい風の冷たさを感じつつ、気分が良いので鼻歌など鳴らしながら駅への道を歩いていると、ふと思いつきました。


「だから4回なのか...」



このとき思いついたことを元に、ゲーム(というほどのものではないですが)を作ってみました。
タイトルは「迷いの森」。ふた昔くらい前のゲームではよくあった、ループゾーンを脱出するギミックを模したものです。

(クイズ:さて、この鼻歌は何の曲だったのでしょうか。ヒントはコードの中にあるよ!)


※念のため:このプログラムはシミュレートが目的です。通常はこういう場合QueueかStringBuilder(の方が簡単)などを使ってください。
Javaコード ※2進リテラルを使っているため、Java1.7でコンパイルする必要があります(コメントの16進リテラルに書き換えれば1.5でもコンパイルできます)

import static javax.swing.ScrollPaneConstants.*;

import java.awt.*;
import java.awt.event.*;
import java.io.*;

import javax.swing.*;
import javax.swing.text.*;

final class LoopTrap extends JFrame {

    //    static final byte target = (byte)0b11_11_11_11; // 0xFF 北北北北
    static final byte target = (byte)0b11_01_00_01; // 0xD1 北西南西

    PrintWriter out;
    byte history;

    void onKeyPressed(KeyEvent e) {
        final int keyCode = e.getKeyCode();
        Direction direction = Direction.asKeyCode(keyCode);
        out.println("入力: " + KeyEvent.getKeyText(keyCode));
        updateHistory(direction);
        checkHistory();
    }

    void updateHistory(Direction direction) {
        history = (byte)((history << 2) | direction.ordinal());
    }

    void checkHistory() {
        out.print("履歴: " + getHistoryString());
        out.println(" => " + (history == target ? "脱出した!" : "迷いの森にいる!"));
    }

    String getHistoryString() {
        StringBuilder buffer = new StringBuilder();
        Direction[] directions = Direction.values();
        for (int i = 3; i >= 0; i--) {
            int order = (history >> i * 2) & 0x03;
            buffer.append(directions[order].toCharacter());
        }
        return buffer.toString();
    }

    enum Direction {
        South('南'), West('西'), East('東'), North('北');
        char c;
        Direction(char c) {
            this.c = c;
        }
        char toCharacter() {
            return c;
        }
        static Direction asKeyCode(int keyCode) {
            switch (keyCode) {
                case KeyEvent.VK_DOWN:
                    return South;
                case KeyEvent.VK_LEFT:
                    return West;
                case KeyEvent.VK_RIGHT:
                    return East;
                case KeyEvent.VK_UP:
                    return North;
            }
            throw new IllegalArgumentException(String.format("code=%d(%s)",
                                                             keyCode,
                                                             KeyEvent.getKeyText(keyCode)));
        }
    }

    // ここから下はGUIのためのコード
    LoopTrap() {
        setTitle("迷いの森");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        final JTextPane text = new JTextPane() {
            @Override
            protected void processKeyEvent(KeyEvent e) {
                if (e.getID() != KeyEvent.KEY_PRESSED)
                    return;
                try {
                    onKeyPressed(e);
                } catch (IllegalArgumentException ex) {
                    //                    out.println(ex);
                }
                setCaretPosition(getDocument().getEndPosition().getOffset() - 1);
            }
        };
        text.setEditable(false);
        this.history = 0x00;
        this.out = new PrintWriter(new DocumentWriter(text.getDocument()), true);
        add(new JScrollPane(text, VERTICAL_SCROLLBAR_ALWAYS, HORIZONTAL_SCROLLBAR_NEVER),
            BorderLayout.CENTER);
    }

    static final class DocumentWriter extends Writer {
        Document doc;
        DocumentWriter(Document doc) {
            this.doc = doc;
        }
        @Override
        public void write(char[] cbuf, int off, int len) throws IOException {
            int offset = doc.getEndPosition().getOffset();
            try {
                doc.insertString(offset, String.valueOf(cbuf, off, len), null);
            } catch (BadLocationException ex) {
                throw new RuntimeException(ex);
            }
        }
        @Override
        public void flush() throws IOException {
            // ignore
        }
        @Override
        public void close() throws IOException {
            // ignore
        }
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            // for 1.6- @Override
            public void run() {
                JFrame f = new LoopTrap();
                f.setSize(300, 300);
                f.setLocationRelativeTo(null);
                f.setVisible(true);
            }
        });
    }

}

実行イメージ


解説です。
これは、指定した順番に進まないと先に進めない「迷いの森」を脱出するゲームです。ランダムで順番を生成したり、制限時間を設けたりすれば、よりゲームらしくなるかもしれません。(当然、たいして面白くはならないでしょうが。)


まず、方角を2ビットで定義します。2ビットでは4通りしかできないので、どれかをデフォルトにします*1

00 : 下・南 (デフォルト)
01 : 左・西
10 : 右・東
11 : 上・北

この値は、enumのDirection#ordinalと対応します。この順番にしたのは、テンキーと方向キーをマッピングすると、(n / 2 - 1)とできるためです。West('西'), North('北'), East('東'), South('南') にしてDirection.values[keyCode - 37]とするのもOK。
後者は定数の実装に依存したロジックなのでやめました。あとはお好みで。


この2ビットを1octet=8ビットに格納すると、4回分が保存できることになります。

履歴
   1   0   0   1   0   1   1   1
   ~~~~~   ~~~~~   ~~~~~   ~~~~~
   3回前   2回前   1回前   今回

10010111=東西西北


ゲーム自体のロジックは、入力→updateHistory→checkHistoryだけです。
updateHistoryでは、以下のようなビット演算により履歴の更新を行っています。

                                     10010111 (=東西西北)
SHIFT<<2
---------------------------------------------
          00000000 00000000 00000010 01011100
AND       00000000 00000000 00000000 11111100
---------------------------------------------
          00000000 00000000 00000000 01011100
OR        00000000 00000000 00000000 00000011
---------------------------------------------
          00000000 00000000 00000000 01011111 (=西西北北)

あとは、historyとtargetが同じかどうかをチェックすれば良いわけです。

*1:制限事項として注意しなければならないのが、この森に入るときに脱出ルートと同じパターンで移動してきた場合。"北北北北"で脱出する迷いの森に、"北北北北"で入ってこれないようにしなければならない。また、デフォルトが"南"なので、脱出パターンが"南南南南"は当然避けるべき。後者はこのゲームに限った話。