はじめに
これは、入門書などの無味乾燥な例題では納得して理解できない初学者のためのプログラミングチュートリアルである。
この記事では、プログラミングはプログラムを書くことによって学ぶという視点に立ち実践を重視する。楽器の演奏や車の運転のように本を読んだだけで出来るようにはならない。ここではオセロとカードゲームUNOの実装を通してプログラミングの重要な概念を伝える。オセロでは値と型、メソッドについて。UNOではオブジェクト指向を取り扱う。
注)後半「ウノ実装」は現在編集中
オセロ実装
1つ目の題材はオセロである。オセロはゲームの状態やプレイヤーの操作が少ないが、ひっくり返すという操作は複雑であり練習にちょうど良い。
オセロをプログラムするためには何が必要になるだろうか。十分な考察ののち、プログラマはその答えをネットの海に求める。検索の技術はプログラマにとって最も重要な技術の一つである。既存の技術を活用し時間や集中力といった資源をより重要な問題に充てることができるからである。検索の技術とは、検索によって得られた情報の中から必要なものを抽出し活用する技術のことである。
では最初の実践である。
実践1 オセロゲームをプログラムする。ただし、次の要求を満たすこと。
- 日本語Wikipedia(https://ja.wikipedia.org/wiki/オセロ_(ボードゲーム)#ルール ) に記載された基本ルールに基づくこと。
- オセロ板の状態が表示される。
- コマを置ける。
- 修了条件を満たしたとき、結果を表示して終了する。
あえて難易度の高い問題を最初に出した。できる範囲で実践1に取り組むことで、以降の内容に説得力を持たせる狙いがある。ぜひ取り組んでほしい。
状態を表現する
課題にあたるとき、最初の一歩は問題の整理である。十分な考察がなければどんな情報も混乱のもとになる。
オセロをプレイするとは何か。プログラムを組むためにはそのすべてを理解していることが求められる。まずオセロのボードとコマが必要になる。そしてゲームをプレイするためには二人のプレイヤーが必要である。
ボードとコマとプレイヤーがあればオセロができる。次の問題は、「プログラムの世界と現実の世界をどう結び付けるか」だ。プログラムで扱えるものは数値のみである。しかしボードやプレイヤーは数値じゃない。ここに飛躍がある。プログラムでオセロを作るのではない、オセロと見なせるプログラムを作るのである。ボードもコマもプレイヤーも必要ない、ボードに見えるもの、コマに見えるもの等々があればよい。つまり、数値でもってボード等を表現すればよいということだ。
ボードとコマとプレイヤーを表現するためにはどうするべきだろうか。ボードには64のマスが存在しコマには裏と表が存在する。プレイヤーは白か黒の番を持ちボードにコマを置く。次は実践だ。
実践2 ボードとコマを表現する。ただし、次の要求を満たすこと。
- 64マスの状態を保存できる。
- マスには「何も置いていない」、「白が置かれている」、「黒が置かれている」を区別できる情報を持たせる。
- 基本ルールに従って中央に4つのコマを配置する。
- ボードの状態を確認できる。
方法は無数に存在するが、ここでは2次元配列を用いる。下のプログラムでは1と2でコマの白黒を表現した。
回答例
class Othello {
public static void main(String[] args) {
int [][] borad = new int[8][8];
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
borad[i][j] = 0;
}
}
borad[4][4] = 1;
borad[4-1][4] = 2;
borad[4][4-1] = 2;
borad[4-1][4-1] = 1;
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
System.out.print(borad[i][j] + " ");
}
System.out.print("\n");
}
}
}
このプログラムの実行結果とみなす。次はプレイヤーを表現する。
状態を変化させる
プレイヤーの要件を確認する。白か黒のコマを置くことである。ただし、置くマスには「ひっくり返せるか」という条件が付く。コマを置いたときひっくり返せないマスにはコマを置けない。
条件に従って動作を変える仕組みは制御構文と呼ばれる。ここでの条件は「コマを置いたときひっくり返せるコマが存在する」かどうかである。
動作を実現する手段は何だろうか。プレイヤーの操作をユーザーが行う場合は、プログラムがユーザーの入力を受け付ける必要がある。そして入力に従ってプログラムを動作させる。「右からx番目、左からy番目のマスに黒のコマを置け」といった入力を受け、プログラムはそのマスに黒のコマを置けるか確認し、置けるときはボードの情報を更新し、置けないときはそれをユーザーに伝える。
入力を受け付けるとき、多くの場合は事前に用意された機能に頼ることになる。JavaではScannerクラスを用いる。入力の詳細に関しては入門の範囲を超えるためここでは扱わない。
コマを置くという動作を整理すると複数の要素からできていることがわかる。
- 入力を受け取る。
- コマを置けるか確認する。
- コマを置き、ひっくり返す。
複雑な問題は分割すると把握が容易になる。ここでは上のように3つの要素ごとに問題を分けて考える。要素の上から順に実践する。
実践3 コマを置くマスの座標を入力として受け取る。
入力を受け付けるためにScannerクラスを用いている。Scannerクラスの用い方は公式のドキュメント(https://docs.oracle.com/javase/jp/8/docs/api/java/util/Scanner.html)を参照する。
回答例
import java.util.Scanner;
class Othello {
public static void main(String[] args) {
int [][] borad = new int[8][8];
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
borad[i][j] = 0;
}
}
borad[4][4] = 1;
borad[4-1][4] = 2;
borad[4][4-1] = 2;
borad[4-1][4-1] = 1;
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
System.out.print(borad[i][j] + " ");
}
System.out.print("\n");
}
// Scannerクラスのインスタンスを作成
// 引数で標準入力System.inを指定する
Scanner scanner = new Scanner(System.in);
// 入力を促すメッセージ
System.out.print("x y の順で入力してください > ");
int x = scanner.nextInt();
int y = scanner.nextInt();
//入力された内容を画面に表示
System.out.println("x=" + x + " y="+y);
// Scannerクラスのインスタンスをクローズ
scanner.close();
}
}
実践4 コマを置けるか確認する。ただし、置くコマは黒(2)とする。
回答例
import java.util.Scanner;
class Othello {
public static void main(String[] args) {
int [][] borad = new int[8][8];
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
borad[i][j] = 0;
}
}
borad[4][4] = 1;
borad[4-1][4] = 2;
borad[4][4-1] = 2;
borad[4-1][4-1] = 1;
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
System.out.print(borad[i][j] + " ");
}
System.out.print("\n");
}
// Scannerクラスのインスタンスを作成
// 引数で標準入力System.inを指定する
Scanner scanner = new Scanner(System.in);
// 入力を促すメッセージ
System.out.print("x y の順で入力してください > ");
int x = scanner.nextInt();
int y = scanner.nextInt();
//入力された内容を画面に表示
System.out.println("x=" + x + " y="+y);
//範囲チェック
if (0<x&&x<8 && 0<y&&y<8) {
//マスが開いているか
if (borad[y][x] == 0) {
//八方向のリスト
int[] dx = {-1, 0, 1, 1, 1, 0, -1, -1};
int[] dy = {-1, -1, -1, 0, 1, 1, 1, 0};
//八方向を順に試す
for (int i=0; i<8; i++) {
//置くマスの隣を(nx, ny)とする
int nx = x+dx[i];
int ny = y+dy[i];
//範囲チェック
if (0<nx&&nx<8 && 0<ny&&ny<8) {
//隣に白のコマがあるかどうか
if (borad[ny][nx] == 1) {
//黒のコマを見ている方向で探す
while (0<nx+dx[i]&&nx+dx[i]<8 && 0<ny+dy[i]&&ny+dy[i]<8) {
if (borad[ny][nx] == 2) {
//OK
System.out.println("OK");
System.out.println("dx=" + dx[i] + " dy="+dy[i]);
}
nx += dx[i];
ny += dy[i];
}
}
}
}
}
}
// Scannerクラスのインスタンスをクローズ
scanner.close();
}
}
実践5 コマを置き、ひっくり返す。
回答例
import java.util.Scanner;
class Othello {
public static void main(String[] args) {
int [][] borad = new int[8][8];
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
borad[i][j] = 0;
}
}
borad[4][4] = 1;
borad[4-1][4] = 2;
borad[4][4-1] = 2;
borad[4-1][4-1] = 1;
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
System.out.print(borad[i][j] + " ");
}
System.out.print("\n");
}
// Scannerクラスのインスタンスを作成
// 引数で標準入力System.inを指定する
Scanner scanner = new Scanner(System.in);
// 入力を促すメッセージ
System.out.print("x y の順で入力してください > ");
int x = scanner.nextInt();
int y = scanner.nextInt();
//入力された内容を画面に表示
System.out.println("x=" + x + " y="+y);
//範囲チェック
if (0<x&&x<8 && 0<y&&y<8) {
//マスが開いているか
if (borad[y][x] == 0) {
//八方向のリスト
int[] dx = {-1, 0, 1, 1, 1, 0, -1, -1};
int[] dy = {-1, -1, -1, 0, 1, 1, 1, 0};
//八方向を順に試す
for (int i=0; i<8; i++) {
//置くマスの隣を(nx, ny)とする
int nx = x+dx[i];
int ny = y+dy[i];
//範囲チェック
if (0<nx&&nx<8 && 0<ny&&ny<8) {
//隣に白のコマがあるかどうか
if (borad[ny][nx] == 1) {
//黒のコマを見ている方向で探す
while (0<nx+dx[i]&&nx+dx[i]<8 && 0<ny+dy[i]&&ny+dy[i]<8) {
if (borad[ny][nx] == 2) {
//OK
System.out.println("OK");
System.out.println("dx=" + dx[i] + " dy="+dy[i]);
while (x!=nx || y!=ny) {
borad[y][x] = 2;
x += dx[i];
y += dy[i];
}
}
nx += dx[i];
ny += dy[i];
}
}
}
}
}
}
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
System.out.print(borad[i][j] + " ");
}
System.out.print("\n");
}
// Scannerクラスのインスタンスをクローズ
scanner.close();
}
}
このように分岐やループが重なり構造が複雑なコードを美しくないという。
プログラムを分ける
操作としては簡単なはずだが、それを表現するコードは複雑になった。この操作を繰り返すことでオセロをプレイできるが面倒だし間違いのもとである。同じ処理をまとめ、名前を付ける機能がJava(とその他たいていの言語)に備わっている。
同じ処理をまとめることで繰り返しが書きやすくなる。Javaではメソッドと呼ばれ、ほかに関数やサブルーチンといった呼ばれ方もする。メソッドを用いてオセロの完成を目指す。コマを交互に置き、置けなくなったら勝敗を表示するということだ。
実践6 オセロの完成。ただし白と黒が交互に入力し、入力の方法は自由とする。
回答例
import java.util.Scanner;
import sun.launcher.resources.launcher;
class Othello {
static boolean turn(int player, int x, int y, int[][] borad, boolean check) {
int opponent = 1;
if (player == 1) {
opponent = 2;
}
boolean res = false;
//範囲チェック
if (0<x&&x<8 && 0<y&&y<8) {
//マスが開いているか
if (borad[y][x] == 0) {
//八方向のリスト
int[] dx = {-1, 0, 1, 1, 1, 0, -1, -1};
int[] dy = {-1, -1, -1, 0, 1, 1, 1, 0};
//八方向を順に試す
for (int i=0; i<8; i++) {
//置くマスの隣を(nx, ny)とする
int nx = x+dx[i];
int ny = y+dy[i];
//範囲チェック
if (0<nx&&nx<8 && 0<ny&&ny<8) {
//隣に白のコマがあるかどうか
if (borad[ny][nx] == opponent) {
//黒のコマを見ている方向で探す
while (0<nx+dx[i]&&nx+dx[i]<8 && 0<ny+dy[i]&&ny+dy[i]<8) {
if (borad[ny][nx] == player) {
//OK
res = true;
if (!check) {
System.out.println("OK");
System.out.println("dx=" + dx[i] + " dy="+dy[i]);
while (x!=nx || y!=ny) {
borad[y][x] = player;
x += dx[i];
y += dy[i];
}
}
}
nx += dx[i];
ny += dy[i];
}
}
}
}
}
}
return res;
}
static void print_borad(int[][] borad) {
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
System.out.print(borad[i][j] + " ");
}
System.out.print("\n");
}
}
static boolean can_put(int[][] borad, int player) {
boolean res = true;
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
res &= turn(player, i, j, borad, true);
}
}
return res;
}
static int[][] init() {
int [][] borad = new int[8][8];
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
borad[i][j] = 0;
}
}
borad[4][4] = 1;
borad[4-1][4] = 2;
borad[4][4-1] = 2;
borad[4-1][4-1] = 1;
print_borad(borad);
return borad;
}
public static void main(String[] args) {
int [][] borad = init();
int player = 2;
boolean is_game_end = false;
// Scannerクラスのインスタンスを作成
// 引数で標準入力System.inを指定する
Scanner scanner = new Scanner(System.in);
int x, y;
while (!is_game_end) {
// 入力を促すメッセージ
System.out.println("Player = " + player);
System.out.print("x y の順で入力してください > ");
x = scanner.nextInt();
y = scanner.nextInt();
//入力された内容を画面に表示
System.out.println("x=" + x + " y="+y);
if (turn(player, x, y, borad, false)) {
if(player == 2) player = 1;
else player = 2;
is_game_end = can_put(borad, player);
print_borad(borad);
}
}
// Scannerクラスのインスタンスをクローズ
scanner.close();
int black = 0, white = 0;
for (int i=0; i<8; i++) {
for (int j=0; j<8; j++) {
if (borad[i][j] == 1) {
white++;
} else if (borad[i][j] == 2) {
black++;
}
}
}
if (black>white) {
System.out.println("Blackの勝ち");
} else if(black<white) {
System.out.println("Whiteの勝ち");
} else {
System.out.println("引き分け");
}
}
}
ウノ実装
ただいま編集中