bye reversi
This commit is contained in:
		| @@ -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>; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo