Merge branch 'develop' into pizzax-indexeddb
This commit is contained in:
		
						commit
						60ce9aa53c
					
				
					 42 changed files with 7 additions and 5230 deletions
				
			
		| 
						 | 
				
			
			@ -12,6 +12,8 @@
 | 
			
		|||
### Changes
 | 
			
		||||
- Room機能が削除されました
 | 
			
		||||
  - 後日別リポジトリとして復活予定です
 | 
			
		||||
- リバーシ機能が削除されました
 | 
			
		||||
  - 後日別リポジトリとして復活予定です
 | 
			
		||||
- Chat UIが削除されました
 | 
			
		||||
 | 
			
		||||
### Improvements
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -242,7 +242,6 @@ uploadFromUrlDescription: "アップロードしたいファイルのURL"
 | 
			
		|||
uploadFromUrlRequested: "アップロードをリクエストしました"
 | 
			
		||||
uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。"
 | 
			
		||||
explore: "みつける"
 | 
			
		||||
games: "Misskey Games"
 | 
			
		||||
messageRead: "既読"
 | 
			
		||||
noMoreHistory: "これより過去の履歴はありません"
 | 
			
		||||
startMessaging: "チャットを開始"
 | 
			
		||||
| 
						 | 
				
			
			@ -669,7 +668,6 @@ emailVerified: "メールアドレスが確認されました"
 | 
			
		|||
noteFavoritesCount: "お気に入りノートの数"
 | 
			
		||||
pageLikesCount: "Pageにいいねした数"
 | 
			
		||||
pageLikedCount: "Pageにいいねされた数"
 | 
			
		||||
reversiCount: "リバーシの対局数"
 | 
			
		||||
contact: "連絡先"
 | 
			
		||||
useSystemFont: "システムのデフォルトのフォントを使う"
 | 
			
		||||
clips: "クリップ"
 | 
			
		||||
| 
						 | 
				
			
			@ -957,40 +955,6 @@ _mfm:
 | 
			
		|||
  rotate: "回転"
 | 
			
		||||
  rotateDescription: "指定した角度で回転させます。"
 | 
			
		||||
 | 
			
		||||
_reversi:
 | 
			
		||||
  reversi: "リバーシ"
 | 
			
		||||
  gameSettings: "対局の設定"
 | 
			
		||||
  chooseBoard: "ボードを選択"
 | 
			
		||||
  blackOrWhite: "先行/後攻"
 | 
			
		||||
  blackIs: "{name}が黒(先行)"
 | 
			
		||||
  rules: "ルール"
 | 
			
		||||
  botSettings: "Botのオプション"
 | 
			
		||||
  thisGameIsStartedSoon: "対局は数秒後に開始されます"
 | 
			
		||||
  waitingForOther: "相手の準備が完了するのを待っています"
 | 
			
		||||
  waitingForMe: "あなたの準備が完了するのを待っています"
 | 
			
		||||
  waitingBoth: "準備してください"
 | 
			
		||||
  ready: "準備完了"
 | 
			
		||||
  cancelReady: "準備を再開"
 | 
			
		||||
  opponentTurn: "相手のターンです"
 | 
			
		||||
  myTurn: "あなたのターンです"
 | 
			
		||||
  turnOf: "{name}のターンです"
 | 
			
		||||
  pastTurnOf: "{name}のターン"
 | 
			
		||||
  surrender: "投了"
 | 
			
		||||
  surrendered: "投了により"
 | 
			
		||||
  drawn: "引き分け"
 | 
			
		||||
  won: "{name}の勝ち"
 | 
			
		||||
  black: "黒"
 | 
			
		||||
  white: "白"
 | 
			
		||||
  total: "合計"
 | 
			
		||||
  turnCount: "{count}ターン目"
 | 
			
		||||
  myGames: "自分の対局"
 | 
			
		||||
  allGames: "みんなの対局"
 | 
			
		||||
  ended: "終了"
 | 
			
		||||
  playing: "対局中"
 | 
			
		||||
  isLlotheo: "石の少ない方が勝ち(ロセオ)"
 | 
			
		||||
  loopedMap: "ループマップ"
 | 
			
		||||
  canPutEverywhere: "どこでも置けるモード"
 | 
			
		||||
 | 
			
		||||
_instanceTicker:
 | 
			
		||||
  none: "表示しない"
 | 
			
		||||
  remote: "リモートユーザーに表示"
 | 
			
		||||
| 
						 | 
				
			
			@ -1118,8 +1082,6 @@ _sfx:
 | 
			
		|||
  chatBg: "チャット(バックグラウンド)"
 | 
			
		||||
  antenna: "アンテナ受信"
 | 
			
		||||
  channel: "チャンネル通知"
 | 
			
		||||
  reversiPutBlack: "リバーシ: 黒が打ったとき"
 | 
			
		||||
  reversiPutWhite: "リバーシ: 白が打ったとき"
 | 
			
		||||
 | 
			
		||||
_ago:
 | 
			
		||||
  unknown: "謎"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,8 +40,6 @@ import { Signin } from '@/models/entities/signin';
 | 
			
		|||
import { AuthSession } from '@/models/entities/auth-session';
 | 
			
		||||
import { FollowRequest } from '@/models/entities/follow-request';
 | 
			
		||||
import { Emoji } from '@/models/entities/emoji';
 | 
			
		||||
import { ReversiGame } from '@/models/entities/games/reversi/game';
 | 
			
		||||
import { ReversiMatching } from '@/models/entities/games/reversi/matching';
 | 
			
		||||
import { UserNotePining } from '@/models/entities/user-note-pining';
 | 
			
		||||
import { Poll } from '@/models/entities/poll';
 | 
			
		||||
import { UserKeypair } from '@/models/entities/user-keypair';
 | 
			
		||||
