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();
+});