diff --git a/src/api/models/othello-game.ts b/src/api/models/othello-game.ts
index a8c3025108..ab90cffa44 100644
--- a/src/api/models/othello-game.ts
+++ b/src/api/models/othello-game.ts
@@ -33,6 +33,8 @@ export interface IGame {
 		can_put_everywhere: boolean;
 		looped_board: boolean;
 	};
+	form1: any;
+	form2: any;
 }
 
 /**
diff --git a/src/api/stream/othello-game.ts b/src/api/stream/othello-game.ts
index 5f61f0cc2c..888c599338 100644
--- a/src/api/stream/othello-game.ts
+++ b/src/api/stream/othello-game.ts
@@ -31,6 +31,21 @@ export default function(request: websocket.request, connection: websocket.connec
 				updateSettings(msg.settings);
 				break;
 
+			case 'init-form':
+				if (msg.body == null) return;
+				initForm(msg.body);
+				break;
+
+			case 'update-form':
+				if (msg.id == null || msg.value === undefined) return;
+				updateForm(msg.id, msg.value);
+				break;
+
+			case 'message':
+				if (msg.body == null) return;
+				message(msg.body);
+				break;
+
 			case 'set':
 				if (msg.pos == null) return;
 				set(msg.pos);
@@ -55,6 +70,67 @@ export default function(request: websocket.request, connection: websocket.connec
 		publishOthelloGameStream(gameId, 'update-settings', settings);
 	}
 
+	async function initForm(form) {
+		const game = await Game.findOne({ _id: gameId });
+
+		if (game.is_started) return;
+		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+
+		const set = game.user1_id.equals(user._id) ? {
+			form1: form
+		} : {
+			form2: form
+		};
+
+		await Game.update({ _id: gameId }, {
+			$set: set
+		});
+
+		publishOthelloGameStream(gameId, 'init-form', {
+			user_id: user._id,
+			form
+		});
+	}
+
+	async function updateForm(id, value) {
+		const game = await Game.findOne({ _id: gameId });
+
+		if (game.is_started) return;
+		if (!game.user1_id.equals(user._id) && !game.user2_id.equals(user._id)) return;
+
+		const form = game.user1_id.equals(user._id) ? game.form2 : game.form1;
+
+		const item = form.find(i => i.id == id);
+
+		if (item == null) return;
+
+		item.value = value;
+
+		const set = game.user1_id.equals(user._id) ? {
+			form2: form
+		} : {
+			form1: form
+		};
+
+		await Game.update({ _id: gameId }, {
+			$set: set
+		});
+
+		publishOthelloGameStream(gameId, 'update-form', {
+			user_id: user._id,
+			id,
+			value
+		});
+	}
+
+	async function message(message) {
+		message.id = Math.random();
+		publishOthelloGameStream(gameId, 'message', {
+			user_id: user._id,
+			message
+		});
+	}
+
 	async function accept(accept: boolean) {
 		const game = await Game.findOne({ _id: gameId });
 
diff --git a/src/web/app/common/views/components/othello.room.vue b/src/web/app/common/views/components/othello.room.vue
index dfdc43ef96..bdefcdc49f 100644
--- a/src/web/app/common/views/components/othello.room.vue
+++ b/src/web/app/common/views/components/othello.room.vue
@@ -2,37 +2,77 @@
 <div class="root">
 	<header><b>{{ game.user1.name }}</b> vs <b>{{ game.user2.name }}</b></header>
 
-	<p>ゲームの設定</p>
+	<div>
+		<p>ゲームの設定</p>
 
-	<el-select class="map" v-model="mapName" placeholder="マップを選択" @change="onMapChange">
-		<el-option label="ランダム" :value="null"/>
-		<el-option-group v-for="c in mapCategories" :key="c" :label="c">
-			<el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">
-				<span style="float: left">{{ m.name }}</span>
-				<span style="float: right; color: #8492a6; font-size: 13px" v-if="m.author">(by <i>{{ m.author }}</i>)</span>
-			</el-option>
-		</el-option-group>
-	</el-select>
+		<el-card class="map">
+			<div slot="header">
+				<el-select :class="$style.mapSelect" v-model="mapName" placeholder="マップを選択" @change="onMapChange">
+					<el-option label="ランダム" :value="null"/>
+					<el-option-group v-for="c in mapCategories" :key="c" :label="c">
+						<el-option v-for="m in maps" v-if="m.category == c" :key="m.name" :label="m.name" :value="m.name">
+							<span style="float: left">{{ m.name }}</span>
+							<span style="float: right; color: #8492a6; font-size: 13px" v-if="m.author">(by <i>{{ m.author }}</i>)</span>
+						</el-option>
+					</el-option-group>
+				</el-select>
+			</div>
+			<div :class="$style.board" v-if="game.settings.map != null" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
+				<div v-for="(x, i) in game.settings.map.join('')"
+					:class="{ none: x == ' ' }"
+					@click="onPixelClick(i, x)"
+				>
+					<template v-if="x == 'b'">%fa:circle%</template>
+					<template v-if="x == 'w'">%fa:circle R%</template>
+				</div>
+			</div>
+		</el-card>
 
-	<div class="board" v-if="game.settings.map != null" :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }">
-		<div v-for="(x, i) in game.settings.map.join('')"
-			:class="{ none: x == ' ' }"
-			@click="onPixelClick(i, x)"
-		>
-			<template v-if="x == 'b'">%fa:circle%</template>
-			<template v-if="x == 'w'">%fa:circle R%</template>
-		</div>
-	</div>
-
-	<div class="rules">
-		<mk-switch v-model="game.settings.is_llotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
-		<mk-switch v-model="game.settings.looped_board" @change="updateSettings" text="ループマップ"/>
-		<mk-switch v-model="game.settings.can_put_everywhere" @change="updateSettings" text="どこでも置けるモード"/>
-		<div>
+		<el-card class="bw">
+			<div slot="header">
+				<span>先手/後手</span>
+			</div>
 			<el-radio v-model="game.settings.bw" label="random" @change="updateSettings">ランダム</el-radio>
 			<el-radio v-model="game.settings.bw" :label="1" @change="updateSettings">{{ game.user1.name }}が黒</el-radio>
 			<el-radio v-model="game.settings.bw" :label="2" @change="updateSettings">{{ game.user2.name }}が黒</el-radio>
-		</div>
+		</el-card>
+
+		<el-card class="rules">
+			<div slot="header">
+				<span>ルール</span>
+			</div>
+			<mk-switch v-model="game.settings.is_llotheo" @change="updateSettings" text="石の少ない方が勝ち(ロセオ)"/>
+			<mk-switch v-model="game.settings.looped_board" @change="updateSettings" text="ループマップ"/>
+			<mk-switch v-model="game.settings.can_put_everywhere" @change="updateSettings" text="どこでも置けるモード"/>
+		</el-card>
+
+		<el-card class="bot-form" v-if="form">
+			<div slot="header">
+				<span>Botの設定</span>
+			</div>
+			<el-alert v-for="message in messages"
+				:title="message.text"
+				:type="message.type"
+				:key="message.id"
+			/>
+			<template v-for="item in form">
+				<mk-switch v-if="item.type == 'button'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm($event, item)">{{ item.desc || '' }}</mk-switch>
+
+				<el-card v-if="item.type == 'radio'" :key="item.id">
+					<div slot="header">
+						<span>{{ item.label }}</span>
+					</div>
+					<el-radio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :label="r.value" @change="onChangeForm($event, item)">{{ r.label }}</el-radio>
+				</el-card>
+
+				<el-card v-if="item.type == 'textbox'" :key="item.id">
+					<div slot="header">
+						<span>{{ item.label }}</span>
+					</div>
+					<el-input v-model="item.value" @change="onChangeForm($event, item)"/>
+				</el-card>
+			</template>
+		</el-card>
 	</div>
 
 	<footer>
@@ -64,7 +104,9 @@ export default Vue.extend({
 			o: null,
 			isLlotheo: false,
 			mapName: maps.eighteight.name,
-			maps: maps
+			maps: maps,
+			form: null,
+			messages: []
 		};
 	},
 
@@ -88,11 +130,56 @@ export default Vue.extend({
 	created() {
 		this.connection.on('change-accepts', this.onChangeAccepts);
 		this.connection.on('update-settings', this.onUpdateSettings);
+		this.connection.on('init-form', this.onInitForm);
+		this.connection.on('message', this.onMessage);
+
+		if (this.game.user1_id != (this as any).os.i.id && this.game.settings.form1) this.form = this.game.settings.form1;
+		if (this.game.user2_id != (this as any).os.i.id && this.game.settings.form2) this.form = this.game.settings.form2;
+
+		// for debugging
+		if ((this as any).os.i.username == 'test1') {
+			setTimeout(() => {
+				this.connection.send({
+					type: 'init-form',
+					body: [{
+						id: 'button1',
+						type: 'button',
+						label: 'Enable hoge',
+						value: false
+					}, {
+						id: 'radio1',
+						type: 'radio',
+						label: '強さ',
+						value: 2,
+						items: [{
+							label: '弱',
+							value: 1
+						}, {
+							label: '中',
+							value: 2
+						}, {
+							label: '強',
+							value: 3
+						}]
+					}]
+				});
+
+				this.connection.send({
+					type: 'message',
+					body: {
+						text: 'Hey',
+						type: 'info'
+					}
+				});
+			}, 2000);
+		}
 	},
 
 	beforeDestroy() {
 		this.connection.off('change-accepts', this.onChangeAccepts);
 		this.connection.off('update-settings', this.onUpdateSettings);
+		this.connection.off('init-form', this.onInitForm);
+		this.connection.off('message', this.onMessage);
 	},
 
 	methods: {
@@ -135,6 +222,24 @@ export default Vue.extend({
 			}
 		},
 
+		onInitForm(x) {
+			if (x.user_id == (this as any).os.i.id) return;
+			this.form = x.form;
+		},
+
+		onMessage(x) {
+			if (x.user_id == (this as any).os.i.id) return;
+			this.messages.unshift(x.message);
+		},
+
+		onChangeForm(v, item) {
+			this.connection.send({
+				type: 'update-form',
+				id: item.id,
+				value: v
+			});
+		},
+
 		onMapChange(v) {
 			if (v == null) {
 				this.game.settings.map = null;
@@ -168,40 +273,21 @@ export default Vue.extend({
 
 .root
 	text-align center
+	background #f9f9f9
 
 	> header
 		padding 8px
 		border-bottom dashed 1px #c4cdd4
 
-	> .map
-		width 300px
+	> div
+		padding 0 16px
 
-	> .board
-		display grid
-		grid-gap 4px
-		width 300px
-		height 300px
-		margin 16px auto
-
-		> div
-			background transparent
-			border solid 2px #ddd
-			border-radius 6px
-			overflow hidden
-			cursor pointer
-
-			*
-				pointer-events none
-				user-select none
-				width 100%
-				height 100%
-
-			&.none
-				border-color transparent
-
-	> .rules
-		max-width 300px
-		margin 0 auto 32px auto
+		> .map
+		> .bw
+		> .rules
+		> .bot-form
+			max-width 400px
+			margin 0 auto 16px auto
 
 	> footer
 		position sticky
@@ -213,3 +299,37 @@ export default Vue.extend({
 		> .status
 			margin 0 0 16px 0
 </style>
+
+<style lang="stylus" module>
+.mapSelect
+	width 100%
+
+.board
+	display grid
+	grid-gap 4px
+	width 300px
+	height 300px
+	margin 0 auto
+
+	> div
+		background transparent
+		border solid 2px #ddd
+		border-radius 6px
+		overflow hidden
+		cursor pointer
+
+		*
+			pointer-events none
+			user-select none
+			width 100%
+			height 100%
+
+		&.none
+			border-color transparent
+
+</style>
+
+<style lang="stylus">
+.el-alert__content
+	position initial !important
+</style>