From e6f68e0b9e7f97f0a7f1c9c98c5a3839c843a100 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 13 Mar 2018 01:49:54 +0900
Subject: [PATCH] =?UTF-8?q?=E3=82=AA=E3=82=BB=E3=83=AD=E3=81=A7=E9=BB=92?=
 =?UTF-8?q?=E7=99=BD=E3=82=92=E7=9C=9F=E7=90=86=E5=80=A4=E3=81=A7=E8=A1=A8?=
 =?UTF-8?q?=E7=8F=BE=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/models/othello-game.ts                |   6 +-
 src/api/stream/othello-game.ts                |  12 +-
 src/common/othello/core.ts                    | 115 ++++++++++++------
 .../common/views/components/othello.game.vue  |  22 ++--
 4 files changed, 98 insertions(+), 57 deletions(-)

diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index 788fb5cba6..01c6ca6c00 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -25,7 +25,11 @@ export interface IGame {
 	is_started: boolean;
 	is_ended: boolean;
 	winner_id: mongo.ObjectID;
-	logs: any[];
+	logs: Array<{
+		at: Date;
+		color: boolean;
+		pos: number;
+	}>;
 	settings: {
 		map: string[];
 		bw: string | number;
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 87d26e2419..368baa2cb6 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -214,9 +214,9 @@ export default function(request: websocket.request, connection: websocket.connec
 
 				if (o.isEnded) {
 					let winner;
-					if (o.winner == 'black') {
+					if (o.winner === true) {
 						winner = freshGame.black == 1 ? freshGame.user1_id : freshGame.user2_id;
-					} else if (o.winner == 'white') {
+					} else if (o.winner === false) {
 						winner = freshGame.black == 1 ? freshGame.user2_id : freshGame.user1_id;
 					} else {
 						winner = null;
@@ -263,17 +263,17 @@ export default function(request: websocket.request, connection: websocket.connec
 
 		const myColor =
 			(game.user1_id.equals(user._id) && game.black == 1) || (game.user2_id.equals(user._id) && game.black == 2)
-				? 'black'
-				: 'white';
+				? true
+				: false;
 
 		if (!o.canPut(myColor, pos)) return;
 		o.put(myColor, pos);
 
 		let winner;
 		if (o.isEnded) {
-			if (o.winner == 'black') {
+			if (o.winner === true) {
 				winner = game.black == 1 ? game.user1_id : game.user2_id;
-			} else if (o.winner == 'white') {
+			} else if (o.winner === false) {
 				winner = game.black == 1 ? game.user2_id : game.user1_id;
 			} else {
 				winner = null;
diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts
index 39ceb921a9..54201d6541 100644
--- a/src/common/othello/core.ts
+++ b/src/common/othello/core.ts
@@ -1,4 +1,11 @@
-export type Color = 'black' | 'white';
+/**
+ * true ... 黒
+ * false ... 白
+ */
+export type Color = boolean;
+const BLACK = true;
+const WHITE = false;
+
 export type MapPixel = 'null' | 'empty';
 
 export type Options = {
@@ -7,6 +14,23 @@ export type Options = {
 	loopedBoard: boolean;
 };
 
+export type Undo = {
+	/**
+	 * 色
+	 */
+	color: Color,
+
+	/**
+	 * どこに打ったか
+	 */
+	pos: number;
+
+	/**
+	 * 反転した石の位置の配列
+	 */
+	effects: number[];
+};
+
 /**
  * オセロエンジン
  */
@@ -15,19 +39,20 @@ export default class Othello {
 	public mapWidth: number;
 	public mapHeight: number;
 	public board: Color[];
-	public turn: Color = 'black';
+	public turn: Color = BLACK;
 	public opts: Options;
 
 	public prevPos = -1;
-	public stats: Array<{
-		b: number;
-		w: number;
-	}>;
+	public prevColor: Color = null;
 
 	/**
 	 * ゲームを初期化します
 	 */
 	constructor(map: string[], opts: Options) {
+		//#region binds
+		this.put = this.put.bind(this);
+		//#endregion
+
 		//#region Options
 		this.opts = opts;
 		if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
@@ -42,8 +67,8 @@ export default class Othello {
 
 		this.board = mapData.split('').map(d => {
 			if (d == '-') return null;
-			if (d == 'b') return 'black';
-			if (d == 'w') return 'white';
+			if (d == 'b') return BLACK;
+			if (d == 'w') return WHITE;
 			return undefined;
 		});
 
@@ -53,18 +78,12 @@ export default class Othello {
 		});
 		//#endregion
 
-		// Init stats
-		this.stats = [{
-			b: this.blackP,
-			w: this.whiteP
-		}];
-
 		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
-		if (this.canPutSomewhere('black').length == 0) {
-			if (this.canPutSomewhere('white').length == 0) {
+		if (this.canPutSomewhere(BLACK).length == 0) {
+			if (this.canPutSomewhere(WHITE).length == 0) {
 				this.turn = null;
 			} else {
-				this.turn = 'white';
+				this.turn = WHITE;
 			}
 		}
 	}
@@ -73,14 +92,14 @@ export default class Othello {
 	 * 黒石の数
 	 */
 	public get blackCount() {
-		return this.board.filter(x => x == 'black').length;
+		return this.board.filter(x => x === BLACK).length;
 	}
 
 	/**
 	 * 白石の数
 	 */
 	public get whiteCount() {
-		return this.board.filter(x => x == 'white').length;
+		return this.board.filter(x => x === WHITE).length;
 	}
 
 	/**
@@ -123,36 +142,53 @@ export default class Othello {
 	 * @param color 石の色
 	 * @param pos 位置
 	 */
-	public put(color: Color, pos: number) {
-		if (!this.canPut(color, pos)) return;
+	public put(color: Color, pos: number, fast = false): Undo {
+		if (!fast && !this.canPut(color, pos)) return null;
 
 		this.prevPos = pos;
+		this.prevColor = color;
 		this.write(color, pos);
 
 		// 反転させられる石を取得
-		const reverses = this.effects(color, pos);
+		const effects = this.effects(color, pos);
 
 		// 反転させる
-		reverses.forEach(pos => {
+		effects.forEach(pos => {
 			this.write(color, pos);
 		});
 
-		this.stats.push({
-			b: this.blackP,
-			w: this.whiteP
-		});
+		this.calcTurn();
 
+		return {
+			color,
+			pos,
+			effects
+		};
+	}
+
+	private calcTurn() {
 		// ターン計算
-		const opColor = color == 'black' ? 'white' : 'black';
+		const opColor = this.prevColor === BLACK ? WHITE : BLACK;
 		if (this.canPutSomewhere(opColor).length > 0) {
-			this.turn = color == 'black' ? 'white' : 'black';
-		} else if (this.canPutSomewhere(color).length > 0) {
-			this.turn = color == 'black' ? 'black' : 'white';
+			this.turn = this.prevColor === BLACK ? WHITE : BLACK;
+		} else if (this.canPutSomewhere(this.prevColor).length > 0) {
+			this.turn = this.prevColor === BLACK ? BLACK : WHITE;
 		} else {
 			this.turn = null;
 		}
 	}
 
+	public undo(undo: Undo) {
+		this.prevColor = undo.color;
+		this.prevPos = undo.pos;
+		this.write(null, undo.pos);
+		for (const pos of undo.effects) {
+			const color = this.board[pos];
+			this.write(!color, pos);
+		}
+		this.calcTurn();
+	}
+
 	/**
 	 * 指定したマスの状態を取得します
 	 * @param pos 位置
@@ -207,8 +243,8 @@ export default class Othello {
 	 * @param color 自分の色
 	 * @param pos 位置
 	 */
-	private effects(color: Color, pos: number): number[] {
-		const enemyColor = color == 'black' ? 'white' : 'black';
+	public effects(color: Color, pos: number): number[] {
+		const enemyColor = !color;
 
 		// ひっくり返せる石(の位置)リスト
 		let stones = [];
@@ -225,7 +261,7 @@ export default class Othello {
 
 				// 座標が指し示す位置がボード外に出たとき
 				if (this.opts.loopedBoard) {
-					if (x <  0             ) x = this.mapWidth - ((-x) % this.mapWidth);
+					if (x <  0             ) x = this.mapWidth  - ((-x) % this.mapWidth);
 					if (y <  0             ) y = this.mapHeight - ((-y) % this.mapHeight);
 					if (x >= this.mapWidth ) x = x % this.mapWidth;
 					if (y >= this.mapHeight) y = y % this.mapHeight;
@@ -259,16 +295,17 @@ export default class Othello {
 				const stone = this.get(pos);
 
 				// 石が置かれていないマスなら走査終了
-				if (stone == null) break;
+				if (stone === null) break;
 
 				// 相手の石なら「ひっくり返せるかもリスト」に入れておく
-				if (stone == enemyColor) found.push(pos);
+				if (stone === enemyColor) found.push(pos);
 
 				// 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了
-				if (stone == color) {
+				if (stone === color) {
 					stones = stones.concat(found);
 					break;
 				}
+
 				i++;
 			}
 		};
@@ -303,9 +340,9 @@ export default class Othello {
 		if (this.blackCount == this.whiteCount) return null;
 
 		if (this.opts.isLlotheo) {
-			return this.blackCount > this.whiteCount ? 'white' : 'black';
+			return this.blackCount > this.whiteCount ? WHITE : BLACK;
 		} else {
-			return this.blackCount > this.whiteCount ? 'black' : 'white';
+			return this.blackCount > this.whiteCount ? BLACK : WHITE;
 		}
 	}
 }
diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue
index 01148f193e..74139f5754 100644
--- a/src/web/app/common/views/components/othello.game.vue
+++ b/src/web/app/common/views/components/othello.game.vue
@@ -60,13 +60,13 @@ export default Vue.extend({
 		},
 		myColor(): Color {
 			if (!this.iAmPlayer) return null;
-			if (this.game.user1_id == (this as any).os.i.id && this.game.black == 1) return 'black';
-			if (this.game.user2_id == (this as any).os.i.id && this.game.black == 2) return 'black';
-			return 'white';
+			if (this.game.user1_id == (this as any).os.i.id && this.game.black == 1) return true;
+			if (this.game.user2_id == (this as any).os.i.id && this.game.black == 2) return true;
+			return false;
 		},
 		opColor(): Color {
 			if (!this.iAmPlayer) return null;
-			return this.myColor == 'black' ? 'white' : 'black';
+			return this.myColor === true ? false : true;
 		},
 		blackUser(): any {
 			return this.game.black == 1 ? this.game.user1 : this.game.user2;
@@ -75,9 +75,9 @@ export default Vue.extend({
 			return this.game.black == 1 ? this.game.user2 : this.game.user1;
 		},
 		turnUser(): any {
-			if (this.o.turn == 'black') {
+			if (this.o.turn === true) {
 				return this.game.black == 1 ? this.game.user1 : this.game.user2;
-			} else if (this.o.turn == 'white') {
+			} else if (this.o.turn === false) {
 				return this.game.black == 1 ? this.game.user2 : this.game.user1;
 			} else {
 				return null;
@@ -99,7 +99,7 @@ export default Vue.extend({
 			});
 			this.logs.forEach((log, i) => {
 				if (i < v) {
-					this.o.put(log.color, log.pos);
+					this.o.put(log.color, log.pos, true);
 				}
 			});
 			this.$forceUpdate();
@@ -116,7 +116,7 @@ export default Vue.extend({
 		});
 
 		this.game.logs.forEach(log => {
-			this.o.put(log.color, log.pos);
+			this.o.put(log.color, log.pos, true);
 		});
 
 		this.logs = this.game.logs;
@@ -190,10 +190,10 @@ export default Vue.extend({
 		checkEnd() {
 			this.game.is_ended = this.o.isEnded;
 			if (this.game.is_ended) {
-				if (this.o.winner == 'black') {
+				if (this.o.winner === true) {
 					this.game.winner_id = this.game.black == 1 ? this.game.user1_id : this.game.user2_id;
 					this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
-				} else if (this.o.winner == 'white') {
+				} else if (this.o.winner === false) {
 					this.game.winner_id = this.game.black == 1 ? this.game.user2_id : this.game.user1_id;
 					this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
 				} else {
@@ -214,7 +214,7 @@ export default Vue.extend({
 			});
 
 			this.game.logs.forEach(log => {
-				this.o.put(log.color, log.pos);
+				this.o.put(log.color, log.pos, true);
 			});
 
 			this.logs = this.game.logs;