From 155012846d8f142515390adb2754b45cdfbb74ef Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 8 Mar 2018 17:57:57 +0900 Subject: [PATCH] #1200 --- src/api/endpoints/othello/games.ts | 9 +- src/api/endpoints/othello/match.ts | 20 +- src/api/models/othello-game.ts | 43 ++- src/api/stream/othello-game.ts | 142 ++++++-- src/common/othello.ts | 325 ------------------ src/common/othello/ai.ts | 42 +++ src/common/othello/core.ts | 239 +++++++++++++ src/common/othello/maps.ts | 217 ++++++++++++ .../common/views/components/othello.game.vue | 164 ++++----- .../views/components/othello.gameroom.vue | 42 +++ .../common/views/components/othello.room.vue | 152 ++++++++ .../app/common/views/components/othello.vue | 18 +- 12 files changed, 956 insertions(+), 457 deletions(-) delete mode 100644 src/common/othello.ts create mode 100644 src/common/othello/ai.ts create mode 100644 src/common/othello/core.ts create mode 100644 src/common/othello/maps.ts create mode 100644 src/web/app/common/views/components/othello.gameroom.vue create mode 100644 src/web/app/common/views/components/othello.room.vue diff --git a/src/api/endpoints/othello/games.ts b/src/api/endpoints/othello/games.ts index 39963fcd2..dd9fb5ef5 100644 --- a/src/api/endpoints/othello/games.ts +++ b/src/api/endpoints/othello/games.ts @@ -7,12 +7,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (myErr) return rej('invalid my param'); const q = my ? { + is_started: true, $or: [{ - black_user_id: user._id + user1_id: user._id }, { - white_user_id: user._id + user2_id: user._id }] - } : {}; + } : { + is_started: true + }; // Fetch games const games = await Game.find(q, { diff --git a/src/api/endpoints/othello/match.ts b/src/api/endpoints/othello/match.ts index cb094bbc6..05b87a541 100644 --- a/src/api/endpoints/othello/match.ts +++ b/src/api/endpoints/othello/match.ts @@ -3,6 +3,7 @@ import Matching, { pack as packMatching } from '../../models/othello-matching'; import Game, { pack as packGame } from '../../models/othello-game'; import User from '../../models/user'; import { publishOthelloStream } from '../../event'; +import { eighteight } from '../../../common/othello/maps'; module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'user_id' parameter @@ -26,16 +27,21 @@ module.exports = (params, user) => new Promise(async (res, rej) => { _id: exist._id }); - const parentIsBlack = Math.random() > 0.5; - - // Start game + // Create game const game = await Game.insert({ created_at: new Date(), - black_user_id: parentIsBlack ? exist.parent_id : user._id, - white_user_id: parentIsBlack ? user._id : exist.parent_id, - turn_user_id: parentIsBlack ? exist.parent_id : user._id, + user1_id: exist.parent_id, + user2_id: user._id, + user1_accepted: false, + user2_accepted: false, + is_started: false, is_ended: false, - logs: [] + logs: [], + settings: { + map: eighteight, + bw: 'random', + is_llotheo: false + } }); // Reponse diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts index 73a5c94b2..de7c804c4 100644 --- a/src/api/models/othello-game.ts +++ b/src/api/models/othello-game.ts @@ -2,6 +2,7 @@ import * as mongo from 'mongodb'; import deepcopy = require('deepcopy'); import db from '../../db/mongodb'; import { IUser, pack as packUser } from './user'; +import { Map } from '../../common/othello/maps'; const Game = db.get('othello_games'); export default Game; @@ -9,12 +10,28 @@ export default Game; export interface IGame { _id: mongo.ObjectID; created_at: Date; - black_user_id: mongo.ObjectID; - white_user_id: mongo.ObjectID; - turn_user_id: mongo.ObjectID; + started_at: Date; + user1_id: mongo.ObjectID; + user2_id: mongo.ObjectID; + user1_accepted: boolean; + user2_accepted: boolean; + + /** + * どちらのプレイヤーが先行(黒)か + * 1 ... user1 + * 2 ... user2 + */ + black: number; + + is_started: boolean; is_ended: boolean; winner_id: mongo.ObjectID; logs: any[]; + settings: { + map: Map; + bw: string | number; + is_llotheo: boolean; + }; } /** @@ -24,6 +41,20 @@ export const pack = ( game: any, me?: string | mongo.ObjectID | IUser ) => new Promise(async (resolve, reject) => { + let _game: any; + + // Populate the game if 'game' is ID + if (mongo.ObjectID.prototype.isPrototypeOf(game)) { + _game = await Game.findOne({ + _id: game + }); + } else if (typeof game === 'string') { + _game = await Game.findOne({ + _id: new mongo.ObjectID(game) + }); + } else { + _game = deepcopy(game); + } // Me const meId: mongo.ObjectID = me @@ -34,15 +65,13 @@ export const pack = ( : (me as IUser)._id : null; - const _game = deepcopy(game); - // Rename _id to id _game.id = _game._id; delete _game._id; // Populate user - _game.black_user = await packUser(_game.black_user_id, meId); - _game.white_user = await packUser(_game.white_user_id, meId); + _game.user1 = await packUser(_game.user1_id, meId); + _game.user2 = await packUser(_game.user2_id, meId); if (_game.winner_id) { _game.winner = await packUser(_game.winner_id, meId); } else { diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts index d08647815..1dcd37efa 100644 --- a/src/api/stream/othello-game.ts +++ b/src/api/stream/othello-game.ts @@ -1,8 +1,8 @@ import * as websocket from 'websocket'; import * as redis from 'redis'; -import Game from '../models/othello-game'; +import Game, { pack } from '../models/othello-game'; import { publishOthelloGameStream } from '../event'; -import Othello from '../../common/othello'; +import Othello from '../../common/othello/core'; export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { const gameId = request.resourceURL.query.game; @@ -17,6 +17,19 @@ export default function(request: websocket.request, connection: websocket.connec const msg = JSON.parse(data.utf8Data); switch (msg.type) { + case 'accept': + accept(true); + break; + + case 'cancel-accept': + accept(false); + break; + + case 'update-settings': + if (msg.settings == null) return; + updateSettings(msg.settings); + break; + case 'set': if (msg.pos == null) return; set(msg.pos); @@ -24,38 +37,118 @@ export default function(request: websocket.request, connection: websocket.connec } }); + async function updateSettings(settings) { + const game = await Game.findOne({ _id: gameId }); + + if (game.is_started) return; + if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return; + if (game.user1_id.equals(user._id) && game.user1_accepted) return; + if (game.user2_id.equals(user._id) && game.user2_accepted) return; + + await Game.update({ _id: gameId }, { + $set: { + settings + } + }); + + publishOthelloGameStream(gameId, 'update-settings', settings); + } + + async function accept(accept: boolean) { + const game = await Game.findOne({ _id: gameId }); + + if (game.is_started) return; + + let bothAccepted = false; + + if (game.user1_id.equals(user._id)) { + await Game.update({ _id: gameId }, { + $set: { + user1_accepted: accept + } + }); + + publishOthelloGameStream(gameId, 'change-accepts', { + user1: accept, + user2: game.user2_accepted + }); + + if (accept && game.user2_accepted) bothAccepted = true; + } else if (game.user2_id.equals(user._id)) { + await Game.update({ _id: gameId }, { + $set: { + user2_accepted: accept + } + }); + + publishOthelloGameStream(gameId, 'change-accepts', { + user1: game.user1_accepted, + user2: accept + }); + + if (accept && game.user1_accepted) bothAccepted = true; + } else { + return; + } + + if (bothAccepted) { + // 3秒後、まだacceptされていたらゲーム開始 + setTimeout(async () => { + const freshGame = await Game.findOne({ _id: gameId }); + if (freshGame == null || freshGame.is_started || freshGame.is_ended) return; + + let bw: number; + if (freshGame.settings.bw == 'random') { + bw = Math.random() > 0.5 ? 1 : 2; + } else { + bw = freshGame.settings.bw as number; + } + + await Game.update({ _id: gameId }, { + $set: { + started_at: new Date(), + is_started: true, + black: bw + } + }); + + publishOthelloGameStream(gameId, 'started', await pack(gameId)); + }, 3000); + } + } + async function set(pos) { const game = await Game.findOne({ _id: gameId }); + if (!game.is_started) return; if (game.is_ended) return; - if (!game.black_user_id.equals(user._id) && !game.white_user_id.equals(user._id)) return; + if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return; - const o = new Othello(); - - game.logs.forEach(log => { - o.set(log.color, log.pos); + const o = new Othello(game.settings.map, { + isLlotheo: game.settings.is_llotheo }); - const myColor = game.black_user_id.equals(user._id) ? 'black' : 'white'; - const opColor = myColor == 'black' ? 'white' : 'black'; + game.logs.forEach(log => { + o.put(log.color, log.pos); + }); - if (!o.canReverse(myColor, pos)) return; - o.set(myColor, pos); + const myColor = + (game.user1_id.equals(user._id) && game.black == 1) || (game.user2_id.equals(user._id) && game.black == 2) + ? 'black' + : 'white'; - let turn; - if (o.getPattern(opColor).length > 0) { - turn = myColor == 'black' ? game.white_user_id : game.black_user_id; - } else if (o.getPattern(myColor).length > 0) { - turn = myColor == 'black' ? game.black_user_id : game.white_user_id; - } else { - turn = null; - } - - const isEnded = turn === null; + if (!o.canPut(myColor, pos)) return; + o.put(myColor, pos); let winner; - if (isEnded) { - winner = o.blackCount == o.whiteCount ? null : o.blackCount > o.whiteCount ? game.black_user_id : game.white_user_id; + if (o.isEnded) { + if (o.winner == 'black') { + winner = game.black == 1 ? game.user1_id : game.user2_id; + } else if (o.winner == 'white') { + winner = game.black == 1 ? game.user2_id : game.user1_id; + } else { + winner = null; + } } const log = { @@ -68,8 +161,7 @@ export default function(request: websocket.request, connection: websocket.connec _id: gameId }, { $set: { - turn_user_id: turn, - is_ended: isEnded, + is_ended: o.isEnded, winner_id: winner }, $push: { diff --git a/src/common/othello.ts b/src/common/othello.ts deleted file mode 100644 index 1da8ad36d..000000000 --- a/src/common/othello.ts +++ /dev/null @@ -1,325 +0,0 @@ -const BOARD_SIZE = 8; - -export default class Othello { - public board: Array<'black' | 'white'>; - - public stats: Array<{ - b: number; - w: number; - }> = []; - - /** - * ゲームを初期化します - */ - constructor() { - this.board = [ - null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - null, null, null, 'white', 'black', null, null, null, - null, null, null, 'black', 'white', null, null, null, - null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null - ]; - - this.stats.push({ - b: 0.5, - w: 0.5 - }); - } - - public prevPos = -1; - - public get blackCount() { - return this.board.filter(s => s == 'black').length; - } - - public get whiteCount() { - return this.board.filter(s => s == 'white').length; - } - - public get blackP() { - return this.blackCount / (this.blackCount + this.whiteCount); - } - - public get whiteP() { - return this.whiteCount / (this.blackCount + this.whiteCount); - } - - public setByNumber(color, n) { - const ps = this.getPattern(color); - this.set2(color, ps[n][0], ps[n][1]); - } - - private write(color, x, y) { - const pos = x + (y * 8); - this.board[pos] = color; - } - - /** - * 石を配置します - */ - public set2(color, x, y) { - this.prevPos = x + (y * 8); - this.write(color, x, y); - - const reverses = this.getReverse(color, x, y); - - reverses.forEach(r => { - switch (r[0]) { - case 0: // 上 - for (let c = 0, _y = y - 1; c < r[1]; c++, _y--) { - this.write(color, x, _y); - } - break; - - case 1: // 右上 - for (let c = 0, i = 1; c < r[1]; c++, i++) { - this.write(color, x + i, y - i); - } - break; - - case 2: // 右 - for (let c = 0, _x = x + 1; c < r[1]; c++, _x++) { - this.write(color, _x, y); - } - break; - - case 3: // 右下 - for (let c = 0, i = 1; c < r[1]; c++, i++) { - this.write(color, x + i, y + i); - } - break; - - case 4: // 下 - for (let c = 0, _y = y + 1; c < r[1]; c++, _y++) { - this.write(color, x, _y); - } - break; - - case 5: // 左下 - for (let c = 0, i = 1; c < r[1]; c++, i++) { - this.write(color, x - i, y + i); - } - break; - - case 6: // 左 - for (let c = 0, _x = x - 1; c < r[1]; c++, _x--) { - this.write(color, _x, y); - } - break; - - case 7: // 左上 - for (let c = 0, i = 1; c < r[1]; c++, i++) { - this.write(color, x - i, y - i); - } - break; - } - }); - - this.stats.push({ - b: this.blackP, - w: this.whiteP - }); - } - - public set(color, pos) { - const x = pos % BOARD_SIZE; - const y = Math.floor(pos / BOARD_SIZE); - this.set2(color, x, y); - } - - public get(x, y) { - const pos = x + (y * 8); - return this.board[pos]; - } - - /** - * 打つことができる場所を取得します - */ - public getPattern(myColor): number[][] { - const result = []; - this.board.forEach((stone, i) => { - if (stone != null) return; - const x = i % BOARD_SIZE; - const y = Math.floor(i / BOARD_SIZE); - if (this.canReverse2(myColor, x, y)) result.push([x, y]); - }); - return result; - } - - /** - * 指定の位置に石を打つことができるかどうか(相手の石を1つでも反転させられるか)を取得します - */ - public canReverse2(myColor, x, y): boolean { - return this.canReverse(myColor, x + (y * 8)); - } - public canReverse(myColor, pos): boolean { - if (this.board[pos] != null) return false; - const x = pos % BOARD_SIZE; - const y = Math.floor(pos / BOARD_SIZE); - return this.getReverse(myColor, x, y) !== null; - } - - private getReverse(myColor, targetx, targety): number[] { - const opponentColor = myColor == 'black' ? 'white' : 'black'; - - const createIterater = () => { - let opponentStoneFound = false; - let breaked = false; - return (x, y): any => { - if (breaked) { - return; - } else if (this.get(x, y) == myColor && opponentStoneFound) { - return true; - } else if (this.get(x, y) == myColor && !opponentStoneFound) { - breaked = true; - } else if (this.get(x, y) == opponentColor) { - opponentStoneFound = true; - } else { - breaked = true; - } - }; - }; - - const res = []; - - let iterate; - - // 上 - iterate = createIterater(); - for (let c = 0, y = targety - 1; y >= 0; c++, y--) { - if (iterate(targetx, y)) { - res.push([0, c]); - break; - } - } - - // 右上 - iterate = createIterater(); - for (let c = 0, i = 1; i <= Math.min(BOARD_SIZE - targetx, targety); c++, i++) { - if (iterate(targetx + i, targety - i)) { - res.push([1, c]); - break; - } - } - - // 右 - iterate = createIterater(); - for (let c = 0, x = targetx + 1; x < BOARD_SIZE; c++, x++) { - if (iterate(x, targety)) { - res.push([2, c]); - break; - } - } - - // 右下 - iterate = createIterater(); - for (let c = 0, i = 1; i < Math.min(BOARD_SIZE - targetx, BOARD_SIZE - targety); c++, i++) { - if (iterate(targetx + i, targety + i)) { - res.push([3, c]); - break; - } - } - - // 下 - iterate = createIterater(); - for (let c = 0, y = targety + 1; y < BOARD_SIZE; c++, y++) { - if (iterate(targetx, y)) { - res.push([4, c]); - break; - } - } - - // 左下 - iterate = createIterater(); - for (let c = 0, i = 1; i <= Math.min(targetx, BOARD_SIZE - targety); c++, i++) { - if (iterate(targetx - i, targety + i)) { - res.push([5, c]); - break; - } - } - - // 左 - iterate = createIterater(); - for (let c = 0, x = targetx - 1; x >= 0; c++, x--) { - if (iterate(x, targety)) { - res.push([6, c]); - break; - } - } - - // 左上 - iterate = createIterater(); - for (let c = 0, i = 1; i <= Math.min(targetx, targety); c++, i++) { - if (iterate(targetx - i, targety - i)) { - res.push([7, c]); - break; - } - } - - return res.length === 0 ? null : res; - } - - public toString(): string { - //return this.board.map(row => row.map(state => state === 'black' ? '●' : state === 'white' ? '○' : '┼').join('')).join('\n'); - //return this.board.map(row => row.map(state => state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : '🔹').join('')).join('\n'); - return 'wip'; - } - - public toPatternString(color): string { - //const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; - /*const num = ['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟', '🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🍍']; - - const pattern = this.getPattern(color); - - return this.board.map((row, y) => row.map((state, x) => { - const i = pattern.findIndex(p => p[0] == x && p[1] == y); - //return state === 'black' ? '●' : state === 'white' ? '○' : i != -1 ? num[i] : '┼'; - return state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : i != -1 ? num[i] : '🔹'; - }).join('')).join('\n');*/ - - return 'wip'; - } -} - -export function ai(color: string, othello: Othello) { - const opponentColor = color == 'black' ? 'white' : 'black'; - - function think() { - // 打てる場所を取得 - const ps = othello.getPattern(color); - - if (ps.length > 0) { // 打てる場所がある場合 - // 角を取得 - const corners = ps.filter(p => - // 左上 - (p[0] == 0 && p[1] == 0) || - // 右上 - (p[0] == (BOARD_SIZE - 1) && p[1] == 0) || - // 右下 - (p[0] == (BOARD_SIZE - 1) && p[1] == (BOARD_SIZE - 1)) || - // 左下 - (p[0] == 0 && p[1] == (BOARD_SIZE - 1)) - ); - - if (corners.length > 0) { // どこかしらの角に打てる場合 - // 打てる角からランダムに選択して打つ - const p = corners[Math.floor(Math.random() * corners.length)]; - othello.set(color, p[0], p[1]); - } else { // 打てる角がない場合 - // 打てる場所からランダムに選択して打つ - const p = ps[Math.floor(Math.random() * ps.length)]; - othello.set(color, p[0], p[1]); - } - - // 相手の打つ場所がない場合続けてAIのターン - if (othello.getPattern(opponentColor).length === 0) { - think(); - } - } - } - - think(); -} diff --git a/src/common/othello/ai.ts b/src/common/othello/ai.ts new file mode 100644 index 000000000..3943d04bf --- /dev/null +++ b/src/common/othello/ai.ts @@ -0,0 +1,42 @@ +import Othello, { Color } from './core'; + +export function ai(color: Color, othello: Othello) { + //const opponentColor = color == 'black' ? 'white' : 'black'; +/* wip + + function think() { + // 打てる場所を取得 + const ps = othello.canPutSomewhere(color); + + if (ps.length > 0) { // 打てる場所がある場合 + // 角を取得 + const corners = ps.filter(p => + // 左上 + (p[0] == 0 && p[1] == 0) || + // 右上 + (p[0] == (BOARD_SIZE - 1) && p[1] == 0) || + // 右下 + (p[0] == (BOARD_SIZE - 1) && p[1] == (BOARD_SIZE - 1)) || + // 左下 + (p[0] == 0 && p[1] == (BOARD_SIZE - 1)) + ); + + if (corners.length > 0) { // どこかしらの角に打てる場合 + // 打てる角からランダムに選択して打つ + const p = corners[Math.floor(Math.random() * corners.length)]; + othello.set(color, p[0], p[1]); + } else { // 打てる角がない場合 + // 打てる場所からランダムに選択して打つ + const p = ps[Math.floor(Math.random() * ps.length)]; + othello.set(color, p[0], p[1]); + } + + // 相手の打つ場所がない場合続けてAIのターン + if (othello.getPattern(opponentColor).length === 0) { + think(); + } + } + } + + think();*/ +} diff --git a/src/common/othello/core.ts b/src/common/othello/core.ts new file mode 100644 index 000000000..b76586031 --- /dev/null +++ b/src/common/othello/core.ts @@ -0,0 +1,239 @@ +import { Map } from './maps'; + +export type Color = 'black' | 'white'; +export type MapPixel = 'null' | 'empty'; + +export type Options = { + isLlotheo: boolean; +}; + +/** + * オセロエンジン + */ +export default class Othello { + public map: Map; + public mapData: MapPixel[]; + public board: Color[]; + public turn: Color = 'black'; + public opts: Options; + + public stats: Array<{ + b: number; + w: number; + }>; + + /** + * ゲームを初期化します + */ + constructor(map: Map, opts: Options) { + this.map = map; + this.opts = opts; + + // Parse map data + this.board = this.map.data.split('').map(d => { + if (d == '-') return null; + if (d == 'b') return 'black'; + if (d == 'w') return 'white'; + return undefined; + }); + this.mapData = this.map.data.split('').map(d => { + if (d == '-' || d == 'b' || d == 'w') return 'empty'; + return 'null'; + }); + + // Init stats + this.stats = [{ + b: this.blackP, + w: this.whiteP + }]; + } + + public prevPos = -1; + + /** + * 黒石の数 + */ + public get blackCount() { + return this.board.filter(x => x == 'black').length; + } + + /** + * 白石の数 + */ + public get whiteCount() { + return this.board.filter(x => x == 'white').length; + } + + /** + * 黒石の比率 + */ + public get blackP() { + return this.blackCount / (this.blackCount + this.whiteCount); + } + + /** + * 白石の比率 + */ + public get whiteP() { + return this.whiteCount / (this.blackCount + this.whiteCount); + } + + public transformPosToXy(pos: number): number[] { + const x = pos % this.map.size; + const y = Math.floor(pos / this.map.size); + return [x, y]; + } + + public transformXyToPos(x: number, y: number): number { + return x + (y * this.map.size); + } + + /** + * 指定のマスに石を書き込みます + * @param color 石の色 + * @param pos 位置 + */ + private write(color: Color, pos: number) { + this.board[pos] = color; + } + + /** + * 指定のマスに石を打ちます + * @param color 石の色 + * @param pos 位置 + */ + public put(color: Color, pos: number) { + if (!this.canPut(color, pos)) return; + + this.prevPos = pos; + this.write(color, pos); + + // 反転させられる石を取得 + const reverses = this.effects(color, pos); + + // 反転させる + reverses.forEach(pos => { + this.write(color, pos); + }); + + this.stats.push({ + b: this.blackP, + w: this.whiteP + }); + + // ターン計算 + const opColor = color == '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'; + } else { + this.turn = null; + } + } + + /** + * 指定したマスの状態を取得します + * @param pos 位置 + */ + public get(pos: number) { + return this.board[pos]; + } + + /** + * 指定した位置のマップデータのマスを取得します + * @param pos 位置 + */ + public mapDataGet(pos: number): MapPixel { + if (pos < 0 || pos >= this.mapData.length) return 'null'; + return this.mapData[pos]; + } + + /** + * 打つことができる場所を取得します + */ + public canPutSomewhere(color: Color): number[] { + const result = []; + + this.board.forEach((x, i) => { + if (this.canPut(color, i)) result.push(i); + }); + + return result; + } + + /** + * 指定のマスに石を打つことができるかどうか(相手の石を1つでも反転させられるか)を取得します + * @param color 自分の色 + * @param pos 位置 + */ + public canPut(color: Color, pos: number): boolean { + // 既に石が置いてある場所には打てない + if (this.get(pos) !== null) return false; + return this.effects(color, pos).length !== 0; + } + + /** + * 指定のマスに石を置いた時の、反転させられる石を取得します + * @param color 自分の色 + * @param pos 位置 + */ + private effects(color: Color, pos: number): number[] { + const enemyColor = color == 'black' ? 'white' : 'black'; + const [x, y] = this.transformPosToXy(pos); + let stones = []; + + const iterate = (fn: (i: number) => number[]) => { + let i = 1; + const found = []; + while (true) { + const [x, y] = fn(i); + if (x < 0 || y < 0 || x >= this.map.size || y >= this.map.size) break; + const pos = this.transformXyToPos(x, y); + const pixel = this.mapDataGet(pos); + if (pixel == 'null') break; + const stone = this.get(pos); + if (stone == null) break; + if (stone == enemyColor) found.push(pos); + if (stone == color) { + stones = stones.concat(found); + break; + } + i++; + } + }; + + iterate(i => [x , y - i]); // 上 + iterate(i => [x + i, y - i]); // 右上 + iterate(i => [x + i, y ]); // 右 + iterate(i => [x + i, y + i]); // 右下 + iterate(i => [x , y + i]); // 下 + iterate(i => [x - i, y + i]); // 左下 + iterate(i => [x - i, y ]); // 左 + iterate(i => [x - i, y - i]); // 左上 + + return stones; + } + + /** + * ゲームが終了したか否か + */ + public get isEnded(): boolean { + return this.turn === null; + } + + /** + * ゲームの勝者 (null = 引き分け) + */ + public get winner(): Color { + if (!this.isEnded) return undefined; + + if (this.blackCount == this.whiteCount) return null; + + if (this.opts.isLlotheo) { + return this.blackCount > this.whiteCount ? 'white' : 'black'; + } else { + return this.blackCount > this.whiteCount ? 'black' : 'white'; + } + } +} diff --git a/src/common/othello/maps.ts b/src/common/othello/maps.ts new file mode 100644 index 000000000..e6f3f409e --- /dev/null +++ b/src/common/othello/maps.ts @@ -0,0 +1,217 @@ +/** + * 組み込みマップ定義 + * + * データ値: + * (スペース) ... マス無し + * - ... マス + * b ... 初期配置される黒石 + * w ... 初期配置される白石 + */ + +export type Map = { + name?: string; + size: number; + data: string; +}; + +export const fourfour: Map = { + name: '4x4', + size: 4, + data: + '----' + + '-wb-' + + '-bw-' + + '----' +}; + +export const sixsix: Map = { + name: '6x6', + size: 6, + data: + '------' + + '------' + + '--wb--' + + '--bw--' + + '------' + + '------' +}; + +export const eighteight: Map = { + name: '8x8', + size: 8, + data: + '--------' + + '--------' + + '--------' + + '---wb---' + + '---bw---' + + '--------' + + '--------' + + '--------' +}; + +export const roundedEighteight: Map = { + name: '8x8 rounded', + size: 8, + data: + ' ------ ' + + '--------' + + '--------' + + '---wb---' + + '---bw---' + + '--------' + + '--------' + + ' ------ ' +}; + +export const roundedEighteight2: Map = { + name: '8x8 rounded 2', + size: 8, + data: + ' ---- ' + + ' ------ ' + + '--------' + + '---wb---' + + '---bw---' + + '--------' + + ' ------ ' + + ' ---- ' +}; + +export const eighteightWithNotch: Map = { + name: '8x8 with notch', + size: 8, + data: + '--- ---' + + '--------' + + '--------' + + ' --wb-- ' + + ' --bw-- ' + + '--------' + + '--------' + + '--- ---' +}; + +export const eighteightWithSomeHoles: Map = { + name: '8x8 with some holes', + size: 8, + data: + '--- ----' + + '----- --' + + '-- -----' + + '---wb---' + + '---bw- -' + + ' -------' + + '--- ----' + + '--------' +}; + +export const sixeight: Map = { + name: '6x8', + size: 8, + data: + ' ------ ' + + ' ------ ' + + ' ------ ' + + ' --wb-- ' + + ' --bw-- ' + + ' ------ ' + + ' ------ ' + + ' ------ ' +}; + +export const tenthtenth: Map = { + name: '10x10', + size: 10, + data: + '----------' + + '----------' + + '----------' + + '----------' + + '----wb----' + + '----bw----' + + '----------' + + '----------' + + '----------' + + '----------' +}; + +export const hole: Map = { + name: 'hole', + size: 10, + data: + '----------' + + '----------' + + '--wb--wb--' + + '--bw--bw--' + + '---- ----' + + '---- ----' + + '--wb--wb--' + + '--bw--bw--' + + '----------' + + '----------' +}; + +export const spark: Map = { + name: 'spark', + size: 10, + data: + ' - - ' + + '----------' + + ' -------- ' + + ' -------- ' + + ' ---wb--- ' + + ' ---bw--- ' + + ' -------- ' + + ' -------- ' + + '----------' + + ' - - ' +}; + +export const islands: Map = { + name: 'islands', + size: 10, + data: + '-------- ' + + '---wb--- ' + + '---bw--- ' + + '-------- ' + + ' - - ' + + ' - - ' + + ' --------' + + ' ---bw---' + + ' ---wb---' + + ' --------' +}; + +export const grid: Map = { + name: 'grid', + size: 10, + data: + '----------' + + '- - -- - -' + + '----------' + + '- - -- - -' + + '----wb----' + + '----bw----' + + '- - -- - -' + + '----------' + + '- - -- - -' + + '----------' +}; + +export const iphonex: Map = { + name: 'iPhone X', + size: 10, + data: + ' -- -- ' + + ' -------- ' + + ' -------- ' + + ' -------- ' + + ' ---wb--- ' + + ' ---bw--- ' + + ' -------- ' + + ' -------- ' + + ' -------- ' + + ' ------ ' +}; diff --git a/src/web/app/common/views/components/othello.game.vue b/src/web/app/common/views/components/othello.game.vue index 1cb2400f7..2ef6b645c 100644 --- a/src/web/app/common/views/components/othello.game.vue +++ b/src/web/app/common/views/components/othello.game.vue @@ -1,23 +1,27 @@