2010年2月19日金曜日

[習作][GAE] GAEでオセロ対戦アプリ(になったらいいな) (1)

時代に乗り遅れることはなはだしいのだけれど、最近初めてGAE/Jをさわっている。
Google Plugin for Eclipseをインストール、スタートガイドをさらったところで、フルスクラッチで小さなアプリを書いてみようと思う。題材は、よくあるオセロ対戦アプリ。誰の役にも立たない覚書きになりそうだ...。

・永続化層は(とりあえず)、公式サイト推奨のJDO。
・フロントエンドをREST(JAX-RS)にしておいてクライアントは後で考える。HTML5ページとか、Googleガジェットとか、モバイルアプリとか。
・ユーザ認証は面倒なので外部サービス(Google認証など)を想定して自前ではもたない。最初のアクセスで認証サービスにとんで、認証が通ったらあとはセッションにユーザ情報を入れとく(?)。
・対戦相手の選択は要検討
・対戦履歴とか後で見れたらいいかな
・CPU対戦できたらいいかな


最初にコアとなるロジック部分を実装。永続化のことはなにも考えず、コンソールから実行するJavaアプリとして作成した。
クラスは
・盤面を表現する Board クラス
・ゲームを表現する Match クラス (プレイヤー情報やBoardクラスへの参照を保持する)
の2つ。Mainメソッドは Match クラスに放りこむ。

これを(何の変哲もない)POJOで実装し、後で JDO のアノテーションを付与したものが以下。基本的に、永続化したいクラスに @PersistenceCapable アノテーション、永続化対象のフィールドに @Persistent アノテーションをつけるだけ。
つまった点があるとすれば、盤面の状態を Board の boardフィールド ( int の2次元配列 ) で表しているのだけれど、配列がデータストアでサポートされていなかった。そのため Board クラスは @PersistenceCapable をつけて永続化することができない。幸い、Board クラスは Match クラスに従属していて、それ自体を単体で操作したり検索したりすることがないため、Match クラスのBlobフィールドとすればOKだった。Serializable インタフェースを implements するだけでいいらしい。

データストアでサポートされている型やシリアライズについては ここ

// Board.java
// 「盤面」を表現したクラス

package othello.core;

import java.io.Serializable;

public class Board implements Serializable {
private static final long serialVersionUID = 1L;

public static final int BLANK = 0;
public static final int BLACK = 1;
public static final int WHITE = 2;
public static final int WALL = -1;
public static final int BOARD_SIZE = 8;
public static final String[] COLOR_DISP = new String[]{ "空", "黒", "白", "壁" };

private int[][] board; // 盤面

public Board() {
board = new int[BOARD_SIZE+2][BOARD_SIZE+2];

for (int i = 0; i < BOARD_SIZE+2; i++) board[0][i] = WALL;
for (int i = 0; i < BOARD_SIZE+2; i++) board[BOARD_SIZE+1][i] = WALL;
for (int i = 0; i < BOARD_SIZE+2; i++) board[i][0] = WALL;
for (int i = 0; i < BOARD_SIZE+2; i++) board[i][BOARD_SIZE+1] = WALL;
board[4][4] = WHITE;
board[4][5] = BLACK;
board[5][4] = BLACK;
board[5][5] = WHITE;

}

public Board(int[][] board) {
this.board = board;
}

public int[][] getBoard() {
return board;
}

public void setCell(int row, int col, int color) {
if (row < 0 || row > BOARD_SIZE+1) throw new IllegalArgumentException();
if (col < 0 || col > BOARD_SIZE+1) throw new IllegalArgumentException();
if (color < BLANK || color > WHITE) throw new IllegalArgumentException();
if (board[row][col] > WALL) {
board[row][col] = color;
}
}

public int getCell(int row, int col) {
if (row < 0 || row > BOARD_SIZE+1) throw new IllegalArgumentException();
if (col < 0 || col > BOARD_SIZE+1) throw new IllegalArgumentException();
return board[row][col];
}

public boolean isFilled() {
for (int i = 1; i < BOARD_SIZE+2; i++) {
for (int j = 1; j < BOARD_SIZE+2; j++) {
if (board[i][j] == BLANK) return false;
}
}
return true;
}

public boolean canPut(int row, int col, int color) {
if (row < 0 || row > BOARD_SIZE+1) throw new IllegalArgumentException();
if (col < 0 || col > BOARD_SIZE+1) throw new IllegalArgumentException();
if (board[row][col] == WALL || board[row][col] != BLANK) return false;
return checkNorth(row, col, color)
|| checkSouth(row, col, color)
|| checkEast(row, col, color)
|| checkWest(row, col, color)
|| checkNorthEast(row, col, color)
|| checkSouthEast(row, col, color)
|| checkNorthWest(row, col, color)
|| checkSouthWest(row, col, color);
}

private boolean checkNorth(int row, int col, int color) {
int next = board[row-1][col];
if (next == WALL || next == BLANK || next == color) return false;
int r = row - 2;
while(board[r][col] != WALL && board[r][col] != BLANK) {
if(board[r][col] == color) return true;
r -= 1;
}
return false;
}

private boolean checkSouth(int row, int col, int color) {
int next = board[row+1][col];
if (next == WALL || next == BLANK || next == color) return false;
int r = row + 2;
while(board[r][col] != WALL && board[r][col] != BLANK) {
if(board[r][col] == color) return true;
r += 1;
}
return false;
}

private boolean checkEast(int row, int col, int color) {
int next = board[row][col+1];
if (next == WALL || next == BLANK || next == color) return false;
int c = col + 2;
while(board[row][c] != WALL && board[row][c] != BLANK) {
if(board[row][c] == color) return true;
c += 1;
}
return false;
}

private boolean checkWest(int row, int col, int color) {
int next = board[row][col-1];
if (next == WALL || next == BLANK || next == color) return false;
int c = col - 2;
while(board[row][c] != WALL && board[row][c] != BLANK) {
if(board[row][c] == color) return true;
c -= 1;
}
return false;
}

private boolean checkNorthEast(int row, int col, int color) {
int next = board[row-1][col+1];
if (next == WALL || next == BLANK || next == color) return false;
int r = row - 2; int c = col + 2;
while(board[r][c] != WALL && board[r][c] != BLANK) {
if(board[r][c] == color) return true;
r -= 1; c += 1;
}
return false;
}

private boolean checkSouthEast(int row, int col, int color) {
int next = board[row+1][col+1];
if (next == WALL || next == BLANK || next == color) return false;
int r = row + 2; int c = col + 2;
while(board[r][c] != WALL && board[r][c] != BLANK) {
if(board[r][c] == color) return true;
r += 1; c += 1;
}
return false;
}

private boolean checkNorthWest(int row, int col, int color) {
int next = board[row-1][col-1];
if (next == WALL || next == BLANK || next == color) return false;
int r = row - 2; int c = col - 2;
while(board[r][c] != WALL && board[r][c] != BLANK) {
if(board[r][c] == color) return true;
r -= 1; c -= 1;
}
return false;
}

private boolean checkSouthWest(int row, int col, int color) {
int next = board[row+1][col-1];
if (next == WALL || next == BLANK || next == color) return false;
int r = row + 2; int c = col - 2;
while(board[r][c] != WALL && board[r][c] != BLANK) {
if(board[r][c] == color) return true;
r += 1; c -= 1;
}
return false;
}

public void put(int row, int col, int color) {
setCell(row, col, color);
if(checkNorth(row, col, color)) reverseNorth(row, col, color);
if(checkSouth(row, col, color)) reverseSouth(row, col, color);
if(checkEast(row, col, color)) reverseEast(row, col, color);
if(checkWest(row, col, color)) reverseWest(row, col, color);
if(checkNorthEast(row, col, color)) reverseNorthEast(row, col, color);
if(checkSouthEast(row, col, color)) reverseSouthEast(row, col, color);
if(checkNorthWest(row, col, color)) reverseNorthWest(row, col, color);
if(checkSouthWest(row, col, color)) reverseSouthWest(row, col, color);
}

private void reverseNorth(int row, int col, int color) {
int r = row - 1;
while(board[r][col] != color) {
setCell(r, col, color);
r -= 1;
}
}

private void reverseSouth(int row, int col, int color) {
int r = row + 1;
while(board[r][col] != color) {
setCell(r, col, color);
r += 1;
}
}

private void reverseEast(int row, int col, int color) {
int c = col + 1;
while(board[row][c] != color) {
setCell(row, c, color);
c += 1;
}
}

private void reverseWest(int row, int col, int color) {
int c = col - 1;
while(board[row][c] != color) {
setCell(row, c, color);
c -= 1;
}
}

private void reverseNorthEast(int row, int col, int color) {
int r = row - 1; int c = col + 1;
while(board[r][c] != color) {
setCell(r, c, color);
r -= 1; c += 1;
}
}

private void reverseSouthEast(int row, int col, int color) {
int r = row + 1; int c = col + 1;
while(board[r][c] != color) {
setCell(r, c, color);
r += 1; c += 1;
}
}

private void reverseNorthWest(int row, int col, int color) {
int r = row - 1; int c = col - 1;
while(board[r][c] != color) {
setCell(r, c, color);
r -= 1; c -= 1;
}
}

private void reverseSouthWest(int row, int col, int color) {
int r = row + 1; int c = col - 1;
while(board[r][c] != color) {
setCell(r, c, color);
r += 1; c -= 1;
}
}

public int getCount(int color) {
if (color < BLANK || color > WHITE) throw new IllegalArgumentException();
int count = 0;
for (int i = 1; i < BOARD_SIZE+1; i++) {
for (int j = 1; j < BOARD_SIZE+1; j++) {
if (board[i][j] == color) count += 1;
}
}
return count;
}

public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < BOARD_SIZE+2; i++) {
for (int j = 0; j < BOARD_SIZE+2; j++) {
switch(board[i][j]) {
case BLANK : sb.append("-"); break;
case BLACK : sb.append("●"); break;
case WHITE : sb.append("○"); break;
case WALL : sb.append("+"); break;
}
sb.append("\n");
}
}
return sb.toString();
}

}



// Match.java
// ゲームをモデリングしたクラス。アノテーションをはずして java othello.Match で実行可。

package othello.core;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Match {

public static final String DRAW = "draw";

@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;

@Persistent
private String player1;

@Persistent
private String player2;

@Persistent
private Boolean cancelled;

@Persistent
private String nextPlayer;

@Persistent
private Integer nextColor;

@Persistent
private Integer turn;

@Persistent
private String winner;

// Board オブジェクトはblobとしてもつ
@Persistent(serialized = "true")
private Board board;

public Match(String player1, String player2) {
this.player1 = player1;
this.player2 = player2;
this.cancelled = false;
this.nextPlayer = player1;
this.nextColor = Board.BLACK;
this.turn = 1;
this.board = new Board();
}

public boolean isCancelled() {
return cancelled;
}

public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}

public Long getId() {
return id;
}

public String getPlayer1() {
return player1;
}

public String getPlayer2() {
return player2;
}

public Board getBoard() {
return board;
}

public void setBoard(Board board) {
this.board = board;
}

public String getNextPlayer() {
return nextPlayer;
}

public int getNextColor() {
return nextColor;
}

public int getTurn() {
return turn;
}

public String getWinner() {
return winner;
}

public static class Pos {
int row;
int col;
public Pos(int row, int col) {
this.row = row; this.col = col;
}

public int getRow() { return row; }
public int getCol() { return col; }
public String toString() { return "[" + row + "," + col + "]"; }
}

public List<Pos> getNextHands() {
return getHands(nextColor);
}

private List<Pos> getHands(int color) {
List<Pos> hands = new ArrayList<Pos>();
for (int i = 0; i < Board.BOARD_SIZE+2; i++) {
for (int j = 0; j < Board.BOARD_SIZE+2; j++) {
if (board.canPut(i, j, color))
hands.add(new Pos(i,j));
}
}
return hands;
}

public boolean isFinished() {
return board.isFilled()
|| (getHands(Board.BLACK).size() == 0 && getHands(Board.WHITE).size() == 0);
}

public void passTurn() {
switchTurn();
}

public boolean putNextHand(Pos pos) {
if (!board.canPut(pos.getRow(), pos.getCol(), nextColor)) return false;
board.put(pos.getRow(), pos.getCol(), nextColor);
if (isFinished()) {
int count1 = board.getCount(Board.BLACK);
int count2 = board.getCount(Board.WHITE);
if (count1 == count2) winner = DRAW;
else if (count1 > count2) winner = player1;
else winner = player2;
} else {
switchTurn();
turn += 1;
}
return true;
}

private void switchTurn() {
nextPlayer = (nextPlayer.equals(player1)) ? player2 : player1;
nextColor = (nextColor == Board.BLACK) ? Board.WHITE : Board.BLACK;
}

public static void main(String args[]) {
System.out.println("-*- match start -*-");
Match match = new Match("Alice", "Bob");
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while(!match.isFinished()) {
System.out.println(match.getBoard());
System.out.println("Turn " + match.getTurn() + ": " + Board.COLOR_DISP[match.nextColor]);
List<Pos> hands = match.getNextHands();
if (hands.size() == 0) {
System.out.println("No hands. Passed.");
match.passTurn();
continue;
} else {
for (Pos pos : hands) { System.out.print(pos + " "); }
System.out.println("\n");
try {
String line = reader.readLine();
String[] ary = line.split(",");
if (ary.length < 2) {
System.out.println("Please input (row, col)");
continue;
}
int row = Integer.parseInt(ary[0]);
int col = Integer.parseInt(ary[1]);
Pos pos = new Pos(row, col);
if (!match.putNextHand(pos)) {
System.out.println("Invalid Hand!");
continue;
}
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("黒: " + match.getBoard().getCount(Board.BLACK));
System.out.println("白: " + match.getBoard().getCount(Board.WHITE));
}
System.out.println("黒: " + match.getBoard().getCount(Board.BLACK));
System.out.println("白: " + match.getBoard().getCount(Board.WHITE));
if (match.winner == null) {
System.out.println("Some error occurred...");
} else if (match.winner.equals(Match.DRAW)) {
System.out.println("引き分け");
} else {
System.out.println("WINNER <" + match.winner + ">");
}
}
}



この程度の単純なクラスであれば、アノテーションをつけるだけで(当然だけれど面倒なDB設定などもなく)すんなり PersistenceManager でCRUDができるようになる。複数クラス間の関連は、一応サポートされているようだが制限が厳しそう(TODO 後で調べる)。
JDOの設定ファイル (jdoconfig.xml) はEclipseの Google Plugin が勝手に生成・配置してくれるので、なにかしらカスタマイズが必要なときはそれをいじればよいのだと思う。

継承とかどうなるのだろう。

0 件のコメント:

コメントを投稿