diff --git a/locales/ja.yml b/locales/ja.yml index 9760f976ae..936bdea214 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -182,6 +182,10 @@ common/views/components/games/reversi/reversi.vue: waiting-for: "{}を待っています" cancel: "キャンセル" +common/views/components/games/reversi/reversi.game.vue: + surrender: "投了" + surrendered: "投了により" + common/views/components/games/reversi/reversi.index.vue: title: "Misskey Reversi" sub-title: "他のMisskeyユーザーとリバーシで対戦しよう" diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue index bbfec2c1cc..66973e1970 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.game.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue @@ -1,6 +1,6 @@ <template> <div class="xqnhankfuuilcwvhgsopeqncafzsquya"> - <header><b>{{ blackUser | userName }}</b>(%i18n:common.reversi.black%) vs <b>{{ whiteUser | userName }}</b>(%i18n:common.reversi.white%)</header> + <header><b><router-link :to="blackUser | userPage">{{ blackUser | userName }}</router-link></b>(%i18n:common.reversi.black%) vs <b><router-link :to="whiteUser | userPage">{{ whiteUser | userName }}</router-link></b>(%i18n:common.reversi.white%)</header> <div style="overflow: hidden"> <p class="turn" v-if="!iAmPlayer && !game.isEnded">{{ '%i18n:common.reversi.turn-of%'.replace('{}', $options.filters.userName(turnUser)) }}<mk-ellipsis/></p> @@ -8,7 +8,10 @@ <p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">%i18n:common.reversi.opponent-turn%<mk-ellipsis/></p> <p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">%i18n:common.reversi.my-turn%</p> <p class="result" v-if="game.isEnded && logPos == logs.length"> - <template v-if="game.winner">{{ '%i18n:common.reversi.won%'.replace('{}', $options.filters.userName(game.winner)) }}{{ game.settings.isLlotheo ? ' (ロセオ)' : '' }}</template> + <template v-if="game.winner"> + <span>{{ '%i18n:common.reversi.won%'.replace('{}', $options.filters.userName(game.winner)) }}</span> + <span v-if="game.surrendered != null"> (%i18n:@surrendered%)</span> + </template> <template v-else>%i18n:common.reversi.drawn%</template> </p> </div> @@ -41,6 +44,10 @@ <p class="status"><b>{{ '%i18n:common.reversi.this-turn%'.split('{}')[0] }}{{ logPos }}{{ '%i18n:common.reversi.this-turn%'.split('{}')[1] }}</b> %i18n:common.reversi.black%:{{ o.blackCount }} %i18n:common.reversi.white%:{{ o.whiteCount }} %i18n:common.reversi.total%:{{ o.blackCount + o.whiteCount }}</p> + <div class="actions" v-if="!game.isEnded && iAmPlayer"> + <form-button @click="surrender">%i18n:@surrender%</form-button> + </div> + <div class="player" v-if="game.isEnded"> <el-button-group> <el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button> @@ -79,22 +86,27 @@ export default Vue.extend({ if (!this.$store.getters.isSignedIn) return false; return this.game.user1Id == this.$store.state.i.id || this.game.user2Id == this.$store.state.i.id; }, + myColor(): Color { if (!this.iAmPlayer) return null; if (this.game.user1Id == this.$store.state.i.id && this.game.black == 1) return true; if (this.game.user2Id == this.$store.state.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; }, + turnUser(): any { if (this.o.turn === true) { return this.game.black == 1 ? this.game.user1 : this.game.user2; @@ -104,11 +116,13 @@ export default Vue.extend({ return null; } }, + isMyTurn(): boolean { if (!this.iAmPlayer) return false; if (this.turnUser == null) return false; return this.turnUser.id == this.$store.state.i.id; }, + cellsStyle(): any { return { 'grid-template-rows': `repeat(${this.game.settings.map.length}, 1fr)`, @@ -165,11 +179,13 @@ export default Vue.extend({ mounted() { this.connection.on('set', this.onSet); this.connection.on('rescue', this.onRescue); + this.connection.on('ended', this.onEnded); }, beforeDestroy() { this.connection.off('set', this.onSet); this.connection.off('rescue', this.onRescue); + this.connection.off('ended', this.onEnded); clearInterval(this.pollingClock); }, @@ -215,6 +231,10 @@ export default Vue.extend({ } }, + onEnded(x) { + this.game = x.game; + }, + checkEnd() { this.game.isEnded = this.o.isEnded; if (this.game.isEnded) { @@ -250,6 +270,12 @@ export default Vue.extend({ this.checkEnd(); this.$forceUpdate(); + }, + + surrender() { + (this as any).api('games/reversi/games/surrender', { + gameId: this.game.id + }); } } }); @@ -265,6 +291,9 @@ root(isDark) padding 8px border-bottom dashed 1px isDark ? #4c5761 : #c4cdd4 + a + color inherit + > .board width calc(100% - 16px) max-width 500px @@ -381,6 +410,9 @@ root(isDark) margin 0 padding 16px 0 + > .actions + padding-bottom 16px + > .player padding-bottom 32px diff --git a/src/models/games/reversi/game.ts b/src/models/games/reversi/game.ts index 8255db0584..6a6c6463d9 100644 --- a/src/models/games/reversi/game.ts +++ b/src/models/games/reversi/game.ts @@ -25,6 +25,7 @@ export interface IReversiGame { isStarted: boolean; isEnded: boolean; winnerId: mongo.ObjectID; + surrendered: mongo.ObjectID; logs: Array<{ at: Date; color: boolean; diff --git a/src/server/api/endpoints/games/reversi/games/surrender.ts b/src/server/api/endpoints/games/reversi/games/surrender.ts new file mode 100644 index 0000000000..dc9908aef1 --- /dev/null +++ b/src/server/api/endpoints/games/reversi/games/surrender.ts @@ -0,0 +1,59 @@ +import $ from 'cafy'; import ID from '../../../../../../misc/cafy-id'; +import ReversiGame, { pack } from '../../../../../../models/games/reversi/game'; +import { ILocalUser } from '../../../../../../models/user'; +import getParams from '../../../../get-params'; +import { publishReversiGameStream } from '../../../../../../stream'; + +export const meta = { + desc: { + ja: '指定したリバーシの対局で投了します。' + }, + + requireCredential: true, + + params: { + gameId: $.type(ID).optional.note({ + desc: { + ja: '投了したい対局' + } + }) + } +}; + +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + const game = await ReversiGame.findOne({ _id: ps.gameId }); + + if (game == null) { + return rej('game not found'); + } + + if (game.isEnded) { + return rej('this game is already ended'); + } + + if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) { + return rej('access denied'); + } + + const winnerId = game.user1Id.equals(user._id) ? game.user2Id : game.user1Id; + + await ReversiGame.update({ + _id: game._id + }, { + $set: { + surrendered: user._id, + isEnded: true, + winnerId: winnerId + } + }); + + publishReversiGameStream(game._id, 'ended', { + winnerId: winnerId, + game: await pack(game._id, user) + }); + + res(); +});