| 
						 | 
				
			
			@ -166,8 +164,6 @@ export const entities = [
 | 
			
		|||
	AntennaNote,
 | 
			
		||||
	PromoNote,
 | 
			
		||||
	PromoRead,
 | 
			
		||||
	ReversiGame,
 | 
			
		||||
	ReversiMatching,
 | 
			
		||||
	Relay,
 | 
			
		||||
	MutedNote,
 | 
			
		||||
	Channel,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,263 +0,0 @@
 | 
			
		|||
import { count, concat } from '@/prelude/array';
 | 
			
		||||
 | 
			
		||||
// MISSKEY REVERSI ENGINE
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * true ... 黒
 | 
			
		||||
 * false ... 白
 | 
			
		||||
 */
 | 
			
		||||
export type Color = boolean;
 | 
			
		||||
const BLACK = true;
 | 
			
		||||
const WHITE = false;
 | 
			
		||||
 | 
			
		||||
export type MapPixel = 'null' | 'empty';
 | 
			
		||||
 | 
			
		||||
export type Options = {
 | 
			
		||||
	isLlotheo: boolean;
 | 
			
		||||
	canPutEverywhere: boolean;
 | 
			
		||||
	loopedBoard: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Undo = {
 | 
			
		||||
	/**
 | 
			
		||||
	 * 色
 | 
			
		||||
	 */
 | 
			
		||||
	color: Color;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * どこに打ったか
 | 
			
		||||
	 */
 | 
			
		||||
	pos: number;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 反転した石の位置の配列
 | 
			
		||||
	 */
 | 
			
		||||
	effects: number[];
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ターン
 | 
			
		||||
	 */
 | 
			
		||||
	turn: Color | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * リバーシエンジン
 | 
			
		||||
 */
 | 
			
		||||
export default class Reversi {
 | 
			
		||||
	public map: MapPixel[];
 | 
			
		||||
	public mapWidth: number;
 | 
			
		||||
	public mapHeight: number;
 | 
			
		||||
	public board: (Color | null | undefined)[];
 | 
			
		||||
	public turn: Color | null = BLACK;
 | 
			
		||||
	public opts: Options;
 | 
			
		||||
 | 
			
		||||
	public prevPos = -1;
 | 
			
		||||
	public prevColor: Color | null = null;
 | 
			
		||||
 | 
			
		||||
	private logs: Undo[] = [];
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ゲームを初期化します
 | 
			
		||||
	 */
 | 
			
		||||
	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;
 | 
			
		||||
		if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
 | 
			
		||||
		if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
		//#region Parse map data
 | 
			
		||||
		this.mapWidth = map[0].length;
 | 
			
		||||
		this.mapHeight = map.length;
 | 
			
		||||
		const mapData = map.join('');
 | 
			
		||||
 | 
			
		||||
		this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined);
 | 
			
		||||
 | 
			
		||||
		this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null');
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
 | 
			
		||||
		if (!this.canPutSomewhere(BLACK)) this.turn = this.canPutSomewhere(WHITE) ? WHITE : null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 黒石の数
 | 
			
		||||
	 */
 | 
			
		||||
	public get blackCount() {
 | 
			
		||||
		return count(BLACK, this.board);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 白石の数
 | 
			
		||||
	 */
 | 
			
		||||
	public get whiteCount() {
 | 
			
		||||
		return count(WHITE, this.board);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public transformPosToXy(pos: number): number[] {
 | 
			
		||||
		const x = pos % this.mapWidth;
 | 
			
		||||
		const y = Math.floor(pos / this.mapWidth);
 | 
			
		||||
		return [x, y];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public transformXyToPos(x: number, y: number): number {
 | 
			
		||||
		return x + (y * this.mapWidth);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 指定のマスに石を打ちます
 | 
			
		||||
	 * @param color 石の色
 | 
			
		||||
	 * @param pos 位置
 | 
			
		||||
	 */
 | 
			
		||||
	public put(color: Color, pos: number) {
 | 
			
		||||
		this.prevPos = pos;
 | 
			
		||||
		this.prevColor = color;
 | 
			
		||||
 | 
			
		||||
		this.board[pos] = color;
 | 
			
		||||
 | 
			
		||||
		// 反転させられる石を取得
 | 
			
		||||
		const effects = this.effects(color, pos);
 | 
			
		||||
 | 
			
		||||
		// 反転させる
 | 
			
		||||
		for (const pos of effects) {
 | 
			
		||||
			this.board[pos] = color;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const turn = this.turn;
 | 
			
		||||
 | 
			
		||||
		this.logs.push({
 | 
			
		||||
			color,
 | 
			
		||||
			pos,
 | 
			
		||||
			effects,
 | 
			
		||||
			turn,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.calcTurn();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private calcTurn() {
 | 
			
		||||
		// ターン計算
 | 
			
		||||
		this.turn =
 | 
			
		||||
			this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
 | 
			
		||||
			this.canPutSomewhere(this.prevColor!) ? this.prevColor :
 | 
			
		||||
			null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public undo() {
 | 
			
		||||
		const undo = this.logs.pop()!;
 | 
			
		||||
		this.prevColor = undo.color;
 | 
			
		||||
		this.prevPos = undo.pos;
 | 
			
		||||
		this.board[undo.pos] = null;
 | 
			
		||||
		for (const pos of undo.effects) {
 | 
			
		||||
			const color = this.board[pos];
 | 
			
		||||
			this.board[pos] = !color;
 | 
			
		||||
		}
 | 
			
		||||
		this.turn = undo.turn;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 指定した位置のマップデータのマスを取得します
 | 
			
		||||
	 * @param pos 位置
 | 
			
		||||
	 */
 | 
			
		||||
	public mapDataGet(pos: number): MapPixel {
 | 
			
		||||
		const [x, y] = this.transformPosToXy(pos);
 | 
			
		||||
		return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 打つことができる場所を取得します
 | 
			
		||||
	 */
 | 
			
		||||
	public puttablePlaces(color: Color): number[] {
 | 
			
		||||
		return Array.from(this.board.keys()).filter(i => this.canPut(color, i));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 打つことができる場所があるかどうかを取得します
 | 
			
		||||
	 */
 | 
			
		||||
	public canPutSomewhere(color: Color): boolean {
 | 
			
		||||
		return this.puttablePlaces(color).length > 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 指定のマスに石を打つことができるかどうかを取得します
 | 
			
		||||
	 * @param color 自分の色
 | 
			
		||||
	 * @param pos 位置
 | 
			
		||||
	 */
 | 
			
		||||
	public canPut(color: Color, pos: number): boolean {
 | 
			
		||||
		return (
 | 
			
		||||
			this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない
 | 
			
		||||
			this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード
 | 
			
		||||
			this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 指定のマスに石を置いた時の、反転させられる石を取得します
 | 
			
		||||
	 * @param color 自分の色
 | 
			
		||||
	 * @param initPos 位置
 | 
			
		||||
	 */
 | 
			
		||||
	public effects(color: Color, initPos: number): number[] {
 | 
			
		||||
		const enemyColor = !color;
 | 
			
		||||
 | 
			
		||||
		const diffVectors: [number, number][] = [
 | 
			
		||||
			[  0,  -1], // 上
 | 
			
		||||
			[ +1,  -1], // 右上
 | 
			
		||||
			[ +1,   0], // 右
 | 
			
		||||
			[ +1,  +1], // 右下
 | 
			
		||||
			[  0,  +1], // 下
 | 
			
		||||
			[ -1,  +1], // 左下
 | 
			
		||||
			[ -1,   0], // 左
 | 
			
		||||
			[ -1,  -1],  // 左上
 | 
			
		||||
		];
 | 
			
		||||
 | 
			
		||||
		const effectsInLine = ([dx, dy]: [number, number]): number[] => {
 | 
			
		||||
			const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy];
 | 
			
		||||
 | 
			
		||||
			const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
 | 
			
		||||
			let [x, y] = this.transformPosToXy(initPos);
 | 
			
		||||
			while (true) {
 | 
			
		||||
				[x, y] = nextPos(x, y);
 | 
			
		||||
 | 
			
		||||
				// 座標が指し示す位置がボード外に出たとき
 | 
			
		||||
				if (this.opts.loopedBoard && this.transformXyToPos(
 | 
			
		||||
					(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
 | 
			
		||||
					(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos) {
 | 
			
		||||
						// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
 | 
			
		||||
					return found;
 | 
			
		||||
				} else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight) {
 | 
			
		||||
					return []; // 挟めないことが確定 (盤面外に到達)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const pos = this.transformXyToPos(x, y);
 | 
			
		||||
				if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
 | 
			
		||||
				const stone = this.board[pos];
 | 
			
		||||
				if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達)
 | 
			
		||||
				if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見)
 | 
			
		||||
				if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見)
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return concat(diffVectors.map(effectsInLine));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ゲームが終了したか否か
 | 
			
		||||
	 */
 | 
			
		||||
	public get isEnded(): boolean {
 | 
			
		||||
		return this.turn === null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ゲームの勝者 (null = 引き分け)
 | 
			
		||||
	 */
 | 
			
		||||
	public get winner(): Color | null {
 | 
			
		||||
		return this.isEnded ?
 | 
			
		||||
			this.blackCount == this.whiteCount ? null :
 | 
			
		||||
			this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
 | 
			
		||||
			undefined as never;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,896 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * 組み込みマップ定義
 | 
			
		||||
 *
 | 
			
		||||
 * データ値:
 | 
			
		||||
 * (スペース) ... マス無し
 | 
			
		||||
 * - ... マス
 | 
			
		||||
 * b ... 初期配置される黒石
 | 
			
		||||
 * w ... 初期配置される白石
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export type Map = {
 | 
			
		||||
	name?: string;
 | 
			
		||||
	category?: string;
 | 
			
		||||
	author?: string;
 | 
			
		||||
	data: string[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fourfour: Map = {
 | 
			
		||||
	name: '4x4',
 | 
			
		||||
	category: '4x4',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----',
 | 
			
		||||
		'-wb-',
 | 
			
		||||
		'-bw-',
 | 
			
		||||
		'----',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sixsix: Map = {
 | 
			
		||||
	name: '6x6',
 | 
			
		||||
	category: '6x6',
 | 
			
		||||
	data: [
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
		'--wb--',
 | 
			
		||||
		'--bw--',
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const roundedSixsix: Map = {
 | 
			
		||||
	name: '6x6 rounded',
 | 
			
		||||
	category: '6x6',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' ---- ',
 | 
			
		||||
		'------',
 | 
			
		||||
		'--wb--',
 | 
			
		||||
		'--bw--',
 | 
			
		||||
		'------',
 | 
			
		||||
		' ---- ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const roundedSixsix2: Map = {
 | 
			
		||||
	name: '6x6 rounded 2',
 | 
			
		||||
	category: '6x6',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'  --  ',
 | 
			
		||||
		' ---- ',
 | 
			
		||||
		'--wb--',
 | 
			
		||||
		'--bw--',
 | 
			
		||||
		' ---- ',
 | 
			
		||||
		'  --  ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteight: Map = {
 | 
			
		||||
	name: '8x8',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightH1: Map = {
 | 
			
		||||
	name: '8x8 handicap 1',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b-------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightH2: Map = {
 | 
			
		||||
	name: '8x8 handicap 2',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b-------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-------b',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightH3: Map = {
 | 
			
		||||
	name: '8x8 handicap 3',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-------b',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightH4: Map = {
 | 
			
		||||
	name: '8x8 handicap 4',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'b------b',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightH28: Map = {
 | 
			
		||||
	name: '8x8 handicap 28',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'bbbbbbbb',
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'b--wb--b',
 | 
			
		||||
		'b--bw--b',
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'bbbbbbbb',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const roundedEighteight: Map = {
 | 
			
		||||
	name: '8x8 rounded',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const roundedEighteight2: Map = {
 | 
			
		||||
	name: '8x8 rounded 2',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'  ----  ',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'  ----  ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const roundedEighteight3: Map = {
 | 
			
		||||
	name: '8x8 rounded 3',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'   --   ',
 | 
			
		||||
		'  ----  ',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'  ----  ',
 | 
			
		||||
		'   --   ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightWithNotch: Map = {
 | 
			
		||||
	name: '8x8 with notch',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'---  ---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		' --wb-- ',
 | 
			
		||||
		' --bw-- ',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---  ---',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightWithSomeHoles: Map = {
 | 
			
		||||
	name: '8x8 with some holes',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--- ----',
 | 
			
		||||
		'----- --',
 | 
			
		||||
		'-- -----',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw- -',
 | 
			
		||||
		' -------',
 | 
			
		||||
		'--- ----',
 | 
			
		||||
		'--------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const circle: Map = {
 | 
			
		||||
	name: 'Circle',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'   --   ',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'   --   ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const smile: Map = {
 | 
			
		||||
	name: 'Smile',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-- -- --',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'-- bw --',
 | 
			
		||||
		'---  ---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const window: Map = {
 | 
			
		||||
	name: 'Window',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-  --  -',
 | 
			
		||||
		'-  --  -',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'-  --  -',
 | 
			
		||||
		'-  --  -',
 | 
			
		||||
		'--------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const reserved: Map = {
 | 
			
		||||
	name: 'Reserved',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'w------b',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'b------w',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const x: Map = {
 | 
			
		||||
	name: 'X',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'w------b',
 | 
			
		||||
		'-w----b-',
 | 
			
		||||
		'--w--b--',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--b--w--',
 | 
			
		||||
		'-b----w-',
 | 
			
		||||
		'b------w',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const parallel: Map = {
 | 
			
		||||
	name: 'Parallel',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---bb---',
 | 
			
		||||
		'---ww---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const lackOfBlack: Map = {
 | 
			
		||||
	name: 'Lack of Black',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---w----',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const squareParty: Map = {
 | 
			
		||||
	name: 'Square Party',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-wwwbbb-',
 | 
			
		||||
		'-w-wb-b-',
 | 
			
		||||
		'-wwwbbb-',
 | 
			
		||||
		'-bbbwww-',
 | 
			
		||||
		'-b-bw-w-',
 | 
			
		||||
		'-bbbwww-',
 | 
			
		||||
		'--------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const minesweeper: Map = {
 | 
			
		||||
	name: 'Minesweeper',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b-b--w-w',
 | 
			
		||||
		'-w-wb-b-',
 | 
			
		||||
		'w-b--w-b',
 | 
			
		||||
		'-b-wb-w-',
 | 
			
		||||
		'-w-bw-b-',
 | 
			
		||||
		'b-w--b-w',
 | 
			
		||||
		'-b-bw-w-',
 | 
			
		||||
		'w-w--b-b',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const tenthtenth: Map = {
 | 
			
		||||
	name: '10x10',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----wb----',
 | 
			
		||||
		'----bw----',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const hole: Map = {
 | 
			
		||||
	name: 'The Hole',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'--wb--wb--',
 | 
			
		||||
		'--bw--bw--',
 | 
			
		||||
		'----  ----',
 | 
			
		||||
		'----  ----',
 | 
			
		||||
		'--wb--wb--',
 | 
			
		||||
		'--bw--bw--',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const grid: Map = {
 | 
			
		||||
	name: 'Grid',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----------',
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
		'----wb----',
 | 
			
		||||
		'----bw----',
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
		'----------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const cross: Map = {
 | 
			
		||||
	name: 'Cross',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----wb----',
 | 
			
		||||
		'----bw----',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const charX: Map = {
 | 
			
		||||
	name: 'Char X',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'---    ---',
 | 
			
		||||
		'----  ----',
 | 
			
		||||
		'----------',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'  --wb--  ',
 | 
			
		||||
		'  --bw--  ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----  ----',
 | 
			
		||||
		'---    ---',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const charY: Map = {
 | 
			
		||||
	name: 'Char Y',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'---    ---',
 | 
			
		||||
		'----  ----',
 | 
			
		||||
		'----------',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'  --wb--  ',
 | 
			
		||||
		'  --bw--  ',
 | 
			
		||||
		'  ------  ',
 | 
			
		||||
		'  ------  ',
 | 
			
		||||
		'  ------  ',
 | 
			
		||||
		'  ------  ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const walls: Map = {
 | 
			
		||||
	name: 'Walls',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		' bbbbbbbb ',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		'w---wb---w',
 | 
			
		||||
		'w---bw---w',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		' bbbbbbbb ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const cpu: Map = {
 | 
			
		||||
	name: 'CPU',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' b b  b b ',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		' ---wb--- ',
 | 
			
		||||
		' ---bw--- ',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		' b b  b b ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const checker: Map = {
 | 
			
		||||
	name: 'Checker',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'---wbwb---',
 | 
			
		||||
		'---bwbw---',
 | 
			
		||||
		'---wbwb---',
 | 
			
		||||
		'---bwbw---',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const japaneseCurry: Map = {
 | 
			
		||||
	name: 'Japanese curry',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'w-b-b-b-b-',
 | 
			
		||||
		'-w-b-b-b-b',
 | 
			
		||||
		'w-w-b-b-b-',
 | 
			
		||||
		'-w-w-b-b-b',
 | 
			
		||||
		'w-w-wwb-b-',
 | 
			
		||||
		'-w-wbb-b-b',
 | 
			
		||||
		'w-w-w-b-b-',
 | 
			
		||||
		'-w-w-w-b-b',
 | 
			
		||||
		'w-w-w-w-b-',
 | 
			
		||||
		'-w-w-w-w-b',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const mosaic: Map = {
 | 
			
		||||
	name: 'Mosaic',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'- - - - - ',
 | 
			
		||||
		' - - - - -',
 | 
			
		||||
		'- - - - - ',
 | 
			
		||||
		' - w w - -',
 | 
			
		||||
		'- - b b - ',
 | 
			
		||||
		' - w w - -',
 | 
			
		||||
		'- - b b - ',
 | 
			
		||||
		' - - - - -',
 | 
			
		||||
		'- - - - - ',
 | 
			
		||||
		' - - - - -',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const arena: Map = {
 | 
			
		||||
	name: 'Arena',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
		' - -  - - ',
 | 
			
		||||
		'- ------ -',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'- --wb-- -',
 | 
			
		||||
		'- --bw-- -',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'- ------ -',
 | 
			
		||||
		' - -  - - ',
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const reactor: Map = {
 | 
			
		||||
	name: 'Reactor',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'-w------b-',
 | 
			
		||||
		'b- -  - -w',
 | 
			
		||||
		'- --wb-- -',
 | 
			
		||||
		'---b  w---',
 | 
			
		||||
		'- b wb w -',
 | 
			
		||||
		'- w bw b -',
 | 
			
		||||
		'---w  b---',
 | 
			
		||||
		'- --bw-- -',
 | 
			
		||||
		'w- -  - -b',
 | 
			
		||||
		'-b------w-',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sixeight: Map = {
 | 
			
		||||
	name: '6x8',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	data: [
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
		'--wb--',
 | 
			
		||||
		'--bw--',
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const spark: Map = {
 | 
			
		||||
	name: 'Spark',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' -      - ',
 | 
			
		||||
		'----------',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		' ---wb--- ',
 | 
			
		||||
		' ---bw--- ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'----------',
 | 
			
		||||
		' -      - ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const islands: Map = {
 | 
			
		||||
	name: 'Islands',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------  ',
 | 
			
		||||
		'---wb---  ',
 | 
			
		||||
		'---bw---  ',
 | 
			
		||||
		'--------  ',
 | 
			
		||||
		'  -    -  ',
 | 
			
		||||
		'  -    -  ',
 | 
			
		||||
		'  --------',
 | 
			
		||||
		'  --------',
 | 
			
		||||
		'  --------',
 | 
			
		||||
		'  --------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const galaxy: Map = {
 | 
			
		||||
	name: 'Galaxy',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'   ------   ',
 | 
			
		||||
		'  --www---  ',
 | 
			
		||||
		' ------w--- ',
 | 
			
		||||
		'---bbb--w---',
 | 
			
		||||
		'--b---b-w-b-',
 | 
			
		||||
		'-b--wwb-w-b-',
 | 
			
		||||
		'-b-w-bww--b-',
 | 
			
		||||
		'-b-w-b---b--',
 | 
			
		||||
		'---w--bbb---',
 | 
			
		||||
		' ---w------ ',
 | 
			
		||||
		'  ---www--  ',
 | 
			
		||||
		'   ------   ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const triangle: Map = {
 | 
			
		||||
	name: 'Triangle',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'    --    ',
 | 
			
		||||
		'    --    ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'  --wb--  ',
 | 
			
		||||
		'  --bw--  ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const iphonex: Map = {
 | 
			
		||||
	name: 'iPhone X',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' --  -- ',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dealWithIt: Map = {
 | 
			
		||||
	name: 'Deal with it!',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'------------',
 | 
			
		||||
		'--w-b-------',
 | 
			
		||||
		' --b-w------',
 | 
			
		||||
		'  --w-b---- ',
 | 
			
		||||
		'   -------  ',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const experiment: Map = {
 | 
			
		||||
	name: 'Let\'s experiment',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' ------------ ',
 | 
			
		||||
		'------wb------',
 | 
			
		||||
		'------bw------',
 | 
			
		||||
		'--------------',
 | 
			
		||||
		'    -    -    ',
 | 
			
		||||
		'------  ------',
 | 
			
		||||
		'bbbbbb  wwwwww',
 | 
			
		||||
		'bbbbbb  wwwwww',
 | 
			
		||||
		'bbbbbb  wwwwww',
 | 
			
		||||
		'bbbbbb  wwwwww',
 | 
			
		||||
		'wwwwww  bbbbbb',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const bigBoard: Map = {
 | 
			
		||||
	name: 'Big board',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'-------wb-------',
 | 
			
		||||
		'-------bw-------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const twoBoard: Map = {
 | 
			
		||||
	name: 'Two board',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
		'---wb--- ---wb---',
 | 
			
		||||
		'---bw--- ---bw---',
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const test1: Map = {
 | 
			
		||||
	name: 'Test1',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const test2: Map = {
 | 
			
		||||
	name: 'Test2',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
		'-b--w-',
 | 
			
		||||
		'-w--b-',
 | 
			
		||||
		'-w--b-',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const test3: Map = {
 | 
			
		||||
	name: 'Test3',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'-w-',
 | 
			
		||||
		'--w',
 | 
			
		||||
		'w--',
 | 
			
		||||
		'-w-',
 | 
			
		||||
		'--w',
 | 
			
		||||
		'w--',
 | 
			
		||||
		'-w-',
 | 
			
		||||
		'--w',
 | 
			
		||||
		'w--',
 | 
			
		||||
		'-w-',
 | 
			
		||||
		'---',
 | 
			
		||||
		'b--',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const test4: Map = {
 | 
			
		||||
	name: 'Test4',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'-w--b-',
 | 
			
		||||
		'-w--b-',
 | 
			
		||||
		'------',
 | 
			
		||||
		'-w--b-',
 | 
			
		||||
		'-w--b-',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)A1に打ってしまう
 | 
			
		||||
export const test6: Map = {
 | 
			
		||||
	name: 'Test6',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--wwwww-',
 | 
			
		||||
		'wwwwwwww',
 | 
			
		||||
		'wbbbwbwb',
 | 
			
		||||
		'wbbbbwbb',
 | 
			
		||||
		'wbwbbwbb',
 | 
			
		||||
		'wwbwbbbb',
 | 
			
		||||
		'--wbbbbb',
 | 
			
		||||
		'-wwwww--',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)G7に打ってしまう
 | 
			
		||||
export const test7: Map = {
 | 
			
		||||
	name: 'Test7',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b--w----',
 | 
			
		||||
		'b-wwww--',
 | 
			
		||||
		'bwbwwwbb',
 | 
			
		||||
		'wbwwwwb-',
 | 
			
		||||
		'wwwwwww-',
 | 
			
		||||
		'-wwbbwwb',
 | 
			
		||||
		'--wwww--',
 | 
			
		||||
		'--wwww--',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 検証用: この盤面で藍(lv5)が黒で始めると何故か(?)A1に打ってしまう
 | 
			
		||||
export const test8: Map = {
 | 
			
		||||
	name: 'Test8',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-----w--',
 | 
			
		||||
		'w--www--',
 | 
			
		||||
		'wwwwww--',
 | 
			
		||||
		'bbbbwww-',
 | 
			
		||||
		'wwwwww--',
 | 
			
		||||
		'--www---',
 | 
			
		||||
		'--ww----',
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "misskey-reversi",
 | 
			
		||||
  "version": "0.0.5",
 | 
			
		||||
  "description": "Misskey reversi engine",
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "misskey"
 | 
			
		||||
  ],
 | 
			
		||||
  "author": "syuilo <i@syuilo.com>",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "repository": "https://github.com/misskey-dev/misskey.git",
 | 
			
		||||
  "bugs": "https://github.com/misskey-dev/misskey/issues",
 | 
			
		||||
  "main": "./built/core.js",
 | 
			
		||||
  "types": "./built/core.d.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "build": "tsc"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"compilerOptions": {
 | 
			
		||||
		"noEmitOnError": false,
 | 
			
		||||
		"noImplicitAny": false,
 | 
			
		||||
		"noImplicitReturns": true,
 | 
			
		||||
		"noFallthroughCasesInSwitch": true,
 | 
			
		||||
		"experimentalDecorators": true,
 | 
			
		||||
		"declaration": true,
 | 
			
		||||
		"sourceMap": false,
 | 
			
		||||
		"target": "es2017",
 | 
			
		||||
		"module": "commonjs",
 | 
			
		||||
		"removeComments": false,
 | 
			
		||||
		"noLib": false,
 | 
			
		||||
		"outDir": "./built",
 | 
			
		||||
		"rootDir": "./"
 | 
			
		||||
	},
 | 
			
		||||
	"compileOnSave": false,
 | 
			
		||||
	"include": [
 | 
			
		||||
		"./core.ts"
 | 
			
		||||
	]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -22,8 +22,6 @@ import { packedFederationInstanceSchema } from '@/models/repositories/federation
 | 
			
		|||
import { packedQueueCountSchema } from '@/models/repositories/queue';
 | 
			
		||||
import { packedGalleryPostSchema } from '@/models/repositories/gallery-post';
 | 
			
		||||
import { packedEmojiSchema } from '@/models/repositories/emoji';
 | 
			
		||||
import { packedReversiGameSchema } from '@/models/repositories/games/reversi/game';
 | 
			
		||||
import { packedReversiMatchingSchema } from '@/models/repositories/games/reversi/matching';
 | 
			
		||||
 | 
			
		||||
export const refs = {
 | 
			
		||||
	User: packedUserSchema,
 | 
			
		||||
| 
						 | 
				
			
			@ -49,8 +47,6 @@ export const refs = {
 | 
			
		|||
	FederationInstance: packedFederationInstanceSchema,
 | 
			
		||||
	GalleryPost: packedGalleryPostSchema,
 | 
			
		||||
	Emoji: packedEmojiSchema,
 | 
			
		||||
	ReversiGame: packedReversiGameSchema,
 | 
			
		||||
	ReversiMatching: packedReversiMatchingSchema,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Packed<x extends keyof typeof refs> = ObjType<(typeof refs[x])['properties']>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,133 +0,0 @@
 | 
			
		|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { User } from '../../user';
 | 
			
		||||
import { id } from '../../../id';
 | 
			
		||||
 | 
			
		||||
@Entity()
 | 
			
		||||
export class ReversiGame {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('timestamp with time zone', {
 | 
			
		||||
		comment: 'The created date of the ReversiGame.',
 | 
			
		||||
	})
 | 
			
		||||
	public createdAt: Date;
 | 
			
		||||
 | 
			
		||||
	@Column('timestamp with time zone', {
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		comment: 'The started date of the ReversiGame.',
 | 
			
		||||
	})
 | 
			
		||||
	public startedAt: Date | null;
 | 
			
		||||
 | 
			
		||||
	@Column(id())
 | 
			
		||||
	public user1Id: User['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => User, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public user1: User | null;
 | 
			
		||||
 | 
			
		||||
	@Column(id())
 | 
			
		||||
	public user2Id: User['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => User, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public user2: User | null;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public user1Accepted: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public user2Accepted: boolean;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * どちらのプレイヤーが先行(黒)か
 | 
			
		||||
	 * 1 ... user1
 | 
			
		||||
	 * 2 ... user2
 | 
			
		||||
	 */
 | 
			
		||||
	@Column('integer', {
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public black: number | null;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public isStarted: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public isEnded: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public winnerId: User['id'] | null;
 | 
			
		||||
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public surrendered: User['id'] | null;
 | 
			
		||||
 | 
			
		||||
	@Column('jsonb', {
 | 
			
		||||
		default: [],
 | 
			
		||||
	})
 | 
			
		||||
	public logs: {
 | 
			
		||||
		at: Date;
 | 
			
		||||
		color: boolean;
 | 
			
		||||
		pos: number;
 | 
			
		||||
	}[];
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		array: true, length: 64,
 | 
			
		||||
	})
 | 
			
		||||
	public map: string[];
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 32,
 | 
			
		||||
	})
 | 
			
		||||
	public bw: string;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public isLlotheo: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public canPutEverywhere: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public loopedBoard: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('jsonb', {
 | 
			
		||||
		nullable: true, default: null,
 | 
			
		||||
	})
 | 
			
		||||
	public form1: any | null;
 | 
			
		||||
 | 
			
		||||
	@Column('jsonb', {
 | 
			
		||||
		nullable: true, default: null,
 | 
			
		||||
	})
 | 
			
		||||
	public form2: any | null;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ログのposを文字列としてすべて連結したもののCRC32値
 | 
			
		||||
	 */
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 32, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public crc32: string | null;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { User } from '../../user';
 | 
			
		||||
import { id } from '../../../id';
 | 
			
		||||
 | 
			
		||||
@Entity()
 | 
			
		||||
export class ReversiMatching {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('timestamp with time zone', {
 | 
			
		||||
		comment: 'The created date of the ReversiMatching.',
 | 
			
		||||
	})
 | 
			
		||||
	public createdAt: Date;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column(id())
 | 
			
		||||
	public parentId: User['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => User, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public parent: User | null;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column(id())
 | 
			
		||||
	public childId: User['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => User, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public child: User | null;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +18,6 @@ import { AccessToken } from './entities/access-token';
 | 
			
		|||
import { UserNotePining } from './entities/user-note-pining';
 | 
			
		||||
import { SigninRepository } from './repositories/signin';
 | 
			
		||||
import { MessagingMessageRepository } from './repositories/messaging-message';
 | 
			
		||||
import { ReversiGameRepository } from './repositories/games/reversi/game';
 | 
			
		||||
import { UserListRepository } from './repositories/user-list';
 | 
			
		||||
import { UserListJoining } from './entities/user-list-joining';
 | 
			
		||||
import { UserGroupRepository } from './repositories/user-group';
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +29,6 @@ import { BlockingRepository } from './repositories/blocking';
 | 
			
		|||
import { NoteReactionRepository } from './repositories/note-reaction';
 | 
			
		||||
import { NotificationRepository } from './repositories/notification';
 | 
			
		||||
import { NoteFavoriteRepository } from './repositories/note-favorite';
 | 
			
		||||
import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
 | 
			
		||||
import { UserPublickey } from './entities/user-publickey';
 | 
			
		||||
import { UserKeypair } from './entities/user-keypair';
 | 
			
		||||
import { AppRepository } from './repositories/app';
 | 
			
		||||
| 
						 | 
				
			
			@ -107,8 +105,6 @@ export const AuthSessions = getCustomRepository(AuthSessionRepository);
 | 
			
		|||
export const AccessTokens = getRepository(AccessToken);
 | 
			
		||||
export const Signins = getCustomRepository(SigninRepository);
 | 
			
		||||
export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
 | 
			
		||||
export const ReversiGames = getCustomRepository(ReversiGameRepository);
 | 
			
		||||
export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
 | 
			
		||||
export const Pages = getCustomRepository(PageRepository);
 | 
			
		||||
export const PageLikes = getCustomRepository(PageLikeRepository);
 | 
			
		||||
export const GalleryPosts = getCustomRepository(GalleryPostRepository);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,191 +0,0 @@
 | 
			
		|||
import { User } from '@/models/entities/user';
 | 
			
		||||
import { EntityRepository, Repository } from 'typeorm';
 | 
			
		||||
import { Users } from '../../../index';
 | 
			
		||||
import { ReversiGame } from '@/models/entities/games/reversi/game';
 | 
			
		||||
import { Packed } from '@/misc/schema';
 | 
			
		||||
 | 
			
		||||
@EntityRepository(ReversiGame)
 | 
			
		||||
export class ReversiGameRepository extends Repository<ReversiGame> {
 | 
			
		||||
	public async pack(
 | 
			
		||||
		src: ReversiGame['id'] | ReversiGame,
 | 
			
		||||
		me?: { id: User['id'] } | null | undefined,
 | 
			
		||||
		options?: {
 | 
			
		||||
			detail?: boolean
 | 
			
		||||
		}
 | 
			
		||||
	): Promise<Packed<'ReversiGame'>> {
 | 
			
		||||
		const opts = Object.assign({
 | 
			
		||||
			detail: true,
 | 
			
		||||
		}, options);
 | 
			
		||||
 | 
			
		||||
		const game = typeof src === 'object' ? src : await this.findOneOrFail(src);
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			id: game.id,
 | 
			
		||||
			createdAt: game.createdAt.toISOString(),
 | 
			
		||||
			startedAt: game.startedAt && game.startedAt.toISOString(),
 | 
			
		||||
			isStarted: game.isStarted,
 | 
			
		||||
			isEnded: game.isEnded,
 | 
			
		||||
			form1: game.form1,
 | 
			
		||||
			form2: game.form2,
 | 
			
		||||
			user1Accepted: game.user1Accepted,
 | 
			
		||||
			user2Accepted: game.user2Accepted,
 | 
			
		||||
			user1Id: game.user1Id,
 | 
			
		||||
			user2Id: game.user2Id,
 | 
			
		||||
			user1: await Users.pack(game.user1Id, me),
 | 
			
		||||
			user2: await Users.pack(game.user2Id, me),
 | 
			
		||||
			winnerId: game.winnerId,
 | 
			
		||||
			winner: game.winnerId ? await Users.pack(game.winnerId, me) : null,
 | 
			
		||||
			surrendered: game.surrendered,
 | 
			
		||||
			black: game.black,
 | 
			
		||||
			bw: game.bw,
 | 
			
		||||
			isLlotheo: game.isLlotheo,
 | 
			
		||||
			canPutEverywhere: game.canPutEverywhere,
 | 
			
		||||
			loopedBoard: game.loopedBoard,
 | 
			
		||||
			...(opts.detail ? {
 | 
			
		||||
				logs: game.logs.map(log => ({
 | 
			
		||||
					at: log.at.toISOString(),
 | 
			
		||||
					color: log.color,
 | 
			
		||||
					pos: log.pos,
 | 
			
		||||
				})),
 | 
			
		||||
				map: game.map,
 | 
			
		||||
			} : {}),
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const packedReversiGameSchema = {
 | 
			
		||||
	type: 'object' as const,
 | 
			
		||||
	optional: false as const, nullable: false as const,
 | 
			
		||||
	properties: {
 | 
			
		||||
		id: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
			example: 'xxxxxxxxxx',
 | 
			
		||||
		},
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			format: 'date-time',
 | 
			
		||||
		},
 | 
			
		||||
		startedAt: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: true as const,
 | 
			
		||||
			format: 'date-time',
 | 
			
		||||
		},
 | 
			
		||||
		isStarted: {
 | 
			
		||||
			type: 'boolean' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
		},
 | 
			
		||||
		isEnded: {
 | 
			
		||||
			type: 'boolean' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
		},
 | 
			
		||||
		form1: {
 | 
			
		||||
			type: 'any' as const,
 | 
			
		||||
			optional: false as const, nullable: true as const,
 | 
			
		||||
		},
 | 
			
		||||
		form2: {
 | 
			
		||||
			type: 'any' as const,
 | 
			
		||||
			optional: false as const, nullable: true as const,
 | 
			
		||||
		},
 | 
			
		||||
		user1Accepted: {
 | 
			
		||||
			type: 'boolean' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
		},
 | 
			
		||||
		user2Accepted: {
 | 
			
		||||
			type: 'boolean' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
		},
 | 
			
		||||
		user1Id: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
			example: 'xxxxxxxxxx',
 | 
			
		||||
		},
 | 
			
		||||
		user2Id: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
			example: 'xxxxxxxxxx',
 | 
			
		||||
		},
 | 
			
		||||
		user1: {
 | 
			
		||||
			type: 'object' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			ref: 'User' as const,
 | 
			
		||||
		},
 | 
			
		||||
		user2: {
 | 
			
		||||
			type: 'object' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			ref: 'User' as const,
 | 
			
		||||
		},
 | 
			
		||||
		winnerId: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: true as const,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
			example: 'xxxxxxxxxx',
 | 
			
		||||
		},
 | 
			
		||||
		winner: {
 | 
			
		||||
			type: 'object' as const,
 | 
			
		||||
			optional: false as const, nullable: true as const,
 | 
			
		||||
			ref: 'User' as const,
 | 
			
		||||
		},
 | 
			
		||||
		surrendered: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: true as const,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
			example: 'xxxxxxxxxx',
 | 
			
		||||
		},
 | 
			
		||||
		black: {
 | 
			
		||||
			type: 'number' as const,
 | 
			
		||||
			optional: false as const, nullable: true as const,
 | 
			
		||||
		},
 | 
			
		||||
		bw: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
		},
 | 
			
		||||
		isLlotheo: {
 | 
			
		||||
			type: 'boolean' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
		},
 | 
			
		||||
		canPutEverywhere: {
 | 
			
		||||
			type: 'boolean' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
		},
 | 
			
		||||
		loopedBoard: {
 | 
			
		||||
			type: 'boolean' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
		},
 | 
			
		||||
		logs: {
 | 
			
		||||
			type: 'array' as const,
 | 
			
		||||
			optional: true as const, nullable: false as const,
 | 
			
		||||
			items: {
 | 
			
		||||
				type: 'object' as const,
 | 
			
		||||
				optional: true as const, nullable: false as const,
 | 
			
		||||
				properties: {
 | 
			
		||||
					at: {
 | 
			
		||||
						type: 'string' as const,
 | 
			
		||||
						optional: false as const, nullable: false as const,
 | 
			
		||||
						format: 'date-time',
 | 
			
		||||
					},
 | 
			
		||||
					color: {
 | 
			
		||||
						type: 'boolean' as const,
 | 
			
		||||
						optional: false as const, nullable: false as const,
 | 
			
		||||
					},
 | 
			
		||||
					pos: {
 | 
			
		||||
						type: 'number' as const,
 | 
			
		||||
						optional: false as const, nullable: false as const,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		map: {
 | 
			
		||||
			type: 'array' as const,
 | 
			
		||||
			optional: true as const, nullable: false as const,
 | 
			
		||||
			items: {
 | 
			
		||||
				type: 'string' as const,
 | 
			
		||||
				optional: false as const, nullable: false as const,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,69 +0,0 @@
 | 
			
		|||
import { EntityRepository, Repository } from 'typeorm';
 | 
			
		||||
import { ReversiMatching } from '@/models/entities/games/reversi/matching';
 | 
			
		||||
import { Users } from '../../../index';
 | 
			
		||||
import { awaitAll } from '@/prelude/await-all';
 | 
			
		||||
import { User } from '@/models/entities/user';
 | 
			
		||||
import { Packed } from '@/misc/schema';
 | 
			
		||||
 | 
			
		||||
@EntityRepository(ReversiMatching)
 | 
			
		||||
export class ReversiMatchingRepository extends Repository<ReversiMatching> {
 | 
			
		||||
	public async pack(
 | 
			
		||||
		src: ReversiMatching['id'] | ReversiMatching,
 | 
			
		||||
		me: { id: User['id'] }
 | 
			
		||||
	): Promise<Packed<'ReversiMatching'>> {
 | 
			
		||||
		const matching = typeof src === 'object' ? src : await this.findOneOrFail(src);
 | 
			
		||||
 | 
			
		||||
		return await awaitAll({
 | 
			
		||||
			id: matching.id,
 | 
			
		||||
			createdAt: matching.createdAt.toISOString(),
 | 
			
		||||
			parentId: matching.parentId,
 | 
			
		||||
			parent: Users.pack(matching.parentId, me, {
 | 
			
		||||
				detail: true,
 | 
			
		||||
			}),
 | 
			
		||||
			childId: matching.childId,
 | 
			
		||||
			child: Users.pack(matching.childId, me, {
 | 
			
		||||
				detail: true,
 | 
			
		||||
			}),
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const packedReversiMatchingSchema = {
 | 
			
		||||
	type: 'object' as const,
 | 
			
		||||
	optional: false as const, nullable: false as const,
 | 
			
		||||
	properties: {
 | 
			
		||||
		id: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
			example: 'xxxxxxxxxx',
 | 
			
		||||
		},
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			format: 'date-time',
 | 
			
		||||
		},
 | 
			
		||||
		parentId: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
			example: 'xxxxxxxxxx',
 | 
			
		||||
		},
 | 
			
		||||
		parent: {
 | 
			
		||||
			type: 'object' as const,
 | 
			
		||||
			optional: false as const, nullable: true as const,
 | 
			
		||||
			ref: 'User' as const,
 | 
			
		||||
		},
 | 
			
		||||
		childId: {
 | 
			
		||||
			type: 'string' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			format: 'id',
 | 
			
		||||
			example: 'xxxxxxxxxx',
 | 
			
		||||
		},
 | 
			
		||||
		child: {
 | 
			
		||||
			type: 'object' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			ref: 'User' as const,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,157 +0,0 @@
 | 
			
		|||
import $ from 'cafy';
 | 
			
		||||
import { ID } from '@/misc/cafy-id';
 | 
			
		||||
import define from '../../../define';
 | 
			
		||||
import { ReversiGames } from '@/models/index';
 | 
			
		||||
import { makePaginationQuery } from '../../../common/make-pagination-query';
 | 
			
		||||
import { Brackets } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['games'],
 | 
			
		||||
 | 
			
		||||
	params: {
 | 
			
		||||
		limit: {
 | 
			
		||||
			validator: $.optional.num.range(1, 100),
 | 
			
		||||
			default: 10,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		sinceId: {
 | 
			
		||||
			validator: $.optional.type(ID),
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		untilId: {
 | 
			
		||||
			validator: $.optional.type(ID),
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		my: {
 | 
			
		||||
			validator: $.optional.bool,
 | 
			
		||||
			default: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array' as const,
 | 
			
		||||
		optional: false as const, nullable: false as const,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			properties: {
 | 
			
		||||
				id: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				createdAt: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'date-time',
 | 
			
		||||
				},
 | 
			
		||||
				startedAt: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'date-time',
 | 
			
		||||
				},
 | 
			
		||||
				isStarted: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				isEnded: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				form1: {
 | 
			
		||||
					type: 'any' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
				},
 | 
			
		||||
				form2: {
 | 
			
		||||
					type: 'any' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
				},
 | 
			
		||||
				user1Accepted: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					default: false,
 | 
			
		||||
				},
 | 
			
		||||
				user2Accepted: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					default: false,
 | 
			
		||||
				},
 | 
			
		||||
				user1Id: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				user2Id: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				user1: {
 | 
			
		||||
					type: 'object' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					ref: 'User',
 | 
			
		||||
				},
 | 
			
		||||
				user2: {
 | 
			
		||||
					type: 'object' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					ref: 'User',
 | 
			
		||||
				},
 | 
			
		||||
				winnerId: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				winner: {
 | 
			
		||||
					type: 'object' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
					ref: 'User',
 | 
			
		||||
				},
 | 
			
		||||
				surrendered: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				black: {
 | 
			
		||||
					type: 'number' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
				},
 | 
			
		||||
				bw: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				isLlotheo: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				canPutEverywhere: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				loopedBoard: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
export default define(meta, async (ps, user) => {
 | 
			
		||||
	const query = makePaginationQuery(ReversiGames.createQueryBuilder('game'), ps.sinceId, ps.untilId)
 | 
			
		||||
		.andWhere('game.isStarted = TRUE');
 | 
			
		||||
 | 
			
		||||
	if (ps.my && user) {
 | 
			
		||||
		query.andWhere(new Brackets(qb => { qb
 | 
			
		||||
			.where('game.user1Id = :userId', { userId: user.id })
 | 
			
		||||
			.orWhere('game.user2Id = :userId', { userId: user.id });
 | 
			
		||||
		}));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Fetch games
 | 
			
		||||
	const games = await query.take(ps.limit!).getMany();
 | 
			
		||||
 | 
			
		||||
	return await Promise.all(games.map((g) => ReversiGames.pack(g, user, {
 | 
			
		||||
		detail: false,
 | 
			
		||||
	})));
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,169 +0,0 @@
 | 
			
		|||
import $ from 'cafy';
 | 
			
		||||
import { ID } from '@/misc/cafy-id';
 | 
			
		||||
import Reversi from '../../../../../../games/reversi/core';
 | 
			
		||||
import define from '../../../../define';
 | 
			
		||||
import { ApiError } from '../../../../error';
 | 
			
		||||
import { ReversiGames } from '@/models/index';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['games'],
 | 
			
		||||
 | 
			
		||||
	params: {
 | 
			
		||||
		gameId: {
 | 
			
		||||
			validator: $.type(ID),
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchGame: {
 | 
			
		||||
			message: 'No such game.',
 | 
			
		||||
			code: 'NO_SUCH_GAME',
 | 
			
		||||
			id: 'f13a03db-fae1-46c9-87f3-43c8165419e1',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array' as const,
 | 
			
		||||
		optional: false as const, nullable: false as const,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			properties: {
 | 
			
		||||
				id: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				createdAt: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'date-time',
 | 
			
		||||
				},
 | 
			
		||||
				startedAt: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'date-time',
 | 
			
		||||
				},
 | 
			
		||||
				isStarted: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				isEnded: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				form1: {
 | 
			
		||||
					type: 'any' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
				},
 | 
			
		||||
				form2: {
 | 
			
		||||
					type: 'any' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
				},
 | 
			
		||||
				user1Accepted: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					default: false,
 | 
			
		||||
				},
 | 
			
		||||
				user2Accepted: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					default: false,
 | 
			
		||||
				},
 | 
			
		||||
				user1Id: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				user2Id: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				user1: {
 | 
			
		||||
					type: 'object' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					ref: 'User',
 | 
			
		||||
				},
 | 
			
		||||
				user2: {
 | 
			
		||||
					type: 'object' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					ref: 'User',
 | 
			
		||||
				},
 | 
			
		||||
				winnerId: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				winner: {
 | 
			
		||||
					type: 'object' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
					ref: 'User',
 | 
			
		||||
				},
 | 
			
		||||
				surrendered: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				black: {
 | 
			
		||||
					type: 'number' as const,
 | 
			
		||||
					optional: false as const, nullable: true as const,
 | 
			
		||||
				},
 | 
			
		||||
				bw: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				isLlotheo: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				canPutEverywhere: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				loopedBoard: {
 | 
			
		||||
					type: 'boolean' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
				board: {
 | 
			
		||||
					type: 'array' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					items: {
 | 
			
		||||
						type: 'any' as const,
 | 
			
		||||
						optional: false as const, nullable: false as const,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				turn: {
 | 
			
		||||
					type: 'any' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
export default define(meta, async (ps, user) => {
 | 
			
		||||
	const game = await ReversiGames.findOne(ps.gameId);
 | 
			
		||||
 | 
			
		||||
	if (game == null) {
 | 
			
		||||
		throw new ApiError(meta.errors.noSuchGame);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const o = new Reversi(game.map, {
 | 
			
		||||
		isLlotheo: game.isLlotheo,
 | 
			
		||||
		canPutEverywhere: game.canPutEverywhere,
 | 
			
		||||
		loopedBoard: game.loopedBoard,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	for (const log of game.logs) {
 | 
			
		||||
		o.put(log.color, log.pos);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const packed = await ReversiGames.pack(game, user);
 | 
			
		||||
 | 
			
		||||
	return Object.assign({
 | 
			
		||||
		board: o.board,
 | 
			
		||||
		turn: o.turn,
 | 
			
		||||
	}, packed);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,68 +0,0 @@
 | 
			
		|||
import $ from 'cafy';
 | 
			
		||||
import { ID } from '@/misc/cafy-id';
 | 
			
		||||
import { publishReversiGameStream } from '@/services/stream';
 | 
			
		||||
import define from '../../../../define';
 | 
			
		||||
import { ApiError } from '../../../../error';
 | 
			
		||||
import { ReversiGames } from '@/models/index';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['games'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true as const,
 | 
			
		||||
 | 
			
		||||
	params: {
 | 
			
		||||
		gameId: {
 | 
			
		||||
			validator: $.type(ID),
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchGame: {
 | 
			
		||||
			message: 'No such game.',
 | 
			
		||||
			code: 'NO_SUCH_GAME',
 | 
			
		||||
			id: 'ace0b11f-e0a6-4076-a30d-e8284c81b2df',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		alreadyEnded: {
 | 
			
		||||
			message: 'That game has already ended.',
 | 
			
		||||
			code: 'ALREADY_ENDED',
 | 
			
		||||
			id: '6c2ad4a6-cbf1-4a5b-b187-b772826cfc6d',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		accessDenied: {
 | 
			
		||||
			message: 'Access denied.',
 | 
			
		||||
			code: 'ACCESS_DENIED',
 | 
			
		||||
			id: '6e04164b-a992-4c93-8489-2123069973e1',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
export default define(meta, async (ps, user) => {
 | 
			
		||||
	const game = await ReversiGames.findOne(ps.gameId);
 | 
			
		||||
 | 
			
		||||
	if (game == null) {
 | 
			
		||||
		throw new ApiError(meta.errors.noSuchGame);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (game.isEnded) {
 | 
			
		||||
		throw new ApiError(meta.errors.alreadyEnded);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) {
 | 
			
		||||
		throw new ApiError(meta.errors.accessDenied);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id;
 | 
			
		||||
 | 
			
		||||
	await ReversiGames.update(game.id, {
 | 
			
		||||
		surrendered: user.id,
 | 
			
		||||
		isEnded: true,
 | 
			
		||||
		winnerId: winnerId,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	publishReversiGameStream(game.id, 'ended', {
 | 
			
		||||
		winnerId: winnerId,
 | 
			
		||||
		game: await ReversiGames.pack(game.id, user),
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,59 +0,0 @@
 | 
			
		|||
import define from '../../../define';
 | 
			
		||||
import { ReversiMatchings } from '@/models/index';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['games'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true as const,
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array' as const,
 | 
			
		||||
		optional: false as const, nullable: false as const,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object' as const,
 | 
			
		||||
			optional: false as const, nullable: false as const,
 | 
			
		||||
			properties: {
 | 
			
		||||
				id: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				createdAt: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'date-time',
 | 
			
		||||
				},
 | 
			
		||||
				parentId: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				parent: {
 | 
			
		||||
					type: 'object' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					ref: 'User',
 | 
			
		||||
				},
 | 
			
		||||
				childId: {
 | 
			
		||||
					type: 'string' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				child: {
 | 
			
		||||
					type: 'object' as const,
 | 
			
		||||
					optional: false as const, nullable: false as const,
 | 
			
		||||
					ref: 'User',
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
export default define(meta, async (ps, user) => {
 | 
			
		||||
	// Find session
 | 
			
		||||
	const invitations = await ReversiMatchings.find({
 | 
			
		||||
		childId: user.id,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	return await Promise.all(invitations.map((i) => ReversiMatchings.pack(i, user)));
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,109 +0,0 @@
 | 
			
		|||
import $ from 'cafy';
 | 
			
		||||
import { ID } from '@/misc/cafy-id';
 | 
			
		||||
import { publishMainStream, publishReversiStream } from '@/services/stream';
 | 
			
		||||
import { eighteight } from '../../../../../games/reversi/maps';
 | 
			
		||||
import define from '../../../define';
 | 
			
		||||
import { ApiError } from '../../../error';
 | 
			
		||||
import { getUser } from '../../../common/getters';
 | 
			
		||||
import { genId } from '@/misc/gen-id';
 | 
			
		||||
import { ReversiMatchings, ReversiGames } from '@/models/index';
 | 
			
		||||
import { ReversiGame } from '@/models/entities/games/reversi/game';
 | 
			
		||||
import { ReversiMatching } from '@/models/entities/games/reversi/matching';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['games'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true as const,
 | 
			
		||||
 | 
			
		||||
	params: {
 | 
			
		||||
		userId: {
 | 
			
		||||
			validator: $.type(ID),
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchUser: {
 | 
			
		||||
			message: 'No such user.',
 | 
			
		||||
			code: 'NO_SUCH_USER',
 | 
			
		||||
			id: '0b4f0559-b484-4e31-9581-3f73cee89b28',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		isYourself: {
 | 
			
		||||
			message: 'Target user is yourself.',
 | 
			
		||||
			code: 'TARGET_IS_YOURSELF',
 | 
			
		||||
			id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
export default define(meta, async (ps, user) => {
 | 
			
		||||
	// Myself
 | 
			
		||||
	if (ps.userId === user.id) {
 | 
			
		||||
		throw new ApiError(meta.errors.isYourself);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Find session
 | 
			
		||||
	const exist = await ReversiMatchings.findOne({
 | 
			
		||||
		parentId: ps.userId,
 | 
			
		||||
		childId: user.id,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (exist) {
 | 
			
		||||
		// Destroy session
 | 
			
		||||
		ReversiMatchings.delete(exist.id);
 | 
			
		||||
 | 
			
		||||
		// Create game
 | 
			
		||||
		const game = await ReversiGames.save({
 | 
			
		||||
			id: genId(),
 | 
			
		||||
			createdAt: new Date(),
 | 
			
		||||
			user1Id: exist.parentId,
 | 
			
		||||
			user2Id: user.id,
 | 
			
		||||
			user1Accepted: false,
 | 
			
		||||
			user2Accepted: false,
 | 
			
		||||
			isStarted: false,
 | 
			
		||||
			isEnded: false,
 | 
			
		||||
			logs: [],
 | 
			
		||||
			map: eighteight.data,
 | 
			
		||||
			bw: 'random',
 | 
			
		||||
			isLlotheo: false,
 | 
			
		||||
		} as Partial<ReversiGame>);
 | 
			
		||||
 | 
			
		||||
		publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, { id: exist.parentId }));
 | 
			
		||||
 | 
			
		||||
		const other = await ReversiMatchings.count({
 | 
			
		||||
			childId: user.id,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (other == 0) {
 | 
			
		||||
			publishMainStream(user.id, 'reversiNoInvites');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return await ReversiGames.pack(game, user);
 | 
			
		||||
	} else {
 | 
			
		||||
		// Fetch child
 | 
			
		||||
		const child = await getUser(ps.userId).catch(e => {
 | 
			
		||||
			if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
 | 
			
		||||
			throw e;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// 以前のセッションはすべて削除しておく
 | 
			
		||||
		await ReversiMatchings.delete({
 | 
			
		||||
			parentId: user.id,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// セッションを作成
 | 
			
		||||
		const matching = await ReversiMatchings.save({
 | 
			
		||||
			id: genId(),
 | 
			
		||||
			createdAt: new Date(),
 | 
			
		||||
			parentId: user.id,
 | 
			
		||||
			childId: child.id,
 | 
			
		||||
		} as ReversiMatching);
 | 
			
		||||
 | 
			
		||||
		const packed = await ReversiMatchings.pack(matching, child);
 | 
			
		||||
		publishReversiStream(child.id, 'invited', packed);
 | 
			
		||||
		publishMainStream(child.id, 'reversiInvited', packed);
 | 
			
		||||
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
import define from '../../../../define';
 | 
			
		||||
import { ReversiMatchings } from '@/models/index';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['games'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true as const,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
export default define(meta, async (ps, user) => {
 | 
			
		||||
	await ReversiMatchings.delete({
 | 
			
		||||
		parentId: user.id,
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ import $ from 'cafy';
 | 
			
		|||
import define from '../../define';
 | 
			
		||||
import { ApiError } from '../../error';
 | 
			
		||||
import { ID } from '@/misc/cafy-id';
 | 
			
		||||
import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, ReversiGames, Users } from '@/models/index';
 | 
			
		||||
import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['users'],
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +50,6 @@ export default define(meta, async (ps, me) => {
 | 
			
		|||
		pageLikedCount,
 | 
			
		||||
		driveFilesCount,
 | 
			
		||||
		driveUsage,
 | 
			
		||||
		reversiCount,
 | 
			
		||||
	] = await Promise.all([
 | 
			
		||||
		Notes.createQueryBuilder('note')
 | 
			
		||||
			.where('note.userId = :userId', { userId: user.id })
 | 
			
		||||
| 
						 | 
				
			
			@ -113,10 +112,6 @@ export default define(meta, async (ps, me) => {
 | 
			
		|||
			.where('file.userId = :userId', { userId: user.id })
 | 
			
		||||
			.getCount(),
 | 
			
		||||
		DriveFiles.calcDriveUsageOf(user),
 | 
			
		||||
		ReversiGames.createQueryBuilder('game')
 | 
			
		||||
			.where('game.user1Id = :userId', { userId: user.id })
 | 
			
		||||
			.orWhere('game.user2Id = :userId', { userId: user.id })
 | 
			
		||||
			.getCount(),
 | 
			
		||||
	]);
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
| 
						 | 
				
			
			@ -140,6 +135,5 @@ export default define(meta, async (ps, me) => {
 | 
			
		|||
		pageLikedCount,
 | 
			
		||||
		driveFilesCount,
 | 
			
		||||
		driveUsage,
 | 
			
		||||
		reversiCount,
 | 
			
		||||
	};
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,372 +0,0 @@
 | 
			
		|||
import autobind from 'autobind-decorator';
 | 
			
		||||
import * as CRC32 from 'crc-32';
 | 
			
		||||
import { publishReversiGameStream } from '@/services/stream';
 | 
			
		||||
import Reversi from '../../../../../games/reversi/core';
 | 
			
		||||
import * as maps from '../../../../../games/reversi/maps';
 | 
			
		||||
import Channel from '../../channel';
 | 
			
		||||
import { ReversiGame } from '@/models/entities/games/reversi/game';
 | 
			
		||||
import { ReversiGames, Users } from '@/models/index';
 | 
			
		||||
import { User } from '@/models/entities/user';
 | 
			
		||||
 | 
			
		||||
export default class extends Channel {
 | 
			
		||||
	public readonly chName = 'gamesReversiGame';
 | 
			
		||||
	public static shouldShare = false;
 | 
			
		||||
	public static requireCredential = false;
 | 
			
		||||
 | 
			
		||||
	private gameId: ReversiGame['id'] | null = null;
 | 
			
		||||
	private watchers: Record<User['id'], Date> = {};
 | 
			
		||||
	private emitWatchersIntervalId: ReturnType<typeof setInterval>;
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public async init(params: any) {
 | 
			
		||||
		this.gameId = params.gameId;
 | 
			
		||||
 | 
			
		||||
		// Subscribe game stream
 | 
			
		||||
		this.subscriber.on(`reversiGameStream:${this.gameId}`, this.onEvent);
 | 
			
		||||
		this.emitWatchersIntervalId = setInterval(this.emitWatchers, 5000);
 | 
			
		||||
 | 
			
		||||
		const game = await ReversiGames.findOne(this.gameId!);
 | 
			
		||||
		if (game == null) throw new Error('game not found');
 | 
			
		||||
 | 
			
		||||
		// 観戦者イベント
 | 
			
		||||
		this.watch(game);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private onEvent(data: any) {
 | 
			
		||||
		if (data.type === 'watching') {
 | 
			
		||||
			const id = data.body;
 | 
			
		||||
			this.watchers[id] = new Date();
 | 
			
		||||
		} else {
 | 
			
		||||
			this.send(data);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private async emitWatchers() {
 | 
			
		||||
		const now = new Date();
 | 
			
		||||
 | 
			
		||||
		// Remove not watching users
 | 
			
		||||
		for (const [userId, date] of Object.entries(this.watchers)) {
 | 
			
		||||
			if (now.getTime() - date.getTime() > 5000) delete this.watchers[userId];
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const users = await Users.packMany(Object.keys(this.watchers), null, { detail: false });
 | 
			
		||||
 | 
			
		||||
		this.send({
 | 
			
		||||
			type: 'watchers',
 | 
			
		||||
			body: users,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public dispose() {
 | 
			
		||||
		// Unsubscribe events
 | 
			
		||||
		this.subscriber.off(`reversiGameStream:${this.gameId}`, this.onEvent);
 | 
			
		||||
		clearInterval(this.emitWatchersIntervalId);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public onMessage(type: string, body: any) {
 | 
			
		||||
		switch (type) {
 | 
			
		||||
			case 'accept': this.accept(true); break;
 | 
			
		||||
			case 'cancelAccept': this.accept(false); break;
 | 
			
		||||
			case 'updateSettings': this.updateSettings(body.key, body.value); break;
 | 
			
		||||
			case 'initForm': this.initForm(body); break;
 | 
			
		||||
			case 'updateForm': this.updateForm(body.id, body.value); break;
 | 
			
		||||
			case 'message': this.message(body); break;
 | 
			
		||||
			case 'set': this.set(body.pos); break;
 | 
			
		||||
			case 'check': this.check(body.crc32); break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private async updateSettings(key: string, value: any) {
 | 
			
		||||
		if (this.user == null) return;
 | 
			
		||||
 | 
			
		||||
		const game = await ReversiGames.findOne(this.gameId!);
 | 
			
		||||
		if (game == null) throw new Error('game not found');
 | 
			
		||||
 | 
			
		||||
		if (game.isStarted) return;
 | 
			
		||||
		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
 | 
			
		||||
		if ((game.user1Id === this.user.id) && game.user1Accepted) return;
 | 
			
		||||
		if ((game.user2Id === this.user.id) && game.user2Accepted) return;
 | 
			
		||||
 | 
			
		||||
		if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return;
 | 
			
		||||
 | 
			
		||||
		await ReversiGames.update(this.gameId!, {
 | 
			
		||||
			[key]: value,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		publishReversiGameStream(this.gameId!, 'updateSettings', {
 | 
			
		||||
			key: key,
 | 
			
		||||
			value: value,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private async initForm(form: any) {
 | 
			
		||||
		if (this.user == null) return;
 | 
			
		||||
 | 
			
		||||
		const game = await ReversiGames.findOne(this.gameId!);
 | 
			
		||||
		if (game == null) throw new Error('game not found');
 | 
			
		||||
 | 
			
		||||
		if (game.isStarted) return;
 | 
			
		||||
		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
 | 
			
		||||
 | 
			
		||||
		const set = game.user1Id === this.user.id ? {
 | 
			
		||||
			form1: form,
 | 
			
		||||
		} : {
 | 
			
		||||
			form2: form,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		await ReversiGames.update(this.gameId!, set);
 | 
			
		||||
 | 
			
		||||
		publishReversiGameStream(this.gameId!, 'initForm', {
 | 
			
		||||
			userId: this.user.id,
 | 
			
		||||
			form,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private async updateForm(id: string, value: any) {
 | 
			
		||||
		if (this.user == null) return;
 | 
			
		||||
 | 
			
		||||
		const game = await ReversiGames.findOne(this.gameId!);
 | 
			
		||||
		if (game == null) throw new Error('game not found');
 | 
			
		||||
 | 
			
		||||
		if (game.isStarted) return;
 | 
			
		||||
		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
 | 
			
		||||
 | 
			
		||||
		const form = game.user1Id === this.user.id ? game.form2 : game.form1;
 | 
			
		||||
 | 
			
		||||
		const item = form.find((i: any) => i.id == id);
 | 
			
		||||
 | 
			
		||||
		if (item == null) return;
 | 
			
		||||
 | 
			
		||||
		item.value = value;
 | 
			
		||||
 | 
			
		||||
		const set = game.user1Id === this.user.id ? {
 | 
			
		||||
			form2: form,
 | 
			
		||||
		} : {
 | 
			
		||||
				form1: form,
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
		await ReversiGames.update(this.gameId!, set);
 | 
			
		||||
 | 
			
		||||
		publishReversiGameStream(this.gameId!, 'updateForm', {
 | 
			
		||||
			userId: this.user.id,
 | 
			
		||||
			id,
 | 
			
		||||
			value,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private async message(message: any) {
 | 
			
		||||
		if (this.user == null) return;
 | 
			
		||||
 | 
			
		||||
		message.id = Math.random();
 | 
			
		||||
		publishReversiGameStream(this.gameId!, 'message', {
 | 
			
		||||
			userId: this.user.id,
 | 
			
		||||
			message,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private async accept(accept: boolean) {
 | 
			
		||||
		if (this.user == null) return;
 | 
			
		||||
 | 
			
		||||
		const game = await ReversiGames.findOne(this.gameId!);
 | 
			
		||||
		if (game == null) throw new Error('game not found');
 | 
			
		||||
 | 
			
		||||
		if (game.isStarted) return;
 | 
			
		||||
 | 
			
		||||
		let bothAccepted = false;
 | 
			
		||||
 | 
			
		||||
		if (game.user1Id === this.user.id) {
 | 
			
		||||
			await ReversiGames.update(this.gameId!, {
 | 
			
		||||
				user1Accepted: accept,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			publishReversiGameStream(this.gameId!, 'changeAccepts', {
 | 
			
		||||
				user1: accept,
 | 
			
		||||
				user2: game.user2Accepted,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (accept && game.user2Accepted) bothAccepted = true;
 | 
			
		||||
		} else if (game.user2Id === this.user.id) {
 | 
			
		||||
			await ReversiGames.update(this.gameId!, {
 | 
			
		||||
				user2Accepted: accept,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			publishReversiGameStream(this.gameId!, 'changeAccepts', {
 | 
			
		||||
				user1: game.user1Accepted,
 | 
			
		||||
				user2: accept,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (accept && game.user1Accepted) bothAccepted = true;
 | 
			
		||||
		} else {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (bothAccepted) {
 | 
			
		||||
			// 3秒後、まだacceptされていたらゲーム開始
 | 
			
		||||
			setTimeout(async () => {
 | 
			
		||||
				const freshGame = await ReversiGames.findOne(this.gameId!);
 | 
			
		||||
				if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
 | 
			
		||||
				if (!freshGame.user1Accepted || !freshGame.user2Accepted) return;
 | 
			
		||||
 | 
			
		||||
				let bw: number;
 | 
			
		||||
				if (freshGame.bw == 'random') {
 | 
			
		||||
					bw = Math.random() > 0.5 ? 1 : 2;
 | 
			
		||||
				} else {
 | 
			
		||||
					bw = parseInt(freshGame.bw, 10);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				function getRandomMap() {
 | 
			
		||||
					const mapCount = Object.entries(maps).length;
 | 
			
		||||
					const rnd = Math.floor(Math.random() * mapCount);
 | 
			
		||||
					return Object.values(maps)[rnd].data;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const map = freshGame.map != null ? freshGame.map : getRandomMap();
 | 
			
		||||
 | 
			
		||||
				await ReversiGames.update(this.gameId!, {
 | 
			
		||||
					startedAt: new Date(),
 | 
			
		||||
					isStarted: true,
 | 
			
		||||
					black: bw,
 | 
			
		||||
					map: map,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
 | 
			
		||||
				const o = new Reversi(map, {
 | 
			
		||||
					isLlotheo: freshGame.isLlotheo,
 | 
			
		||||
					canPutEverywhere: freshGame.canPutEverywhere,
 | 
			
		||||
					loopedBoard: freshGame.loopedBoard,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (o.isEnded) {
 | 
			
		||||
					let winner;
 | 
			
		||||
					if (o.winner === true) {
 | 
			
		||||
						winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id;
 | 
			
		||||
					} else if (o.winner === false) {
 | 
			
		||||
						winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id;
 | 
			
		||||
					} else {
 | 
			
		||||
						winner = null;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					await ReversiGames.update(this.gameId!, {
 | 
			
		||||
						isEnded: true,
 | 
			
		||||
						winnerId: winner,
 | 
			
		||||
					});
 | 
			
		||||
 | 
			
		||||
					publishReversiGameStream(this.gameId!, 'ended', {
 | 
			
		||||
						winnerId: winner,
 | 
			
		||||
						game: await ReversiGames.pack(this.gameId!, this.user),
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
				//#endregion
 | 
			
		||||
 | 
			
		||||
				publishReversiGameStream(this.gameId!, 'started',
 | 
			
		||||
					await ReversiGames.pack(this.gameId!, this.user));
 | 
			
		||||
			}, 3000);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 石を打つ
 | 
			
		||||
	@autobind
 | 
			
		||||
	private async set(pos: number) {
 | 
			
		||||
		if (this.user == null) return;
 | 
			
		||||
 | 
			
		||||
		const game = await ReversiGames.findOne(this.gameId!);
 | 
			
		||||
		if (game == null) throw new Error('game not found');
 | 
			
		||||
 | 
			
		||||
		if (!game.isStarted) return;
 | 
			
		||||
		if (game.isEnded) return;
 | 
			
		||||
		if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return;
 | 
			
		||||
 | 
			
		||||
		const myColor =
 | 
			
		||||
			((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2)
 | 
			
		||||
				? true
 | 
			
		||||
				: false;
 | 
			
		||||
 | 
			
		||||
		const o = new Reversi(game.map, {
 | 
			
		||||
			isLlotheo: game.isLlotheo,
 | 
			
		||||
			canPutEverywhere: game.canPutEverywhere,
 | 
			
		||||
			loopedBoard: game.loopedBoard,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// 盤面の状態を再生
 | 
			
		||||
		for (const log of game.logs) {
 | 
			
		||||
			o.put(log.color, log.pos);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (o.turn !== myColor) return;
 | 
			
		||||
 | 
			
		||||
		if (!o.canPut(myColor, pos)) return;
 | 
			
		||||
		o.put(myColor, pos);
 | 
			
		||||
 | 
			
		||||
		let winner;
 | 
			
		||||
		if (o.isEnded) {
 | 
			
		||||
			if (o.winner === true) {
 | 
			
		||||
				winner = game.black == 1 ? game.user1Id : game.user2Id;
 | 
			
		||||
			} else if (o.winner === false) {
 | 
			
		||||
				winner = game.black == 1 ? game.user2Id : game.user1Id;
 | 
			
		||||
			} else {
 | 
			
		||||
				winner = null;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const log = {
 | 
			
		||||
			at: new Date(),
 | 
			
		||||
			color: myColor,
 | 
			
		||||
			pos,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString();
 | 
			
		||||
 | 
			
		||||
		game.logs.push(log);
 | 
			
		||||
 | 
			
		||||
		await ReversiGames.update(this.gameId!, {
 | 
			
		||||
			crc32,
 | 
			
		||||
			isEnded: o.isEnded,
 | 
			
		||||
			winnerId: winner,
 | 
			
		||||
			logs: game.logs,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		publishReversiGameStream(this.gameId!, 'set', Object.assign(log, {
 | 
			
		||||
			next: o.turn,
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		if (o.isEnded) {
 | 
			
		||||
			publishReversiGameStream(this.gameId!, 'ended', {
 | 
			
		||||
				winnerId: winner,
 | 
			
		||||
				game: await ReversiGames.pack(this.gameId!, this.user),
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private async check(crc32: string | number) {
 | 
			
		||||
		const game = await ReversiGames.findOne(this.gameId!);
 | 
			
		||||
		if (game == null) throw new Error('game not found');
 | 
			
		||||
 | 
			
		||||
		if (!game.isStarted) return;
 | 
			
		||||
 | 
			
		||||
		if (crc32.toString() !== game.crc32) {
 | 
			
		||||
			this.send('rescue', await ReversiGames.pack(game, this.user));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// ついでに観戦者イベントを発行
 | 
			
		||||
		this.watch(game);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	private watch(game: ReversiGame) {
 | 
			
		||||
		if (this.user != null) {
 | 
			
		||||
			if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) {
 | 
			
		||||
				publishReversiGameStream(this.gameId!, 'watching', this.user.id);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,34 +0,0 @@
 | 
			
		|||
import autobind from 'autobind-decorator';
 | 
			
		||||
import { publishMainStream } from '@/services/stream';
 | 
			
		||||
import Channel from '../../channel';
 | 
			
		||||
import { ReversiMatchings } from '@/models/index';
 | 
			
		||||
 | 
			
		||||
export default class extends Channel {
 | 
			
		||||
	public readonly chName = 'gamesReversi';
 | 
			
		||||
	public static shouldShare = true;
 | 
			
		||||
	public static requireCredential = true;
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public async init(params: any) {
 | 
			
		||||
		// Subscribe reversi stream
 | 
			
		||||
		this.subscriber.on(`reversiStream:${this.user!.id}`, data => {
 | 
			
		||||
			this.send(data);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@autobind
 | 
			
		||||
	public async onMessage(type: string, body: any) {
 | 
			
		||||
		switch (type) {
 | 
			
		||||
			case 'ping': {
 | 
			
		||||
				if (body.id == null) return;
 | 
			
		||||
				const matching = await ReversiMatchings.findOne({
 | 
			
		||||
					parentId: this.user!.id,
 | 
			
		||||
					childId: body.id,
 | 
			
		||||
				});
 | 
			
		||||
				if (matching == null) return;
 | 
			
		||||
				publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, { id: matching.childId }));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,8 +13,6 @@ import drive from './drive';
 | 
			
		|||
import hashtag from './hashtag';
 | 
			
		||||
import channel from './channel';
 | 
			
		||||
import admin from './admin';
 | 
			
		||||
import gamesReversi from './games/reversi';
 | 
			
		||||
import gamesReversiGame from './games/reversi-game';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
	main,
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +30,4 @@ export default {
 | 
			
		|||
	hashtag,
 | 
			
		||||
	channel,
 | 
			
		||||
	admin,
 | 
			
		||||
	gamesReversi,
 | 
			
		||||
	gamesReversiGame,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,6 @@ import { Emoji } from '@/models/entities/emoji';
 | 
			
		|||
import { UserList } from '@/models/entities/user-list';
 | 
			
		||||
import { MessagingMessage } from '@/models/entities/messaging-message';
 | 
			
		||||
import { UserGroup } from '@/models/entities/user-group';
 | 
			
		||||
import { ReversiGame } from '@/models/entities/games/reversi/game';
 | 
			
		||||
import { AbuseUserReport } from '@/models/entities/abuse-user-report';
 | 
			
		||||
import { Signin } from '@/models/entities/signin';
 | 
			
		||||
import { Page } from '@/models/entities/page';
 | 
			
		||||
| 
						 | 
				
			
			@ -77,8 +76,6 @@ export interface MainStreamTypes {
 | 
			
		|||
	readAllChannels: undefined;
 | 
			
		||||
	unreadChannel: Note['id'];
 | 
			
		||||
	myTokenRegenerated: undefined;
 | 
			
		||||
	reversiNoInvites: undefined;
 | 
			
		||||
	reversiInvited: Packed<'ReversiMatching'>;
 | 
			
		||||
	signin: Signin;
 | 
			
		||||
	registryUpdated: {
 | 
			
		||||
		scope?: string[];
 | 
			
		||||
| 
						 | 
				
			
			@ -158,47 +155,6 @@ export interface MessagingIndexStreamTypes {
 | 
			
		|||
	message: Packed<'MessagingMessage'>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReversiStreamTypes {
 | 
			
		||||
	matched: Packed<'ReversiGame'>;
 | 
			
		||||
	invited: Packed<'ReversiMatching'>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReversiGameStreamTypes {
 | 
			
		||||
	started: Packed<'ReversiGame'>;
 | 
			
		||||
	ended: {
 | 
			
		||||
		winnerId?: User['id'] | null,
 | 
			
		||||
		game: Packed<'ReversiGame'>;
 | 
			
		||||
	};
 | 
			
		||||
	updateSettings: {
 | 
			
		||||
		key: string;
 | 
			
		||||
		value: FIXME;
 | 
			
		||||
	};
 | 
			
		||||
	initForm: {
 | 
			
		||||
		userId: User['id'];
 | 
			
		||||
		form: FIXME;
 | 
			
		||||
	};
 | 
			
		||||
	updateForm: {
 | 
			
		||||
		userId: User['id'];
 | 
			
		||||
		id: string;
 | 
			
		||||
		value: FIXME;
 | 
			
		||||
	};
 | 
			
		||||
	message: {
 | 
			
		||||
		userId: User['id'];
 | 
			
		||||
		message: FIXME;
 | 
			
		||||
	};
 | 
			
		||||
	changeAccepts: {
 | 
			
		||||
		user1: boolean;
 | 
			
		||||
		user2: boolean;
 | 
			
		||||
	};
 | 
			
		||||
	set: {
 | 
			
		||||
		at: Date;
 | 
			
		||||
		color: boolean;
 | 
			
		||||
		pos: number;
 | 
			
		||||
		next: boolean;
 | 
			
		||||
	};
 | 
			
		||||
	watching: User['id'];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AdminStreamTypes {
 | 
			
		||||
	newAbuseUserReport: {
 | 
			
		||||
		id: AbuseUserReport['id'];
 | 
			
		||||
| 
						 | 
				
			
			@ -268,14 +224,6 @@ export type StreamMessages = {
 | 
			
		|||
		name: `messagingIndexStream:${User['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<MessagingIndexStreamTypes>;
 | 
			
		||||
	};
 | 
			
		||||
	reversi: {
 | 
			
		||||
		name: `reversiStream:${User['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<ReversiStreamTypes>;
 | 
			
		||||
	};
 | 
			
		||||
	reversiGame: {
 | 
			
		||||
		name: `reversiGameStream:${ReversiGame['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<ReversiGameStreamTypes>;
 | 
			
		||||
	};
 | 
			
		||||
	admin: {
 | 
			
		||||
		name: `adminStream:${User['id']}`;
 | 
			
		||||
		payload: EventUnionFromDictionary<AdminStreamTypes>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -390,9 +390,6 @@ router.get('/cli', async ctx => {
 | 
			
		|||
const override = (source: string, target: string, depth: number = 0) =>
 | 
			
		||||
	[, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
 | 
			
		||||
 | 
			
		||||
router.get('/othello', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games/reversi', 1)));
 | 
			
		||||
router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games')));
 | 
			
		||||
 | 
			
		||||
router.get('/flush', async ctx => {
 | 
			
		||||
	await ctx.render('flush');
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ import { redisClient } from '../db/redis';
 | 
			
		|||
import { User } from '@/models/entities/user';
 | 
			
		||||
import { Note } from '@/models/entities/note';
 | 
			
		||||
import { UserList } from '@/models/entities/user-list';
 | 
			
		||||
import { ReversiGame } from '@/models/entities/games/reversi/game';
 | 
			
		||||
import { UserGroup } from '@/models/entities/user-group';
 | 
			
		||||
import config from '@/config/index';
 | 
			
		||||
import { Antenna } from '@/models/entities/antenna';
 | 
			
		||||
| 
						 | 
				
			
			@ -20,8 +19,6 @@ import {
 | 
			
		|||
	MessagingIndexStreamTypes,
 | 
			
		||||
	MessagingStreamTypes,
 | 
			
		||||
	NoteStreamTypes,
 | 
			
		||||
	ReversiGameStreamTypes,
 | 
			
		||||
	ReversiStreamTypes,
 | 
			
		||||
	UserListStreamTypes,
 | 
			
		||||
	UserStreamTypes,
 | 
			
		||||
} from '@/server/api/stream/types';
 | 
			
		||||
| 
						 | 
				
			
			@ -90,14 +87,6 @@ class Publisher {
 | 
			
		|||
		this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	public publishReversiStream = <K extends keyof ReversiStreamTypes>(userId: User['id'], type: K, value?: ReversiStreamTypes[K]): void => {
 | 
			
		||||
		this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	public publishReversiGameStream = <K extends keyof ReversiGameStreamTypes>(gameId: ReversiGame['id'], type: K, value?: ReversiGameStreamTypes[K]): void => {
 | 
			
		||||
		this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	public publishNotesStream = (note: Packed<'Note'>): void => {
 | 
			
		||||
		this.publish('notesStream', null, note);
 | 
			
		||||
	};
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +113,4 @@ export const publishAntennaStream = publisher.publishAntennaStream;
 | 
			
		|||
export const publishMessagingStream = publisher.publishMessagingStream;
 | 
			
		||||
export const publishGroupMessagingStream = publisher.publishGroupMessagingStream;
 | 
			
		||||
export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
 | 
			
		||||
export const publishReversiStream = publisher.publishReversiStream;
 | 
			
		||||
export const publishReversiGameStream = publisher.publishReversiGameStream;
 | 
			
		||||
export const publishAdminStream = publisher.publishAdminStream;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,7 +76,7 @@
 | 
			
		|||
		"ms": "2.1.3",
 | 
			
		||||
		"nested-property": "4.0.0",
 | 
			
		||||
		"parse5": "6.0.1",
 | 
			
		||||
		"photoswipe": "git://github.com/dimsemenov/photoswipe#v5-beta",
 | 
			
		||||
		"photoswipe": "git+https://github.com/dimsemenov/photoswipe#v5-beta",
 | 
			
		||||
		"portscanner": "2.2.0",
 | 
			
		||||
		"postcss": "8.4.5",
 | 
			
		||||
		"postcss-loader": "6.2.1",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -163,11 +163,6 @@ export const menuDef = reactive({
 | 
			
		|||
		icon: 'fas fa-laugh',
 | 
			
		||||
		to: '/emojis',
 | 
			
		||||
	},
 | 
			
		||||
	games: {
 | 
			
		||||
		title: 'games',
 | 
			
		||||
		icon: 'fas fa-gamepad',
 | 
			
		||||
		to: '/games/reversi',
 | 
			
		||||
	},
 | 
			
		||||
	scratchpad: {
 | 
			
		||||
		title: 'scratchpad',
 | 
			
		||||
		icon: 'fas fa-terminal',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@
 | 
			
		|||
					<option value="local">{{ $ts.local }}</option>
 | 
			
		||||
					<option value="remote">{{ $ts.remote }}</option>
 | 
			
		||||
				</MkSelect>
 | 
			
		||||
				<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params().origin === 'local'">
 | 
			
		||||
				<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
 | 
			
		||||
					<template #label>{{ $ts.host }}</template>
 | 
			
		||||
				</MkInput>
 | 
			
		||||
			</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,528 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="xqnhankfuuilcwvhgsopeqncafzsquya">
 | 
			
		||||
	<header><b><MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA></b>({{ $ts._reversi.black }}) vs <b><MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA></b>({{ $ts._reversi.white }})</header>
 | 
			
		||||
 | 
			
		||||
	<div style="overflow: hidden; line-height: 28px;">
 | 
			
		||||
		<p v-if="!iAmPlayer && !game.isEnded" class="turn">
 | 
			
		||||
			<Mfm :key="'turn:' + turnUser().name" :text="$t('_reversi.turnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
 | 
			
		||||
			<MkEllipsis/>
 | 
			
		||||
		</p>
 | 
			
		||||
		<p v-if="logPos != logs.length" class="turn">
 | 
			
		||||
			<Mfm :key="'past-turn-of:' + turnUser().name" :text="$t('_reversi.pastTurnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
 | 
			
		||||
		</p>
 | 
			
		||||
		<p v-if="iAmPlayer && !game.isEnded && !isMyTurn()" class="turn1">{{ $ts._reversi.opponentTurn }}<MkEllipsis/></p>
 | 
			
		||||
		<p v-if="iAmPlayer && !game.isEnded && isMyTurn()" class="turn2" style="animation: tada 1s linear infinite both;">{{ $ts._reversi.myTurn }}</p>
 | 
			
		||||
		<p v-if="game.isEnded && logPos == logs.length" class="result">
 | 
			
		||||
			<template v-if="game.winner">
 | 
			
		||||
				<Mfm :key="'won'" :text="$t('_reversi.won', { name: game.winner.name })" :plain="true" :custom-emojis="game.winner.emojis"/>
 | 
			
		||||
				<span v-if="game.surrendered != null"> ({{ $ts._reversi.surrendered }})</span>
 | 
			
		||||
			</template>
 | 
			
		||||
			<template v-else>{{ $ts._reversi.drawn }}</template>
 | 
			
		||||
		</p>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="board">
 | 
			
		||||
		<div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-x">
 | 
			
		||||
			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="flex">
 | 
			
		||||
			<div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-y">
 | 
			
		||||
				<div v-for="i in game.map.length">{{ i }}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="cells" :style="cellsStyle">
 | 
			
		||||
				<div v-for="(stone, i) in o.board"
 | 
			
		||||
					:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn(), can: turnUser() ? o.canPut(turnUser().id == blackUser.id, i) : null, prev: o.prevPos == i }"
 | 
			
		||||
					:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"
 | 
			
		||||
					@click="set(i)"
 | 
			
		||||
				>
 | 
			
		||||
					<template v-if="$store.state.gamesReversiUseAvatarStones || true">
 | 
			
		||||
						<img v-if="stone === true" :src="blackUser.avatarUrl" alt="black">
 | 
			
		||||
						<img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white">
 | 
			
		||||
					</template>
 | 
			
		||||
					<template v-else>
 | 
			
		||||
						<i v-if="stone === true" class="fas fa-circle"></i>
 | 
			
		||||
						<i v-if="stone === false" class="far fa-circle"></i>
 | 
			
		||||
					</template>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-y">
 | 
			
		||||
				<div v-for="i in game.map.length">{{ i }}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-x">
 | 
			
		||||
			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<p class="status"><b>{{ $t('_reversi.turnCount', { count: logPos }) }}</b> {{ $ts._reversi.black }}:{{ o.blackCount }} {{ $ts._reversi.white }}:{{ o.whiteCount }} {{ $ts._reversi.total }}:{{ o.blackCount + o.whiteCount }}</p>
 | 
			
		||||
 | 
			
		||||
	<div v-if="!game.isEnded && iAmPlayer" class="actions">
 | 
			
		||||
		<MkButton inline @click="surrender">{{ $ts._reversi.surrender }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div v-if="game.isEnded" class="player">
 | 
			
		||||
		<span>{{ logPos }} / {{ logs.length }}</span>
 | 
			
		||||
		<div v-if="!autoplaying" class="buttons">
 | 
			
		||||
			<MkButton inline :disabled="logPos == 0" @click="logPos = 0"><i class="fas fa-angle-double-left"></i></MkButton>
 | 
			
		||||
			<MkButton inline :disabled="logPos == 0" @click="logPos--"><i class="fas fa-angle-left"></i></MkButton>
 | 
			
		||||
			<MkButton inline :disabled="logPos == logs.length" @click="logPos++"><i class="fas fa-angle-right"></i></MkButton>
 | 
			
		||||
			<MkButton inline :disabled="logPos == logs.length" @click="logPos = logs.length"><i class="fas fa-angle-double-right"></i></MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
		<MkButton :disabled="autoplaying" style="margin: var(--margin) auto 0 auto;" @click="autoplay()"><i class="fas fa-play"></i></MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="info">
 | 
			
		||||
		<p v-if="game.isLlotheo">{{ $ts._reversi.isLlotheo }}</p>
 | 
			
		||||
		<p v-if="game.loopedBoard">{{ $ts._reversi.loopedMap }}</p>
 | 
			
		||||
		<p v-if="game.canPutEverywhere">{{ $ts._reversi.canPutEverywhere }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="watchers">
 | 
			
		||||
		<MkAvatar v-for="user in watchers" :key="user.id" :user="user" class="avatar"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import * as CRC32 from 'crc-32';
 | 
			
		||||
import Reversi, { Color } from '@/scripts/games/reversi/core';
 | 
			
		||||
import { url } from '@/config';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import { userPage } from '@/filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import * as sound from '@/scripts/sound';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		initGame: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			require: true
 | 
			
		||||
		},
 | 
			
		||||
		connection: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			require: true
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			game: JSON.parse(JSON.stringify(this.initGame)),
 | 
			
		||||
			o: null as Reversi,
 | 
			
		||||
			logs: [],
 | 
			
		||||
			logPos: 0,
 | 
			
		||||
			watchers: [],
 | 
			
		||||
			pollingClock: null,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		iAmPlayer(): boolean {
 | 
			
		||||
			if (!this.$i) return false;
 | 
			
		||||
			return this.game.user1Id == this.$i.id || this.game.user2Id == this.$i.id;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		myColor(): Color {
 | 
			
		||||
			if (!this.iAmPlayer) return null;
 | 
			
		||||
			if (this.game.user1Id == this.$i.id && this.game.black == 1) return true;
 | 
			
		||||
			if (this.game.user2Id == this.$i.id && this.game.black == 2) return true;
 | 
			
		||||
			return false;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		opColor(): Color {
 | 
			
		||||
			if (!this.iAmPlayer) return null;
 | 
			
		||||
			return this.myColor === true ? false : true;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		blackUser(): any {
 | 
			
		||||
			return this.game.black == 1 ? this.game.user1 : this.game.user2;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		whiteUser(): any {
 | 
			
		||||
			return this.game.black == 1 ? this.game.user2 : this.game.user1;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cellsStyle(): any {
 | 
			
		||||
			return {
 | 
			
		||||
				'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`,
 | 
			
		||||
				'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)`
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		logPos(v) {
 | 
			
		||||
			if (!this.game.isEnded) return;
 | 
			
		||||
			const o = new Reversi(this.game.map, {
 | 
			
		||||
				isLlotheo: this.game.isLlotheo,
 | 
			
		||||
				canPutEverywhere: this.game.canPutEverywhere,
 | 
			
		||||
				loopedBoard: this.game.loopedBoard
 | 
			
		||||
			});
 | 
			
		||||
			for (const log of this.logs.slice(0, v)) {
 | 
			
		||||
				o.put(log.color, log.pos);
 | 
			
		||||
			}
 | 
			
		||||
			this.o = o;
 | 
			
		||||
			//this.$forceUpdate();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.o = new Reversi(this.game.map, {
 | 
			
		||||
			isLlotheo: this.game.isLlotheo,
 | 
			
		||||
			canPutEverywhere: this.game.canPutEverywhere,
 | 
			
		||||
			loopedBoard: this.game.loopedBoard
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		for (const log of this.game.logs) {
 | 
			
		||||
			this.o.put(log.color, log.pos);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.logs = this.game.logs;
 | 
			
		||||
		this.logPos = this.logs.length;
 | 
			
		||||
 | 
			
		||||
		// 通信を取りこぼしてもいいように定期的にポーリングさせる
 | 
			
		||||
		if (this.game.isStarted && !this.game.isEnded) {
 | 
			
		||||
			this.pollingClock = setInterval(() => {
 | 
			
		||||
				if (this.game.isEnded) return;
 | 
			
		||||
				const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join(''));
 | 
			
		||||
				this.connection.send('check', {
 | 
			
		||||
					crc32: crc32
 | 
			
		||||
				});
 | 
			
		||||
			}, 3000);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.connection.on('set', this.onSet);
 | 
			
		||||
		this.connection.on('rescue', this.onRescue);
 | 
			
		||||
		this.connection.on('ended', this.onEnded);
 | 
			
		||||
		this.connection.on('watchers', this.onWatchers);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		this.connection.off('set', this.onSet);
 | 
			
		||||
		this.connection.off('rescue', this.onRescue);
 | 
			
		||||
		this.connection.off('ended', this.onEnded);
 | 
			
		||||
		this.connection.off('watchers', this.onWatchers);
 | 
			
		||||
 | 
			
		||||
		clearInterval(this.pollingClock);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		userPage,
 | 
			
		||||
 | 
			
		||||
		// this.o がリアクティブになった折にはcomputedにできる
 | 
			
		||||
		turnUser(): any {
 | 
			
		||||
			if (this.o.turn === true) {
 | 
			
		||||
				return this.game.black == 1 ? this.game.user1 : this.game.user2;
 | 
			
		||||
			} else if (this.o.turn === false) {
 | 
			
		||||
				return this.game.black == 1 ? this.game.user2 : this.game.user1;
 | 
			
		||||
			} else {
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// this.o がリアクティブになった折にはcomputedにできる
 | 
			
		||||
		isMyTurn(): boolean {
 | 
			
		||||
			if (!this.iAmPlayer) return false;
 | 
			
		||||
			if (this.turnUser() == null) return false;
 | 
			
		||||
			return this.turnUser().id == this.$i.id;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		set(pos) {
 | 
			
		||||
			if (this.game.isEnded) return;
 | 
			
		||||
			if (!this.iAmPlayer) return;
 | 
			
		||||
			if (!this.isMyTurn()) return;
 | 
			
		||||
			if (!this.o.canPut(this.myColor, pos)) return;
 | 
			
		||||
 | 
			
		||||
			this.o.put(this.myColor, pos);
 | 
			
		||||
 | 
			
		||||
			// サウンドを再生する
 | 
			
		||||
			sound.play(this.myColor ? 'reversiPutBlack' : 'reversiPutWhite');
 | 
			
		||||
 | 
			
		||||
			this.connection.send('set', {
 | 
			
		||||
				pos: pos
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.checkEnd();
 | 
			
		||||
 | 
			
		||||
			this.$forceUpdate();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onSet(x) {
 | 
			
		||||
			this.logs.push(x);
 | 
			
		||||
			this.logPos++;
 | 
			
		||||
			this.o.put(x.color, x.pos);
 | 
			
		||||
			this.checkEnd();
 | 
			
		||||
			this.$forceUpdate();
 | 
			
		||||
 | 
			
		||||
			// サウンドを再生する
 | 
			
		||||
			if (x.color !== this.myColor) {
 | 
			
		||||
				sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite');
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onEnded(x) {
 | 
			
		||||
			this.game = JSON.parse(JSON.stringify(x.game));
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		checkEnd() {
 | 
			
		||||
			this.game.isEnded = this.o.isEnded;
 | 
			
		||||
			if (this.game.isEnded) {
 | 
			
		||||
				if (this.o.winner === true) {
 | 
			
		||||
					this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id;
 | 
			
		||||
					this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
 | 
			
		||||
				} else if (this.o.winner === false) {
 | 
			
		||||
					this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id;
 | 
			
		||||
					this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
 | 
			
		||||
				} else {
 | 
			
		||||
					this.game.winnerId = null;
 | 
			
		||||
					this.game.winner = null;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// 正しいゲーム情報が送られてきたとき
 | 
			
		||||
		onRescue(game) {
 | 
			
		||||
			this.game = JSON.parse(JSON.stringify(game));
 | 
			
		||||
 | 
			
		||||
			this.o = new Reversi(this.game.map, {
 | 
			
		||||
				isLlotheo: this.game.isLlotheo,
 | 
			
		||||
				canPutEverywhere: this.game.canPutEverywhere,
 | 
			
		||||
				loopedBoard: this.game.loopedBoard
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			for (const log of this.game.logs) {
 | 
			
		||||
				this.o.put(log.color, log.pos, true);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.logs = this.game.logs;
 | 
			
		||||
			this.logPos = this.logs.length;
 | 
			
		||||
 | 
			
		||||
			this.checkEnd();
 | 
			
		||||
			this.$forceUpdate();
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onWatchers(users) {
 | 
			
		||||
			this.watchers = users;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		surrender() {
 | 
			
		||||
			os.api('games/reversi/games/surrender', {
 | 
			
		||||
				gameId: this.game.id
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		autoplay() {
 | 
			
		||||
			this.autoplaying = true;
 | 
			
		||||
			this.logPos = 0;
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
				this.logPos = 1;
 | 
			
		||||
 | 
			
		||||
				let i = 1;
 | 
			
		||||
				let previousLog = this.game.logs[0];
 | 
			
		||||
				const tick = () => {
 | 
			
		||||
					const log = this.game.logs[i];
 | 
			
		||||
					const time = new Date(log.at).getTime() - new Date(previousLog.at).getTime()
 | 
			
		||||
					setTimeout(() => {
 | 
			
		||||
						i++;
 | 
			
		||||
						this.logPos++;
 | 
			
		||||
						previousLog = log;
 | 
			
		||||
 | 
			
		||||
						if (i < this.game.logs.length) {
 | 
			
		||||
							tick();
 | 
			
		||||
						} else {
 | 
			
		||||
							this.autoplaying = false;
 | 
			
		||||
						}
 | 
			
		||||
					}, time);
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				tick();
 | 
			
		||||
			}, 1000);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 | 
			
		||||
@use "sass:math";
 | 
			
		||||
 | 
			
		||||
.xqnhankfuuilcwvhgsopeqncafzsquya {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
 | 
			
		||||
	> .go-index {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		z-index: 1;
 | 
			
		||||
		width: 42px;
 | 
			
		||||
		height :42px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> header {
 | 
			
		||||
		padding: 8px;
 | 
			
		||||
		border-bottom: dashed 1px var(--divider);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .board {
 | 
			
		||||
		width: calc(100% - 16px);
 | 
			
		||||
		max-width: 500px;
 | 
			
		||||
		margin: 0 auto;
 | 
			
		||||
 | 
			
		||||
		$label-size: 16px;
 | 
			
		||||
		$gap: 4px;
 | 
			
		||||
 | 
			
		||||
		> .labels-x {
 | 
			
		||||
			height: $label-size;
 | 
			
		||||
			padding: 0 $label-size;
 | 
			
		||||
			display: flex;
 | 
			
		||||
 | 
			
		||||
			> * {
 | 
			
		||||
				flex: 1;
 | 
			
		||||
				display: flex;
 | 
			
		||||
				align-items: center;
 | 
			
		||||
				justify-content: center;
 | 
			
		||||
				font-size: 0.8em;
 | 
			
		||||
 | 
			
		||||
				&:first-child {
 | 
			
		||||
					margin-left: -(math.div($gap, 2));
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:last-child {
 | 
			
		||||
					margin-right: -(math.div($gap, 2));
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .flex {
 | 
			
		||||
			display: flex;
 | 
			
		||||
 | 
			
		||||
			> .labels-y {
 | 
			
		||||
				width: $label-size;
 | 
			
		||||
				display: flex;
 | 
			
		||||
				flex-direction: column;
 | 
			
		||||
 | 
			
		||||
				> * {
 | 
			
		||||
					flex: 1;
 | 
			
		||||
					display: flex;
 | 
			
		||||
					align-items: center;
 | 
			
		||||
					justify-content: center;
 | 
			
		||||
					font-size: 12px;
 | 
			
		||||
 | 
			
		||||
					&:first-child {
 | 
			
		||||
						margin-top: -(math.div($gap, 2));
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&:last-child {
 | 
			
		||||
						margin-bottom: -(math.div($gap, 2));
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .cells {
 | 
			
		||||
				flex: 1;
 | 
			
		||||
				display: grid;
 | 
			
		||||
				grid-gap: $gap;
 | 
			
		||||
 | 
			
		||||
				> div {
 | 
			
		||||
					background: transparent;
 | 
			
		||||
					border-radius: 6px;
 | 
			
		||||
					overflow: hidden;
 | 
			
		||||
 | 
			
		||||
					* {
 | 
			
		||||
						pointer-events: none;
 | 
			
		||||
						user-select: none;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&.empty {
 | 
			
		||||
						border: solid 2px var(--divider);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&.empty.can {
 | 
			
		||||
						border-color: var(--accent);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&.empty.myTurn {
 | 
			
		||||
						border-color: var(--divider);
 | 
			
		||||
 | 
			
		||||
						&.can {
 | 
			
		||||
							border-color: var(--accent);
 | 
			
		||||
							cursor: pointer;
 | 
			
		||||
 | 
			
		||||
							&:hover {
 | 
			
		||||
								background: var(--accent);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&.prev {
 | 
			
		||||
						box-shadow: 0 0 0 4px var(--accent);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&.isEnded {
 | 
			
		||||
						border-color: var(--divider);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&.none {
 | 
			
		||||
						border-color: transparent !important;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> svg, > img {
 | 
			
		||||
						display: block;
 | 
			
		||||
						width: 100%;
 | 
			
		||||
						height: 100%;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .status {
 | 
			
		||||
		margin: 0;
 | 
			
		||||
		padding: 16px 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .actions {
 | 
			
		||||
		padding-bottom: 16px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .player {
 | 
			
		||||
		padding: 0 16px 32px 16px;
 | 
			
		||||
		margin: 0 auto;
 | 
			
		||||
		max-width: 500px;
 | 
			
		||||
 | 
			
		||||
		> span {
 | 
			
		||||
			display: inline-block;
 | 
			
		||||
			margin: 0 8px;
 | 
			
		||||
			min-width: 70px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .buttons {
 | 
			
		||||
			display: flex;
 | 
			
		||||
 | 
			
		||||
			> * {
 | 
			
		||||
				flex: 1;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .watchers {
 | 
			
		||||
		padding: 0 0 16px 0;
 | 
			
		||||
 | 
			
		||||
		&:empty {
 | 
			
		||||
			display: none;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .avatar {
 | 
			
		||||
			width: 32px;
 | 
			
		||||
			height: 32px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,390 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="urbixznjwwuukfsckrwzwsqzsxornqij">
 | 
			
		||||
	<header><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></header>
 | 
			
		||||
 | 
			
		||||
	<div>
 | 
			
		||||
		<p>{{ $ts._reversi.gameSettings }}</p>
 | 
			
		||||
 | 
			
		||||
		<div class="card map _panel">
 | 
			
		||||
			<header>
 | 
			
		||||
				<select v-model="mapName" :placeholder="$ts._reversi.chooseBoard" @change="onMapChange">
 | 
			
		||||
					<option v-if="mapName == '-Custom-'" label="-Custom-" :value="mapName"/>
 | 
			
		||||
					<option :label="$ts.random" :value="null"/>
 | 
			
		||||
					<optgroup v-for="c in mapCategories" :key="c" :label="c">
 | 
			
		||||
						<option v-for="m in Object.values(maps).filter(m => m.category == c)" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option>
 | 
			
		||||
					</optgroup>
 | 
			
		||||
				</select>
 | 
			
		||||
			</header>
 | 
			
		||||
 | 
			
		||||
			<div>
 | 
			
		||||
				<div v-if="game.map == null" class="random"><i class="fas fa-dice"></i></div>
 | 
			
		||||
				<div v-else class="board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
 | 
			
		||||
					<div v-for="(x, i) in game.map.join('')" :class="{ none: x == ' ' }" @click="onPixelClick(i, x)">
 | 
			
		||||
						<i v-if="x === 'b'" class="fas fa-circle"></i>
 | 
			
		||||
						<i v-if="x === 'w'" class="far fa-circle"></i>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="card _panel">
 | 
			
		||||
			<header>
 | 
			
		||||
				<span>{{ $ts._reversi.blackOrWhite }}</span>
 | 
			
		||||
			</header>
 | 
			
		||||
 | 
			
		||||
			<div>
 | 
			
		||||
				<MkRadio v-model="game.bw" value="random" @update:modelValue="updateSettings('bw')">{{ $ts.random }}</MkRadio>
 | 
			
		||||
				<MkRadio v-model="game.bw" :value="'1'" @update:modelValue="updateSettings('bw')">
 | 
			
		||||
					<I18n :src="$ts._reversi.blackIs" tag="span">
 | 
			
		||||
						<template #name>
 | 
			
		||||
							<b><MkUserName :user="game.user1"/></b>
 | 
			
		||||
						</template>
 | 
			
		||||
					</I18n>
 | 
			
		||||
				</MkRadio>
 | 
			
		||||
				<MkRadio v-model="game.bw" :value="'2'" @update:modelValue="updateSettings('bw')">
 | 
			
		||||
					<I18n :src="$ts._reversi.blackIs" tag="span">
 | 
			
		||||
						<template #name>
 | 
			
		||||
							<b><MkUserName :user="game.user2"/></b>
 | 
			
		||||
						</template>
 | 
			
		||||
					</I18n>
 | 
			
		||||
				</MkRadio>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="card _panel">
 | 
			
		||||
			<header>
 | 
			
		||||
				<span>{{ $ts._reversi.rules }}</span>
 | 
			
		||||
			</header>
 | 
			
		||||
 | 
			
		||||
			<div>
 | 
			
		||||
				<MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ $ts._reversi.isLlotheo }}</MkSwitch>
 | 
			
		||||
				<MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ $ts._reversi.loopedMap }}</MkSwitch>
 | 
			
		||||
				<MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ $ts._reversi.canPutEverywhere }}</MkSwitch>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div v-if="form" class="card form _panel">
 | 
			
		||||
			<header>
 | 
			
		||||
				<span>{{ $ts._reversi.botSettings }}</span>
 | 
			
		||||
			</header>
 | 
			
		||||
 | 
			
		||||
			<div>
 | 
			
		||||
				<template v-for="item in form">
 | 
			
		||||
					<MkSwitch v-if="item.type == 'switch'" :key="item.id" v-model="item.value" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</MkSwitch>
 | 
			
		||||
 | 
			
		||||
					<div v-if="item.type == 'radio'" :key="item.id" class="card">
 | 
			
		||||
						<header>
 | 
			
		||||
							<span>{{ item.label }}</span>
 | 
			
		||||
						</header>
 | 
			
		||||
 | 
			
		||||
						<div>
 | 
			
		||||
							<MkRadio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @update:modelValue="onChangeForm(item)">{{ r.label }}</MkRadio>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div v-if="item.type == 'slider'" :key="item.id" class="card">
 | 
			
		||||
						<header>
 | 
			
		||||
							<span>{{ item.label }}</span>
 | 
			
		||||
						</header>
 | 
			
		||||
 | 
			
		||||
						<div>
 | 
			
		||||
							<input v-model="item.value" type="range" :min="item.min" :max="item.max" :step="item.step || 1" @change="onChangeForm(item)"/>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div v-if="item.type == 'textbox'" :key="item.id" class="card">
 | 
			
		||||
						<header>
 | 
			
		||||
							<span>{{ item.label }}</span>
 | 
			
		||||
						</header>
 | 
			
		||||
 | 
			
		||||
						<div>
 | 
			
		||||
							<input v-model="item.value" @change="onChangeForm(item)"/>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</template>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<footer class="_acrylic">
 | 
			
		||||
		<p class="status">
 | 
			
		||||
			<template v-if="isAccepted && isOpAccepted">{{ $ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
 | 
			
		||||
			<template v-if="isAccepted && !isOpAccepted">{{ $ts._reversi.waitingForOther }}<MkEllipsis/></template>
 | 
			
		||||
			<template v-if="!isAccepted && isOpAccepted">{{ $ts._reversi.waitingForMe }}</template>
 | 
			
		||||
			<template v-if="!isAccepted && !isOpAccepted">{{ $ts._reversi.waitingBoth }}<MkEllipsis/></template>
 | 
			
		||||
		</p>
 | 
			
		||||
 | 
			
		||||
		<div class="actions">
 | 
			
		||||
			<MkButton inline @click="exit">{{ $ts.cancel }}</MkButton>
 | 
			
		||||
			<MkButton v-if="!isAccepted" inline primary @click="accept">{{ $ts._reversi.ready }}</MkButton>
 | 
			
		||||
			<MkButton v-if="isAccepted" inline primary @click="cancel">{{ $ts._reversi.cancelReady }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</footer>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import * as maps from '@/scripts/games/reversi/maps';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkSwitch from '@/components/form/switch.vue';
 | 
			
		||||
import MkRadio from '@/components/form/radio.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
		MkRadio,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		initGame: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			require: true
 | 
			
		||||
		},
 | 
			
		||||
		connection: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			require: true
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			game: this.initGame,
 | 
			
		||||
			o: null,
 | 
			
		||||
			isLlotheo: false,
 | 
			
		||||
			mapName: maps.eighteight.name,
 | 
			
		||||
			maps: maps,
 | 
			
		||||
			form: null,
 | 
			
		||||
			messages: [],
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		mapCategories(): string[] {
 | 
			
		||||
			const categories = Object.values(maps).map(x => x.category);
 | 
			
		||||
			return categories.filter((item, pos) => categories.indexOf(item) == pos);
 | 
			
		||||
		},
 | 
			
		||||
		isAccepted(): boolean {
 | 
			
		||||
			if (this.game.user1Id == this.$i.id && this.game.user1Accepted) return true;
 | 
			
		||||
			if (this.game.user2Id == this.$i.id && this.game.user2Accepted) return true;
 | 
			
		||||
			return false;
 | 
			
		||||
		},
 | 
			
		||||
		isOpAccepted(): boolean {
 | 
			
		||||
			if (this.game.user1Id != this.$i.id && this.game.user1Accepted) return true;
 | 
			
		||||
			if (this.game.user2Id != this.$i.id && this.game.user2Accepted) return true;
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		this.connection.on('changeAccepts', this.onChangeAccepts);
 | 
			
		||||
		this.connection.on('updateSettings', this.onUpdateSettings);
 | 
			
		||||
		this.connection.on('initForm', this.onInitForm);
 | 
			
		||||
		this.connection.on('message', this.onMessage);
 | 
			
		||||
 | 
			
		||||
		if (this.game.user1Id != this.$i.id && this.game.form1) this.form = this.game.form1;
 | 
			
		||||
		if (this.game.user2Id != this.$i.id && this.game.form2) this.form = this.game.form2;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		this.connection.off('changeAccepts', this.onChangeAccepts);
 | 
			
		||||
		this.connection.off('updateSettings', this.onUpdateSettings);
 | 
			
		||||
		this.connection.off('initForm', this.onInitForm);
 | 
			
		||||
		this.connection.off('message', this.onMessage);
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		exit() {
 | 
			
		||||
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		accept() {
 | 
			
		||||
			this.connection.send('accept', {});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cancel() {
 | 
			
		||||
			this.connection.send('cancelAccept', {});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onChangeAccepts(accepts) {
 | 
			
		||||
			this.game.user1Accepted = accepts.user1;
 | 
			
		||||
			this.game.user2Accepted = accepts.user2;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		updateSettings(key: string) {
 | 
			
		||||
			this.connection.send('updateSettings', {
 | 
			
		||||
				key: key,
 | 
			
		||||
				value: this.game[key]
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onUpdateSettings({ key, value }) {
 | 
			
		||||
			this.game[key] = value;
 | 
			
		||||
			if (this.game.map == null) {
 | 
			
		||||
				this.mapName = null;
 | 
			
		||||
			} else {
 | 
			
		||||
				const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join(''));
 | 
			
		||||
				this.mapName = found ? found.name : '-Custom-';
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onInitForm(x) {
 | 
			
		||||
			if (x.userId == this.$i.id) return;
 | 
			
		||||
			this.form = x.form;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onMessage(x) {
 | 
			
		||||
			if (x.userId == this.$i.id) return;
 | 
			
		||||
			this.messages.unshift(x.message);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onChangeForm(item) {
 | 
			
		||||
			this.connection.send('updateForm', {
 | 
			
		||||
				id: item.id,
 | 
			
		||||
				value: item.value
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onMapChange() {
 | 
			
		||||
			if (this.mapName == null) {
 | 
			
		||||
				this.game.map = null;
 | 
			
		||||
			} else {
 | 
			
		||||
				this.game.map = Object.values(maps).find(x => x.name == this.mapName).data;
 | 
			
		||||
			}
 | 
			
		||||
			this.updateSettings('map');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onPixelClick(pos, pixel) {
 | 
			
		||||
			const x = pos % this.game.map[0].length;
 | 
			
		||||
			const y = Math.floor(pos / this.game.map[0].length);
 | 
			
		||||
			const newPixel =
 | 
			
		||||
				pixel == ' ' ? '-' :
 | 
			
		||||
				pixel == '-' ? 'b' :
 | 
			
		||||
				pixel == 'b' ? 'w' :
 | 
			
		||||
				' ';
 | 
			
		||||
			const line = this.game.map[y].split('');
 | 
			
		||||
			line[x] = newPixel;
 | 
			
		||||
			this.game.map[y] = line.join('');
 | 
			
		||||
			this.updateSettings('map');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.urbixznjwwuukfsckrwzwsqzsxornqij {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	background: var(--bg);
 | 
			
		||||
 | 
			
		||||
	> header {
 | 
			
		||||
		padding: 8px;
 | 
			
		||||
		border-bottom: dashed 1px #c4cdd4;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> div {
 | 
			
		||||
		padding: 0 16px;
 | 
			
		||||
 | 
			
		||||
		> .card {
 | 
			
		||||
			margin: 0 auto 16px auto;
 | 
			
		||||
 | 
			
		||||
			&.map {
 | 
			
		||||
				> header {
 | 
			
		||||
					> select {
 | 
			
		||||
						width: 100%;
 | 
			
		||||
						padding: 12px 14px;
 | 
			
		||||
						background: var(--face);
 | 
			
		||||
						border: 1px solid var(--inputBorder);
 | 
			
		||||
						border-radius: 4px;
 | 
			
		||||
						color: var(--fg);
 | 
			
		||||
						cursor: pointer;
 | 
			
		||||
						transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
 | 
			
		||||
						-webkit-appearance: none;
 | 
			
		||||
						-moz-appearance: none;
 | 
			
		||||
						appearance: none;
 | 
			
		||||
 | 
			
		||||
						&:focus-visible,
 | 
			
		||||
						&:active {
 | 
			
		||||
							border-color: var(--accent);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> div {
 | 
			
		||||
					> .random {
 | 
			
		||||
						padding: 32px 0;
 | 
			
		||||
						font-size: 64px;
 | 
			
		||||
						color: var(--fg);
 | 
			
		||||
						opacity: 0.7;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .board {
 | 
			
		||||
						display: grid;
 | 
			
		||||
						grid-gap: 4px;
 | 
			
		||||
						width: 300px;
 | 
			
		||||
						height: 300px;
 | 
			
		||||
						margin: 0 auto;
 | 
			
		||||
						color: var(--fg);
 | 
			
		||||
 | 
			
		||||
						> div {
 | 
			
		||||
							background: transparent;
 | 
			
		||||
							border: solid 2px var(--divider);
 | 
			
		||||
							border-radius: 6px;
 | 
			
		||||
							overflow: hidden;
 | 
			
		||||
							cursor: pointer;
 | 
			
		||||
 | 
			
		||||
							* {
 | 
			
		||||
								pointer-events: none;
 | 
			
		||||
								user-select: none;
 | 
			
		||||
								width: 100%;
 | 
			
		||||
								height: 100%;
 | 
			
		||||
							}
 | 
			
		||||
 | 
			
		||||
							&.none {
 | 
			
		||||
								border-color: transparent;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.form {
 | 
			
		||||
				> div {
 | 
			
		||||
					> .card + .card {
 | 
			
		||||
						margin-top: 16px;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					input[type='range'] {
 | 
			
		||||
						width: 100%;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.card {
 | 
			
		||||
			max-width: 400px;
 | 
			
		||||
 | 
			
		||||
			> header {
 | 
			
		||||
				padding: 18px 20px;
 | 
			
		||||
				border-bottom: 1px solid var(--divider);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> div {
 | 
			
		||||
				padding: 20px;
 | 
			
		||||
				color: var(--fg);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> footer {
 | 
			
		||||
		position: sticky;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
		padding: 16px;
 | 
			
		||||
		border-top: solid 1px var(--divider);
 | 
			
		||||
 | 
			
		||||
		> .status {
 | 
			
		||||
			margin: 0 0 16px 0;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,77 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div v-if="game == null"><MkLoading/></div>
 | 
			
		||||
<GameSetting v-else-if="!game.isStarted" :init-game="game" :connection="connection"/>
 | 
			
		||||
<GameBoard v-else :init-game="game" :connection="connection"/>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
import GameSetting from './game.setting.vue';
 | 
			
		||||
import GameBoard from './game.board.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { stream } from '@/stream';
 | 
			
		||||
import * as symbols from '@/symbols';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		GameSetting,
 | 
			
		||||
		GameBoard,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		gameId: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			[symbols.PAGE_INFO]: {
 | 
			
		||||
				title: this.$ts._reversi.reversi,
 | 
			
		||||
				icon: 'fas fa-gamepad'
 | 
			
		||||
			},
 | 
			
		||||
			game: null,
 | 
			
		||||
			connection: null,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		gameId() {
 | 
			
		||||
			this.fetch();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.fetch();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		if (this.connection) {
 | 
			
		||||
			this.connection.dispose();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetch() {
 | 
			
		||||
			os.api('games/reversi/games/show', {
 | 
			
		||||
				gameId: this.gameId
 | 
			
		||||
			}).then(game => {
 | 
			
		||||
				this.game = game;
 | 
			
		||||
 | 
			
		||||
				if (this.connection) {
 | 
			
		||||
					this.connection.dispose();
 | 
			
		||||
				}
 | 
			
		||||
				this.connection = markRaw(stream.useChannel('gamesReversiGame', {
 | 
			
		||||
					gameId: this.game.id
 | 
			
		||||
				}));
 | 
			
		||||
				this.connection.on('started', this.onStarted);
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onStarted(game) {
 | 
			
		||||
			Object.assign(this.game, game);
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,280 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div v-if="!matching" class="bgvwxkhb">
 | 
			
		||||
	<h1>Misskey {{ $ts._reversi.reversi }}</h1>
 | 
			
		||||
 | 
			
		||||
	<div class="play">
 | 
			
		||||
		<MkButton primary round style="margin: var(--margin) auto 0 auto;" @click="match">{{ $ts.invite }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<MkFolder v-if="invitations.length > 0">
 | 
			
		||||
			<template #header>{{ $ts.invitations }}</template>
 | 
			
		||||
			<div class="nfcacttm">
 | 
			
		||||
				<button v-for="invitation in invitations" class="invitation _panel _button" tabindex="-1" @click="accept(invitation)">
 | 
			
		||||
					<MkAvatar class="avatar" :user="invitation.parent" :show-indicator="true"/>
 | 
			
		||||
					<span class="name"><b><MkUserName :user="invitation.parent"/></b></span>
 | 
			
		||||
					<span class="username">@{{ invitation.parent.username }}</span>
 | 
			
		||||
					<MkTime :time="invitation.createdAt" class="time"/>
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
 | 
			
		||||
		<MkFolder v-if="myGames.length > 0">
 | 
			
		||||
			<template #header>{{ $ts._reversi.myGames }}</template>
 | 
			
		||||
			<div class="knextgwz">
 | 
			
		||||
				<MkA v-for="g in myGames" :key="g.id" class="game _panel" tabindex="-1" :to="`/games/reversi/${g.id}`">
 | 
			
		||||
					<div class="players">
 | 
			
		||||
						<MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
 | 
			
		||||
					</div>
 | 
			
		||||
					<footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer>
 | 
			
		||||
				</MkA>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
 | 
			
		||||
		<MkFolder v-if="games.length > 0">
 | 
			
		||||
			<template #header>{{ $ts._reversi.allGames }}</template>
 | 
			
		||||
			<div class="knextgwz">
 | 
			
		||||
				<MkA v-for="g in games" :key="g.id" class="game _panel" tabindex="-1" :to="`/games/reversi/${g.id}`">
 | 
			
		||||
					<div class="players">
 | 
			
		||||
						<MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
 | 
			
		||||
					</div>
 | 
			
		||||
					<footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer>
 | 
			
		||||
				</MkA>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
<div v-else class="sazhgisb">
 | 
			
		||||
	<h1>
 | 
			
		||||
		<I18n :src="$ts.waitingFor" tag="span">
 | 
			
		||||
			<template #x>
 | 
			
		||||
				<b><MkUserName :user="matching"/></b>
 | 
			
		||||
			</template>
 | 
			
		||||
		</I18n>
 | 
			
		||||
		<MkEllipsis/>
 | 
			
		||||
	</h1>
 | 
			
		||||
	<div class="cancel">
 | 
			
		||||
		<MkButton inline round @click="cancel">{{ $ts.cancel }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, markRaw } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { stream } from '@/stream';
 | 
			
		||||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import MkFolder from '@/components/ui/folder.vue';
 | 
			
		||||
import * as symbols from '@/symbols';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkButton, MkFolder,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	inject: ['navHook'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			[symbols.PAGE_INFO]: {
 | 
			
		||||
				title: this.$ts._reversi.reversi,
 | 
			
		||||
				icon: 'fas fa-gamepad'
 | 
			
		||||
			},
 | 
			
		||||
			games: [],
 | 
			
		||||
			gamesFetching: true,
 | 
			
		||||
			gamesMoreFetching: false,
 | 
			
		||||
			myGames: [],
 | 
			
		||||
			matching: null,
 | 
			
		||||
			invitations: [],
 | 
			
		||||
			connection: null,
 | 
			
		||||
			pingClock: null,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		if (this.$i) {
 | 
			
		||||
			this.connection = markRaw(stream.useChannel('gamesReversi'));
 | 
			
		||||
 | 
			
		||||
			this.connection.on('invited', this.onInvited);
 | 
			
		||||
 | 
			
		||||
			this.connection.on('matched', this.onMatched);
 | 
			
		||||
 | 
			
		||||
			this.pingClock = setInterval(() => {
 | 
			
		||||
				if (this.matching) {
 | 
			
		||||
					this.connection.send('ping', {
 | 
			
		||||
						id: this.matching.id
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}, 3000);
 | 
			
		||||
 | 
			
		||||
			os.api('games/reversi/games', {
 | 
			
		||||
				my: true
 | 
			
		||||
			}).then(games => {
 | 
			
		||||
				this.myGames = games;
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			os.api('games/reversi/invitations').then(invitations => {
 | 
			
		||||
				this.invitations = this.invitations.concat(invitations);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		os.api('games/reversi/games').then(games => {
 | 
			
		||||
			this.games = games;
 | 
			
		||||
			this.gamesFetching = false;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	beforeUnmount() {
 | 
			
		||||
		if (this.connection) {
 | 
			
		||||
			this.connection.dispose();
 | 
			
		||||
			clearInterval(this.pingClock);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		go(game) {
 | 
			
		||||
			const url = '/games/reversi/' + game.id;
 | 
			
		||||
			if (this.navHook) {
 | 
			
		||||
				this.navHook(url);
 | 
			
		||||
			} else {
 | 
			
		||||
				this.$router.push(url);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async match() {
 | 
			
		||||
			const user = await os.selectUser({ local: true });
 | 
			
		||||
			if (user == null) return;
 | 
			
		||||
			os.api('games/reversi/match', {
 | 
			
		||||
				userId: user.id
 | 
			
		||||
			}).then(res => {
 | 
			
		||||
				if (res == null) {
 | 
			
		||||
					this.matching = user;
 | 
			
		||||
				} else {
 | 
			
		||||
					this.go(res);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cancel() {
 | 
			
		||||
			this.matching = null;
 | 
			
		||||
			os.api('games/reversi/match/cancel');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		accept(invitation) {
 | 
			
		||||
			os.api('games/reversi/match', {
 | 
			
		||||
				userId: invitation.parent.id
 | 
			
		||||
			}).then(game => {
 | 
			
		||||
				if (game) {
 | 
			
		||||
					this.go(game);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onMatched(game) {
 | 
			
		||||
			this.go(game);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onInvited(invite) {
 | 
			
		||||
			this.invitations.unshift(invite);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.bgvwxkhb {
 | 
			
		||||
	> h1 {
 | 
			
		||||
		margin: 0;
 | 
			
		||||
		padding: 24px;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		font-size: 1.5em;
 | 
			
		||||
		background: linear-gradient(0deg, #43c583, #438881);
 | 
			
		||||
		color: #fff;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .play {
 | 
			
		||||
		text-align: center;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sazhgisb {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nfcacttm {
 | 
			
		||||
	> .invitation {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		box-sizing: border-box;
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		padding: 16px;
 | 
			
		||||
		line-height: 32px;
 | 
			
		||||
		text-align: left;
 | 
			
		||||
 | 
			
		||||
		> .avatar {
 | 
			
		||||
			width: 32px;
 | 
			
		||||
			height: 32px;
 | 
			
		||||
			margin-right: 8px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .name {
 | 
			
		||||
			margin-right: 8px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .username {
 | 
			
		||||
			margin-right: 8px;
 | 
			
		||||
			opacity: 0.7;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .time {
 | 
			
		||||
			margin-left: auto;
 | 
			
		||||
			opacity: 0.7;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.knextgwz {
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
 | 
			
		||||
	grid-gap: var(--margin);
 | 
			
		||||
 | 
			
		||||
	> .game {
 | 
			
		||||
		> .players {
 | 
			
		||||
			text-align: center;
 | 
			
		||||
			padding: 16px;
 | 
			
		||||
			line-height: 32px;
 | 
			
		||||
 | 
			
		||||
			> .avatar {
 | 
			
		||||
				width: 32px;
 | 
			
		||||
				height: 32px;
 | 
			
		||||
 | 
			
		||||
				&:first-child {
 | 
			
		||||
					margin-right: 8px;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:last-child {
 | 
			
		||||
					margin-left: 8px;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> footer {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			align-items: baseline;
 | 
			
		||||
			border-top: solid 0.5px var(--divider);
 | 
			
		||||
			padding: 6px 8px;
 | 
			
		||||
			font-size: 0.9em;
 | 
			
		||||
 | 
			
		||||
			> .state {
 | 
			
		||||
				&.playing {
 | 
			
		||||
					color: var(--accent);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .time {
 | 
			
		||||
				margin-left: auto;
 | 
			
		||||
				opacity: 0.7;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -94,10 +94,6 @@
 | 
			
		|||
			<template #key>{{ $ts.driveUsage }}</template>
 | 
			
		||||
			<template #value>{{ bytes(stats.driveUsage) }}</template>
 | 
			
		||||
		</MkKeyValue>
 | 
			
		||||
		<MkKeyValue oneline style="margin: 1em 0;">
 | 
			
		||||
			<template #key>{{ $ts.reversiCount }}</template>
 | 
			
		||||
			<template #value>{{ number(stats.reversiCount) }}</template>
 | 
			
		||||
		</MkKeyValue>
 | 
			
		||||
	</FormSection>
 | 
			
		||||
 | 
			
		||||
	<FormSection>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -94,8 +94,6 @@ export default defineComponent({
 | 
			
		|||
		this.sounds.chatBg = ColdDeviceStorage.get('sound_chatBg');
 | 
			
		||||
		this.sounds.antenna = ColdDeviceStorage.get('sound_antenna');
 | 
			
		||||
		this.sounds.channel = ColdDeviceStorage.get('sound_channel');
 | 
			
		||||
		this.sounds.reversiPutBlack = ColdDeviceStorage.get('sound_reversiPutBlack');
 | 
			
		||||
		this.sounds.reversiPutWhite = ColdDeviceStorage.get('sound_reversiPutWhite');
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,8 +73,6 @@ const defaultRoutes = [
 | 
			
		|||
	{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
 | 
			
		||||
	{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
 | 
			
		||||
	{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) },
 | 
			
		||||
	{ path: '/games/reversi', component: page('reversi/index') },
 | 
			
		||||
	{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
 | 
			
		||||
	{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
 | 
			
		||||
	{ path: '/api-console', component: page('api-console') },
 | 
			
		||||
	{ path: '/preview', component: page('preview') },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,263 +0,0 @@
 | 
			
		|||
import { count, concat } from '@/scripts/array';
 | 
			
		||||
 | 
			
		||||
// MISSKEY REVERSI ENGINE
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * true ... 黒
 | 
			
		||||
 * false ... 白
 | 
			
		||||
 */
 | 
			
		||||
export type Color = boolean;
 | 
			
		||||
const BLACK = true;
 | 
			
		||||
const WHITE = false;
 | 
			
		||||
 | 
			
		||||
export type MapPixel = 'null' | 'empty';
 | 
			
		||||
 | 
			
		||||
export type Options = {
 | 
			
		||||
	isLlotheo: boolean;
 | 
			
		||||
	canPutEverywhere: boolean;
 | 
			
		||||
	loopedBoard: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Undo = {
 | 
			
		||||
	/**
 | 
			
		||||
	 * 色
 | 
			
		||||
	 */
 | 
			
		||||
	color: Color;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * どこに打ったか
 | 
			
		||||
	 */
 | 
			
		||||
	pos: number;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 反転した石の位置の配列
 | 
			
		||||
	 */
 | 
			
		||||
	effects: number[];
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ターン
 | 
			
		||||
	 */
 | 
			
		||||
	turn: Color | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * リバーシエンジン
 | 
			
		||||
 */
 | 
			
		||||
export default class Reversi {
 | 
			
		||||
	public map: MapPixel[];
 | 
			
		||||
	public mapWidth: number;
 | 
			
		||||
	public mapHeight: number;
 | 
			
		||||
	public board: (Color | null | undefined)[];
 | 
			
		||||
	public turn: Color | null = BLACK;
 | 
			
		||||
	public opts: Options;
 | 
			
		||||
 | 
			
		||||
	public prevPos = -1;
 | 
			
		||||
	public prevColor: Color | null = null;
 | 
			
		||||
 | 
			
		||||
	private logs: Undo[] = [];
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ゲームを初期化します
 | 
			
		||||
	 */
 | 
			
		||||
	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;
 | 
			
		||||
		if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
 | 
			
		||||
		if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
		//#region Parse map data
 | 
			
		||||
		this.mapWidth = map[0].length;
 | 
			
		||||
		this.mapHeight = map.length;
 | 
			
		||||
		const mapData = map.join('');
 | 
			
		||||
 | 
			
		||||
		this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined);
 | 
			
		||||
 | 
			
		||||
		this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null');
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
		// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
 | 
			
		||||
		if (!this.canPutSomewhere(BLACK))
 | 
			
		||||
			this.turn = this.canPutSomewhere(WHITE) ? WHITE : null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 黒石の数
 | 
			
		||||
	 */
 | 
			
		||||
	public get blackCount() {
 | 
			
		||||
		return count(BLACK, this.board);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 白石の数
 | 
			
		||||
	 */
 | 
			
		||||
	public get whiteCount() {
 | 
			
		||||
		return count(WHITE, this.board);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public transformPosToXy(pos: number): number[] {
 | 
			
		||||
		const x = pos % this.mapWidth;
 | 
			
		||||
		const y = Math.floor(pos / this.mapWidth);
 | 
			
		||||
		return [x, y];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public transformXyToPos(x: number, y: number): number {
 | 
			
		||||
		return x + (y * this.mapWidth);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 指定のマスに石を打ちます
 | 
			
		||||
	 * @param color 石の色
 | 
			
		||||
	 * @param pos 位置
 | 
			
		||||
	 */
 | 
			
		||||
	public put(color: Color, pos: number) {
 | 
			
		||||
		this.prevPos = pos;
 | 
			
		||||
		this.prevColor = color;
 | 
			
		||||
 | 
			
		||||
		this.board[pos] = color;
 | 
			
		||||
 | 
			
		||||
		// 反転させられる石を取得
 | 
			
		||||
		const effects = this.effects(color, pos);
 | 
			
		||||
 | 
			
		||||
		// 反転させる
 | 
			
		||||
		for (const pos of effects) {
 | 
			
		||||
			this.board[pos] = color;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const turn = this.turn;
 | 
			
		||||
 | 
			
		||||
		this.logs.push({
 | 
			
		||||
			color,
 | 
			
		||||
			pos,
 | 
			
		||||
			effects,
 | 
			
		||||
			turn
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.calcTurn();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private calcTurn() {
 | 
			
		||||
		// ターン計算
 | 
			
		||||
		this.turn =
 | 
			
		||||
			this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
 | 
			
		||||
			this.canPutSomewhere(this.prevColor!) ? this.prevColor :
 | 
			
		||||
			null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public undo() {
 | 
			
		||||
		const undo = this.logs.pop()!;
 | 
			
		||||
		this.prevColor = undo.color;
 | 
			
		||||
		this.prevPos = undo.pos;
 | 
			
		||||
		this.board[undo.pos] = null;
 | 
			
		||||
		for (const pos of undo.effects) {
 | 
			
		||||
			const color = this.board[pos];
 | 
			
		||||
			this.board[pos] = !color;
 | 
			
		||||
		}
 | 
			
		||||
		this.turn = undo.turn;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 指定した位置のマップデータのマスを取得します
 | 
			
		||||
	 * @param pos 位置
 | 
			
		||||
	 */
 | 
			
		||||
	public mapDataGet(pos: number): MapPixel {
 | 
			
		||||
		const [x, y] = this.transformPosToXy(pos);
 | 
			
		||||
		return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 打つことができる場所を取得します
 | 
			
		||||
	 */
 | 
			
		||||
	public puttablePlaces(color: Color): number[] {
 | 
			
		||||
		return Array.from(this.board.keys()).filter(i => this.canPut(color, i));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 打つことができる場所があるかどうかを取得します
 | 
			
		||||
	 */
 | 
			
		||||
	public canPutSomewhere(color: Color): boolean {
 | 
			
		||||
		return this.puttablePlaces(color).length > 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 指定のマスに石を打つことができるかどうかを取得します
 | 
			
		||||
	 * @param color 自分の色
 | 
			
		||||
	 * @param pos 位置
 | 
			
		||||
	 */
 | 
			
		||||
	public canPut(color: Color, pos: number): boolean {
 | 
			
		||||
		return (
 | 
			
		||||
			this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない
 | 
			
		||||
			this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード
 | 
			
		||||
			this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 指定のマスに石を置いた時の、反転させられる石を取得します
 | 
			
		||||
	 * @param color 自分の色
 | 
			
		||||
	 * @param initPos 位置
 | 
			
		||||
	 */
 | 
			
		||||
	public effects(color: Color, initPos: number): number[] {
 | 
			
		||||
		const enemyColor = !color;
 | 
			
		||||
 | 
			
		||||
		const diffVectors: [number, number][] = [
 | 
			
		||||
			[  0,  -1], // 上
 | 
			
		||||
			[ +1,  -1], // 右上
 | 
			
		||||
			[ +1,   0], // 右
 | 
			
		||||
			[ +1,  +1], // 右下
 | 
			
		||||
			[  0,  +1], // 下
 | 
			
		||||
			[ -1,  +1], // 左下
 | 
			
		||||
			[ -1,   0], // 左
 | 
			
		||||
			[ -1,  -1]  // 左上
 | 
			
		||||
		];
 | 
			
		||||
 | 
			
		||||
		const effectsInLine = ([dx, dy]: [number, number]): number[] => {
 | 
			
		||||
			const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy];
 | 
			
		||||
 | 
			
		||||
			const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
 | 
			
		||||
			let [x, y] = this.transformPosToXy(initPos);
 | 
			
		||||
			while (true) {
 | 
			
		||||
				[x, y] = nextPos(x, y);
 | 
			
		||||
 | 
			
		||||
				// 座標が指し示す位置がボード外に出たとき
 | 
			
		||||
				if (this.opts.loopedBoard && this.transformXyToPos(
 | 
			
		||||
					(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
 | 
			
		||||
					(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos)
 | 
			
		||||
						// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
 | 
			
		||||
					return found;
 | 
			
		||||
				else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight)
 | 
			
		||||
					return []; // 挟めないことが確定 (盤面外に到達)
 | 
			
		||||
 | 
			
		||||
				const pos = this.transformXyToPos(x, y);
 | 
			
		||||
				if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
 | 
			
		||||
				const stone = this.board[pos];
 | 
			
		||||
				if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達)
 | 
			
		||||
				if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見)
 | 
			
		||||
				if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見)
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return concat(diffVectors.map(effectsInLine));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ゲームが終了したか否か
 | 
			
		||||
	 */
 | 
			
		||||
	public get isEnded(): boolean {
 | 
			
		||||
		return this.turn === null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ゲームの勝者 (null = 引き分け)
 | 
			
		||||
	 */
 | 
			
		||||
	public get winner(): Color | null {
 | 
			
		||||
		return this.isEnded ?
 | 
			
		||||
			this.blackCount == this.whiteCount ? null :
 | 
			
		||||
			this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
 | 
			
		||||
			undefined as never;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,896 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * 組み込みマップ定義
 | 
			
		||||
 *
 | 
			
		||||
 * データ値:
 | 
			
		||||
 * (スペース) ... マス無し
 | 
			
		||||
 * - ... マス
 | 
			
		||||
 * b ... 初期配置される黒石
 | 
			
		||||
 * w ... 初期配置される白石
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export type Map = {
 | 
			
		||||
	name?: string;
 | 
			
		||||
	category?: string;
 | 
			
		||||
	author?: string;
 | 
			
		||||
	data: string[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fourfour: Map = {
 | 
			
		||||
	name: '4x4',
 | 
			
		||||
	category: '4x4',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----',
 | 
			
		||||
		'-wb-',
 | 
			
		||||
		'-bw-',
 | 
			
		||||
		'----'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sixsix: Map = {
 | 
			
		||||
	name: '6x6',
 | 
			
		||||
	category: '6x6',
 | 
			
		||||
	data: [
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
		'--wb--',
 | 
			
		||||
		'--bw--',
 | 
			
		||||
		'------',
 | 
			
		||||
		'------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const roundedSixsix: Map = {
 | 
			
		||||
	name: '6x6 rounded',
 | 
			
		||||
	category: '6x6',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' ---- ',
 | 
			
		||||
		'------',
 | 
			
		||||
		'--wb--',
 | 
			
		||||
		'--bw--',
 | 
			
		||||
		'------',
 | 
			
		||||
		' ---- '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const roundedSixsix2: Map = {
 | 
			
		||||
	name: '6x6 rounded 2',
 | 
			
		||||
	category: '6x6',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'  --  ',
 | 
			
		||||
		' ---- ',
 | 
			
		||||
		'--wb--',
 | 
			
		||||
		'--bw--',
 | 
			
		||||
		' ---- ',
 | 
			
		||||
		'  --  '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteight: Map = {
 | 
			
		||||
	name: '8x8',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightH1: Map = {
 | 
			
		||||
	name: '8x8 handicap 1',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b-------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightH2: Map = {
 | 
			
		||||
	name: '8x8 handicap 2',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b-------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-------b'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightH3: Map = {
 | 
			
		||||
	name: '8x8 handicap 3',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-------b'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightH4: Map = {
 | 
			
		||||
	name: '8x8 handicap 4',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'b------b'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightH28: Map = {
 | 
			
		||||
	name: '8x8 handicap 28',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'bbbbbbbb',
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'b--wb--b',
 | 
			
		||||
		'b--bw--b',
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'b------b',
 | 
			
		||||
		'bbbbbbbb'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const roundedEighteight: Map = {
 | 
			
		||||
	name: '8x8 rounded',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		' ------ '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const roundedEighteight2: Map = {
 | 
			
		||||
	name: '8x8 rounded 2',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'  ----  ',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'  ----  '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const roundedEighteight3: Map = {
 | 
			
		||||
	name: '8x8 rounded 3',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'   --   ',
 | 
			
		||||
		'  ----  ',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'  ----  ',
 | 
			
		||||
		'   --   '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightWithNotch: Map = {
 | 
			
		||||
	name: '8x8 with notch',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'---  ---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		' --wb-- ',
 | 
			
		||||
		' --bw-- ',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---  ---'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const eighteightWithSomeHoles: Map = {
 | 
			
		||||
	name: '8x8 with some holes',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--- ----',
 | 
			
		||||
		'----- --',
 | 
			
		||||
		'-- -----',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw- -',
 | 
			
		||||
		' -------',
 | 
			
		||||
		'--- ----',
 | 
			
		||||
		'--------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const circle: Map = {
 | 
			
		||||
	name: 'Circle',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'   --   ',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'   --   '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const smile: Map = {
 | 
			
		||||
	name: 'Smile',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' ------ ',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-- -- --',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'-- bw --',
 | 
			
		||||
		'---  ---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		' ------ '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const window: Map = {
 | 
			
		||||
	name: 'Window',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-  --  -',
 | 
			
		||||
		'-  --  -',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'-  --  -',
 | 
			
		||||
		'-  --  -',
 | 
			
		||||
		'--------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const reserved: Map = {
 | 
			
		||||
	name: 'Reserved',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'w------b',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'b------w'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const x: Map = {
 | 
			
		||||
	name: 'X',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'w------b',
 | 
			
		||||
		'-w----b-',
 | 
			
		||||
		'--w--b--',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--b--w--',
 | 
			
		||||
		'-b----w-',
 | 
			
		||||
		'b------w'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const parallel: Map = {
 | 
			
		||||
	name: 'Parallel',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---bb---',
 | 
			
		||||
		'---ww---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const lackOfBlack: Map = {
 | 
			
		||||
	name: 'Lack of Black',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---w----',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const squareParty: Map = {
 | 
			
		||||
	name: 'Square Party',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-wwwbbb-',
 | 
			
		||||
		'-w-wb-b-',
 | 
			
		||||
		'-wwwbbb-',
 | 
			
		||||
		'-bbbwww-',
 | 
			
		||||
		'-b-bw-w-',
 | 
			
		||||
		'-bbbwww-',
 | 
			
		||||
		'--------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const minesweeper: Map = {
 | 
			
		||||
	name: 'Minesweeper',
 | 
			
		||||
	category: '8x8',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b-b--w-w',
 | 
			
		||||
		'-w-wb-b-',
 | 
			
		||||
		'w-b--w-b',
 | 
			
		||||
		'-b-wb-w-',
 | 
			
		||||
		'-w-bw-b-',
 | 
			
		||||
		'b-w--b-w',
 | 
			
		||||
		'-b-bw-w-',
 | 
			
		||||
		'w-w--b-b'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const tenthtenth: Map = {
 | 
			
		||||
	name: '10x10',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----wb----',
 | 
			
		||||
		'----bw----',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const hole: Map = {
 | 
			
		||||
	name: 'The Hole',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'--wb--wb--',
 | 
			
		||||
		'--bw--bw--',
 | 
			
		||||
		'----  ----',
 | 
			
		||||
		'----  ----',
 | 
			
		||||
		'--wb--wb--',
 | 
			
		||||
		'--bw--bw--',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const grid: Map = {
 | 
			
		||||
	name: 'Grid',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----------',
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
		'----wb----',
 | 
			
		||||
		'----bw----',
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
		'----------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const cross: Map = {
 | 
			
		||||
	name: 'Cross',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----wb----',
 | 
			
		||||
		'----bw----',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'   ----   '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const charX: Map = {
 | 
			
		||||
	name: 'Char X',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'---    ---',
 | 
			
		||||
		'----  ----',
 | 
			
		||||
		'----------',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'  --wb--  ',
 | 
			
		||||
		'  --bw--  ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----  ----',
 | 
			
		||||
		'---    ---'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const charY: Map = {
 | 
			
		||||
	name: 'Char Y',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'---    ---',
 | 
			
		||||
		'----  ----',
 | 
			
		||||
		'----------',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'  --wb--  ',
 | 
			
		||||
		'  --bw--  ',
 | 
			
		||||
		'  ------  ',
 | 
			
		||||
		'  ------  ',
 | 
			
		||||
		'  ------  ',
 | 
			
		||||
		'  ------  '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const walls: Map = {
 | 
			
		||||
	name: 'Walls',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		' bbbbbbbb ',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		'w---wb---w',
 | 
			
		||||
		'w---bw---w',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		' bbbbbbbb '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const cpu: Map = {
 | 
			
		||||
	name: 'CPU',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' b b  b b ',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		' ---wb--- ',
 | 
			
		||||
		' ---bw--- ',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'w--------w',
 | 
			
		||||
		' b b  b b '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const checker: Map = {
 | 
			
		||||
	name: 'Checker',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'---wbwb---',
 | 
			
		||||
		'---bwbw---',
 | 
			
		||||
		'---wbwb---',
 | 
			
		||||
		'---bwbw---',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const japaneseCurry: Map = {
 | 
			
		||||
	name: 'Japanese curry',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'w-b-b-b-b-',
 | 
			
		||||
		'-w-b-b-b-b',
 | 
			
		||||
		'w-w-b-b-b-',
 | 
			
		||||
		'-w-w-b-b-b',
 | 
			
		||||
		'w-w-wwb-b-',
 | 
			
		||||
		'-w-wbb-b-b',
 | 
			
		||||
		'w-w-w-b-b-',
 | 
			
		||||
		'-w-w-w-b-b',
 | 
			
		||||
		'w-w-w-w-b-',
 | 
			
		||||
		'-w-w-w-w-b'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const mosaic: Map = {
 | 
			
		||||
	name: 'Mosaic',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'- - - - - ',
 | 
			
		||||
		' - - - - -',
 | 
			
		||||
		'- - - - - ',
 | 
			
		||||
		' - w w - -',
 | 
			
		||||
		'- - b b - ',
 | 
			
		||||
		' - w w - -',
 | 
			
		||||
		'- - b b - ',
 | 
			
		||||
		' - - - - -',
 | 
			
		||||
		'- - - - - ',
 | 
			
		||||
		' - - - - -',
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const arena: Map = {
 | 
			
		||||
	name: 'Arena',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'- - -- - -',
 | 
			
		||||
		' - -  - - ',
 | 
			
		||||
		'- ------ -',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'- --wb-- -',
 | 
			
		||||
		'- --bw-- -',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'- ------ -',
 | 
			
		||||
		' - -  - - ',
 | 
			
		||||
		'- - -- - -'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const reactor: Map = {
 | 
			
		||||
	name: 'Reactor',
 | 
			
		||||
	category: '10x10',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'-w------b-',
 | 
			
		||||
		'b- -  - -w',
 | 
			
		||||
		'- --wb-- -',
 | 
			
		||||
		'---b  w---',
 | 
			
		||||
		'- b wb w -',
 | 
			
		||||
		'- w bw b -',
 | 
			
		||||
		'---w  b---',
 | 
			
		||||
		'- --bw-- -',
 | 
			
		||||
		'w- -  - -b',
 | 
			
		||||
		'-b------w-'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sixeight: Map = {
 | 
			
		||||
	name: '6x8',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	data: [
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
		'--wb--',
 | 
			
		||||
		'--bw--',
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
		'------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const spark: Map = {
 | 
			
		||||
	name: 'Spark',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' -      - ',
 | 
			
		||||
		'----------',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		' ---wb--- ',
 | 
			
		||||
		' ---bw--- ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'----------',
 | 
			
		||||
		' -      - '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const islands: Map = {
 | 
			
		||||
	name: 'Islands',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------  ',
 | 
			
		||||
		'---wb---  ',
 | 
			
		||||
		'---bw---  ',
 | 
			
		||||
		'--------  ',
 | 
			
		||||
		'  -    -  ',
 | 
			
		||||
		'  -    -  ',
 | 
			
		||||
		'  --------',
 | 
			
		||||
		'  --------',
 | 
			
		||||
		'  --------',
 | 
			
		||||
		'  --------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const galaxy: Map = {
 | 
			
		||||
	name: 'Galaxy',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'   ------   ',
 | 
			
		||||
		'  --www---  ',
 | 
			
		||||
		' ------w--- ',
 | 
			
		||||
		'---bbb--w---',
 | 
			
		||||
		'--b---b-w-b-',
 | 
			
		||||
		'-b--wwb-w-b-',
 | 
			
		||||
		'-b-w-bww--b-',
 | 
			
		||||
		'-b-w-b---b--',
 | 
			
		||||
		'---w--bbb---',
 | 
			
		||||
		' ---w------ ',
 | 
			
		||||
		'  ---www--  ',
 | 
			
		||||
		'   ------   '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const triangle: Map = {
 | 
			
		||||
	name: 'Triangle',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'    --    ',
 | 
			
		||||
		'    --    ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'   ----   ',
 | 
			
		||||
		'  --wb--  ',
 | 
			
		||||
		'  --bw--  ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		' -------- ',
 | 
			
		||||
		'----------',
 | 
			
		||||
		'----------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const iphonex: Map = {
 | 
			
		||||
	name: 'iPhone X',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' --  -- ',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		'--------',
 | 
			
		||||
		' ------ '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dealWithIt: Map = {
 | 
			
		||||
	name: 'Deal with it!',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		'------------',
 | 
			
		||||
		'--w-b-------',
 | 
			
		||||
		' --b-w------',
 | 
			
		||||
		'  --w-b---- ',
 | 
			
		||||
		'   -------  '
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const experiment: Map = {
 | 
			
		||||
	name: 'Let\'s experiment',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'syuilo',
 | 
			
		||||
	data: [
 | 
			
		||||
		' ------------ ',
 | 
			
		||||
		'------wb------',
 | 
			
		||||
		'------bw------',
 | 
			
		||||
		'--------------',
 | 
			
		||||
		'    -    -    ',
 | 
			
		||||
		'------  ------',
 | 
			
		||||
		'bbbbbb  wwwwww',
 | 
			
		||||
		'bbbbbb  wwwwww',
 | 
			
		||||
		'bbbbbb  wwwwww',
 | 
			
		||||
		'bbbbbb  wwwwww',
 | 
			
		||||
		'wwwwww  bbbbbb'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const bigBoard: Map = {
 | 
			
		||||
	name: 'Big board',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	data: [
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'-------wb-------',
 | 
			
		||||
		'-------bw-------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------',
 | 
			
		||||
		'----------------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const twoBoard: Map = {
 | 
			
		||||
	name: 'Two board',
 | 
			
		||||
	category: 'Special',
 | 
			
		||||
	author: 'Aya',
 | 
			
		||||
	data: [
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
		'---wb--- ---wb---',
 | 
			
		||||
		'---bw--- ---bw---',
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
		'-------- --------',
 | 
			
		||||
		'-------- --------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const test1: Map = {
 | 
			
		||||
	name: 'Test1',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'---wb---',
 | 
			
		||||
		'---bw---',
 | 
			
		||||
		'--------'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const test2: Map = {
 | 
			
		||||
	name: 'Test2',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'------',
 | 
			
		||||
		'------',
 | 
			
		||||
		'-b--w-',
 | 
			
		||||
		'-w--b-',
 | 
			
		||||
		'-w--b-'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const test3: Map = {
 | 
			
		||||
	name: 'Test3',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'-w-',
 | 
			
		||||
		'--w',
 | 
			
		||||
		'w--',
 | 
			
		||||
		'-w-',
 | 
			
		||||
		'--w',
 | 
			
		||||
		'w--',
 | 
			
		||||
		'-w-',
 | 
			
		||||
		'--w',
 | 
			
		||||
		'w--',
 | 
			
		||||
		'-w-',
 | 
			
		||||
		'---',
 | 
			
		||||
		'b--',
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const test4: Map = {
 | 
			
		||||
	name: 'Test4',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'-w--b-',
 | 
			
		||||
		'-w--b-',
 | 
			
		||||
		'------',
 | 
			
		||||
		'-w--b-',
 | 
			
		||||
		'-w--b-'
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)A1に打ってしまう
 | 
			
		||||
export const test6: Map = {
 | 
			
		||||
	name: 'Test6',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--wwwww-',
 | 
			
		||||
		'wwwwwwww',
 | 
			
		||||
		'wbbbwbwb',
 | 
			
		||||
		'wbbbbwbb',
 | 
			
		||||
		'wbwbbwbb',
 | 
			
		||||
		'wwbwbbbb',
 | 
			
		||||
		'--wbbbbb',
 | 
			
		||||
		'-wwwww--',
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)G7に打ってしまう
 | 
			
		||||
export const test7: Map = {
 | 
			
		||||
	name: 'Test7',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'b--w----',
 | 
			
		||||
		'b-wwww--',
 | 
			
		||||
		'bwbwwwbb',
 | 
			
		||||
		'wbwwwwb-',
 | 
			
		||||
		'wwwwwww-',
 | 
			
		||||
		'-wwbbwwb',
 | 
			
		||||
		'--wwww--',
 | 
			
		||||
		'--wwww--',
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 検証用: この盤面で藍(lv5)が黒で始めると何故か(?)A1に打ってしまう
 | 
			
		||||
export const test8: Map = {
 | 
			
		||||
	name: 'Test8',
 | 
			
		||||
	category: 'Test',
 | 
			
		||||
	data: [
 | 
			
		||||
		'--------',
 | 
			
		||||
		'-----w--',
 | 
			
		||||
		'w--www--',
 | 
			
		||||
		'wwwwww--',
 | 
			
		||||
		'bbbbwww-',
 | 
			
		||||
		'wwwwww--',
 | 
			
		||||
		'--www---',
 | 
			
		||||
		'--ww----',
 | 
			
		||||
	]
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "misskey-reversi",
 | 
			
		||||
  "version": "0.0.5",
 | 
			
		||||
  "description": "Misskey reversi engine",
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "misskey"
 | 
			
		||||
  ],
 | 
			
		||||
  "author": "syuilo <i@syuilo.com>",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "repository": "https://github.com/misskey-dev/misskey.git",
 | 
			
		||||
  "bugs": "https://github.com/misskey-dev/misskey/issues",
 | 
			
		||||
  "main": "./built/core.js",
 | 
			
		||||
  "types": "./built/core.d.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "build": "tsc"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
	"compilerOptions": {
 | 
			
		||||
		"noEmitOnError": false,
 | 
			
		||||
		"noImplicitAny": false,
 | 
			
		||||
		"noImplicitReturns": true,
 | 
			
		||||
		"noFallthroughCasesInSwitch": true,
 | 
			
		||||
		"experimentalDecorators": true,
 | 
			
		||||
		"declaration": true,
 | 
			
		||||
		"sourceMap": false,
 | 
			
		||||
		"target": "es2017",
 | 
			
		||||
		"module": "commonjs",
 | 
			
		||||
		"removeComments": false,
 | 
			
		||||
		"noLib": false,
 | 
			
		||||
		"outDir": "./built",
 | 
			
		||||
		"rootDir": "./"
 | 
			
		||||
	},
 | 
			
		||||
	"compileOnSave": false,
 | 
			
		||||
	"include": [
 | 
			
		||||
		"./core.ts"
 | 
			
		||||
	]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -255,8 +255,6 @@ export class ColdDeviceStorage {
 | 
			
		|||
		sound_chatBg: { type: 'syuilo/waon', volume: 1 },
 | 
			
		||||
		sound_antenna: { type: 'syuilo/triple', volume: 1 },
 | 
			
		||||
		sound_channel: { type: 'syuilo/square-pico', volume: 1 },
 | 
			
		||||
		sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
 | 
			
		||||
		sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	public static watchers = [];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4544,9 +4544,9 @@ performance-now@^2.1.0:
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
 | 
			
		||||
  integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 | 
			
		||||
 | 
			
		||||
"photoswipe@git://github.com/dimsemenov/photoswipe#v5-beta":
 | 
			
		||||
"photoswipe@git+https://github.com/dimsemenov/photoswipe#v5-beta":
 | 
			
		||||
  version "5.1.7"
 | 
			
		||||
  resolved "git://github.com/dimsemenov/photoswipe#60040164333bd257409669e715e4327afdb3aec7"
 | 
			
		||||
  resolved "git+https://github.com/dimsemenov/photoswipe#60040164333bd257409669e715e4327afdb3aec7"
 | 
			
		||||
 | 
			
		||||
picocolors@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue