bye reversi
This commit is contained in:
		
							parent
							
								
									45211e14b3
								
							
						
					
					
						commit
						b267a504ca
					
				
					 39 changed files with 3 additions and 5226 deletions
				
			
		|  | @ -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; | ||||
|  |  | |||
|  | @ -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', | ||||
|  |  | |||
|  | @ -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 = []; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue