Last active
November 19, 2024 03:26
-
-
Save longern/7182b418d813be529f06f4e488736047 to your computer and use it in GitHub Desktop.
Outliar - An easy-to-play social deduction tabletop game, playable with poker. https://outliar.longern.com
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- https://outliar.longern.com --> | |
<!-- An easy-to-play social deduction tabletop game. --> | |
<!-- Playable with poker. --> | |
<!-- 3 minutes | 4+ players --> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<meta | |
name="description" | |
content="An easy-to-play social deduction tabletop game, playable with poker." | |
/> | |
<title>Outliar</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script> | |
<script | |
src="https://cdnjs.cloudflare.com/ajax/libs/i18next/23.16.5/i18next.min.js" | |
integrity="sha512-ifgqA1ksbU4HueHvwm37fyawXj/FdO1FlRZpIdz80Ztam+ZLlmOsgckW1nSi9PN4aITOCiMJwKk533n5OW3sHQ==" | |
crossorigin="anonymous" | |
referrerpolicy="no-referrer" | |
></script> | |
<style> | |
* { | |
box-sizing: border-box; | |
} | |
body { | |
margin: 0; | |
line-height: 1.5; | |
font-family: Arial, sans-serif; | |
} | |
.container { | |
max-width: 960px; | |
width: 100%; | |
margin: 0 auto; | |
} | |
header { | |
height: 60px; | |
padding: 0 16px; | |
background-color: #f8f9fa; | |
border-bottom: 1px solid #e9ecef; | |
} | |
.toolbar { | |
display: flex; | |
align-items: center; | |
height: 100%; | |
} | |
.toolbar > a { | |
padding: 0 16px; | |
height: 100%; | |
display: flex; | |
align-items: center; | |
transition: background-color 0.2s; | |
} | |
.toolbar > a:hover { | |
background-color: rgba(0, 0, 0, 0.1); | |
} | |
main { | |
padding: 16px; | |
} | |
.h-first-child { | |
margin-top: -16px; | |
height: 1px; | |
} | |
h2, | |
h3, | |
table { | |
margin-bottom: 0.5rem; | |
} | |
p { | |
margin-top: 0; | |
margin-bottom: 0.5rem; | |
} | |
code { | |
font-family: Consolas, monospace; | |
} | |
ul, | |
ol { | |
padding-inline-start: 20px; | |
margin: 0; | |
} | |
.actions-table { | |
width: 100%; | |
table-layout: fixed; | |
text-align: center; | |
} | |
label { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
gap: 4px; | |
} | |
input, | |
select { | |
padding: 12px; | |
min-width: 120px; | |
} | |
#dealer form { | |
display: flex; | |
flex-direction: column; | |
gap: 24px; | |
width: 100%; | |
max-width: 320px; | |
align-items: stretch; | |
} | |
#dealer label { | |
align-items: stretch; | |
} | |
a { | |
all: unset; | |
cursor: pointer; | |
} | |
button { | |
padding: 0.7rem 1.5rem; | |
background-color: #007bff; | |
color: white; | |
border: none; | |
border-radius: 0.25rem; | |
cursor: pointer; | |
transition: background-color 0.2s; | |
box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 1px -2px, | |
rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, | |
rgba(0, 0, 0, 0.12) 0px 1px 5px 0px; | |
} | |
button:hover { | |
background-color: #0056b3; | |
} | |
button:disabled { | |
background-color: #6c757d; | |
cursor: not-allowed; | |
} | |
.tab:not(.active) { | |
display: none; | |
} | |
header { | |
position: sticky; | |
top: 0; | |
} | |
.main-viewport { | |
min-height: calc(100svh - 92px); | |
height: 1px; | |
} | |
.center { | |
height: 100%; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
.game-placeholder { | |
color: gray; | |
font-size: 0.9rem; | |
} | |
.moderator-actions { | |
display: flex; | |
align-self: center; | |
min-width: 320px; | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<div class="container" style="height: 100%"> | |
<div class="toolbar"> | |
<span>异类</span> | |
<div style="flex-grow: 1"></div> | |
<a href="#rules">规则</a> | |
<a href="#dealer">发牌器</a> | |
<a href="#moderator">法官</a> | |
</div> | |
</div> | |
</header> | |
<div class="container"> | |
<main> | |
<div id="rules" class="tab active"> | |
<div class="h-first-child"></div> | |
<h2><i>异类</i> 规则</h2> | |
<h3>卡组准备</h3> | |
<p> | |
每名玩家分配一种颜色,若玩家数量为 | |
<code>N</code>,则每种颜色的卡牌均有 | |
<code>N - 1</code> | |
张。卡组中还包含若干张彩色和白色卡牌。 | |
</p> | |
<p> | |
推荐配置:<br /> | |
彩色卡牌 4 人局 1 张,5 ~ 6 人局 2 张,7 人局 3 张。<br /> | |
白色卡牌 <code>N - 1</code> 张。 | |
</p> | |
<h3>游戏开始</h3> | |
<p> | |
游戏分为“同类”和“异类”两个阵营,仅有一名玩家为“异类”,其余玩家为“同类”。 | |
</p> | |
<p> | |
游戏开始时,法官随机选出一名玩家为“异类”,并分别向每名“同类”指认“异类”,向“异类”指认随机一名“同类”(玩家此时无法区分自己的身份)。 | |
每名玩家抽取 | |
<code>N - 1</code> 张牌,剩余的牌作为牌库,背面朝上摊开在桌面上。 | |
</p> | |
<h3>游戏过程</h3> | |
<p> | |
每回合所有玩家同时在下列编号为 1~5 的行动中选择一个,选择完成后按 | |
1~5 的顺序依次结算。 | |
</p> | |
<table class="actions-table"> | |
<thead> | |
<tr> | |
<th>1</th> | |
<th>2</th> | |
<th>3</th> | |
<th>4</th> | |
<th>5</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 512 512" | |
width="32" | |
height="32" | |
> | |
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc. --> | |
<path | |
d="M459.1 52.4L442.6 6.5C440.7 2.6 436.5 0 432.1 0s-8.5 2.6-10.4 6.5L405.2 52.4l-46 16.8c-4.3 1.6-7.3 5.9-7.2 10.4c0 4.5 3 8.7 7.2 10.2l45.7 16.8 16.8 45.8c1.5 4.4 5.8 7.5 10.4 7.5s8.9-3.1 10.4-7.5l16.5-45.8 45.7-16.8c4.2-1.5 7.2-5.7 7.2-10.2c0-4.6-3-8.9-7.2-10.4L459.1 52.4zm-132.4 53c-12.5-12.5-32.8-12.5-45.3 0l-2.9 2.9C256.5 100.3 232.7 96 208 96C93.1 96 0 189.1 0 304S93.1 512 208 512s208-93.1 208-208c0-24.7-4.3-48.5-12.2-70.5l2.9-2.9c12.5-12.5 12.5-32.8 0-45.3l-80-80zM200 192c-57.4 0-104 46.6-104 104l0 8c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-8c0-75.1 60.9-136 136-136l8 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-8 0z" | |
/> | |
</svg> | |
<br /> | |
引爆 | |
</td> | |
<td> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 512 512" | |
width="32" | |
height="32" | |
> | |
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc. --> | |
<path | |
d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 208c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-176c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 272c0 1.5 0 3.1 .1 4.6L67.6 283c-16-15.2-41.3-14.6-56.6 1.4s-14.6 41.3 1.4 56.6L124.8 448c43.1 41.1 100.4 64 160 64l19.2 0c97.2 0 176-78.8 176-176l0-208c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 112c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-176c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 176c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-208z" | |
/> | |
</svg> | |
<br /> | |
投票 | |
</td> | |
<td> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 576 512" | |
width="32" | |
height="32" | |
> | |
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc. --> | |
<path | |
d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z" | |
/> | |
</svg> | |
<br /> | |
查看手牌 | |
</td> | |
<td> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 448 512" | |
width="32" | |
height="32" | |
> | |
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc. --> | |
<path | |
d="M438.6 150.6c12.5-12.5 12.5-32.8 0-45.3l-96-96c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.7 96 32 96C14.3 96 0 110.3 0 128s14.3 32 32 32l306.7 0-41.4 41.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l96-96zm-333.3 352c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 416 416 416c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0 41.4-41.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-96 96c-12.5 12.5-12.5 32.8 0 45.3l96 96z" | |
/> | |
</svg> | |
<br /> | |
交换手牌 | |
</td> | |
<td> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
viewBox="0 0 576 512" | |
width="32" | |
height="32" | |
> | |
<!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc. --> | |
<path | |
d="M264.5 5.2c14.9-6.9 32.1-6.9 47 0l218.6 101c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 149.8C37.4 145.8 32 137.3 32 128s5.4-17.9 13.9-21.8L264.5 5.2zM476.9 209.6l53.2 24.6c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 277.8C37.4 273.8 32 265.3 32 256s5.4-17.9 13.9-21.8l53.2-24.6 152 70.2c23.4 10.8 50.4 10.8 73.8 0l152-70.2zm-152 198.2l152-70.2 53.2 24.6c8.5 3.9 13.9 12.4 13.9 21.8s-5.4 17.9-13.9 21.8l-218.6 101c-14.9 6.9-32.1 6.9-47 0L45.9 405.8C37.4 401.8 32 393.3 32 384s5.4-17.9 13.9-21.8l53.2-24.6 152 70.2c23.4 10.8 50.4 10.8 73.8 0z" | |
/> | |
</svg> | |
<br /> | |
交换牌库 | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
<ol> | |
<li> | |
<p>引爆</p> | |
<p>游戏立刻结束,“异类”获胜(无论谁引爆)。</p> | |
</li> | |
<li> | |
<p>投票</p> | |
<ul> | |
<li> | |
<p> | |
若有 2 | |
名玩家或以上发起投票,则所有玩家各选择一张手牌进行投票。投票选定后同时公布,并统计票数(彩色卡牌为所有人计 | |
1 票,白色卡牌不计票)。 | |
</p> | |
<p> | |
若“异类”得到 | |
<code>N - 1</code> 票或以上,则“同类”阵营获胜。<br /> | |
若“同类”得到 | |
<code>N - 1</code> 票或以上,则“异类”玩家获胜。<br /> | |
若最高票数不足 | |
<code>N - 1</code>,则游戏继续,玩家将投票收回手牌。 | |
</p> | |
</li> | |
<li> | |
<p> | |
若只有 1 | |
名玩家发起投票,则该玩家选择一名玩家,查看其所有手牌,并强制交换一张手牌。给出手牌时,必须优先选择自己颜色的手牌。 | |
</p> | |
</li> | |
</ul> | |
</li> | |
<li> | |
<p>查看手牌</p> | |
<p>查看一名玩家的所有手牌。</p> | |
</li> | |
<li> | |
<p>交换手牌</p> | |
<p> | |
选择一名玩家,双方各选择一张手牌进行交换(必须优先选择自己颜色的手牌)。 | |
</p> | |
</li> | |
<li> | |
<p>交换牌库</p> | |
<p> | |
选择一张手牌(必须优先选择自己颜色的手牌),与牌库中的一张牌进行交换,保持位置不变。 | |
</p> | |
</li> | |
</ol> | |
<p>重复进行回合,直到有一方获胜。</p> | |
<h3>常见问题</h3> | |
<ul> | |
<li> | |
<p>游戏过程中可以发言吗?</p> | |
<p>可以在任何时候发言。</p> | |
</li> | |
<li> | |
<p>在行动 2 中强制交换手牌时,必须优先获得对方颜色的手牌吗?</p> | |
<p> | |
不需要,给出的手牌必须优先选择自己的颜色,但获得的手牌没有限制。 | |
</p> | |
</li> | |
<li> | |
<p>在行动 2 中强制交换手牌时,可以放弃交换吗?</p> | |
<p>不可以,但你可以选择交换同色卡牌。</p> | |
</li> | |
</ul> | |
</div> | |
<div id="dealer" class="tab"> | |
<div class="center main-viewport"> | |
<form> | |
<label> | |
<span>种子</span> | |
<input type="text" id="seed" autocomplete="off" /> | |
</label> | |
<label> | |
<span>人数</span> | |
<select id="players"> | |
<option value="4">4</option> | |
<option value="5">5</option> | |
<option value="6">6</option> | |
<option value="7">7</option> | |
</select> | |
</label> | |
<label> | |
<span>编号</span> | |
<select id="index"></select> | |
</label> | |
<formgroup> | |
<button id="sync">同步</button> | |
<button id="next" disabled>下一局</button> | |
</formgroup> | |
<div>房间号:<span id="state"> </span></div> | |
<span id="output"> </span> | |
</form> | |
</div> | |
</div> | |
<div id="moderator" class="tab"> | |
<div | |
class="main-viewport" | |
style="display: flex; gap: 1rem; flex-direction: column" | |
> | |
<div style="flex-grow: 1"> | |
<div class="center game-placeholder">游戏未开始</div> | |
<div style="display: none"> | |
<div class="h-first-child"></div> | |
<h3>身份信息</h3> | |
<p> | |
本局 | |
<span class="outliar">?</span> | |
号玩家为“异类”,其余玩家为“同类”。 | |
</p> | |
<p> | |
向“同类”指认“异类”,向“异类”指认 | |
<span class="fake-outliar">*</span> 号玩家。 | |
</p> | |
<h3>游戏流程</h3> | |
<p>所有玩家闭眼。</p> | |
<ol | |
style="list-style-type: none; padding: 0" | |
id="progress" | |
></ol> | |
<p>所有玩家睁眼。</p> | |
</div> | |
</div> | |
<div class="moderator-actions"> | |
<label style="flex-direction: row; flex-grow: 1"> | |
<select id="moderator-players"> | |
<option value="4">4</option> | |
<option value="5">5</option> | |
<option value="6">6</option> | |
<option value="7">7</option> | |
</select> | |
<span>人局</span> | |
</label> | |
<button id="moderator-next">开始</button> | |
</div> | |
</div> | |
</div> | |
</main> | |
</div> | |
<script> | |
let rng; | |
let players; | |
document.getElementById("sync").addEventListener("click", function (e) { | |
e.preventDefault(); | |
const seed = document.getElementById("seed").value; | |
players = parseInt(document.getElementById("players").value); | |
const index = parseInt(document.getElementById("index").value); | |
if (!seed || isNaN(players) || isNaN(index)) return; | |
rng = new Math.seedrandom(seed + players.toString(), { state: true }); | |
const state = document.getElementById("state"); | |
state.textContent = rng | |
.state() | |
.S.slice(0, 4) | |
.map((s) => s.toString(16).padStart(2, "0")) | |
.join(""); | |
document.getElementById("next").removeAttribute("disabled"); | |
document.getElementById("sync").setAttribute("disabled", true); | |
}); | |
function generateOutliar(rng, players) { | |
const outliar = Math.floor(rng() * players) + 1; | |
let fakeOutliar = Math.floor(rng() * (players - 1)) + 1; | |
if (fakeOutliar === outliar) fakeOutliar = players; | |
return { outliar, fakeOutliar }; | |
} | |
let round = 0; | |
document | |
.getElementById("next") | |
.addEventListener("click", function (event) { | |
event.preventDefault(); | |
const { outliar, fakeOutliar } = generateOutliar(rng, players); | |
const index = parseInt(document.getElementById("index").value); | |
const output = document.getElementById("output"); | |
round++; | |
if (index === outliar) { | |
output.textContent = `Round: ${round} Outliar: ${fakeOutliar}`; | |
} else { | |
output.textContent = `Round: ${round} Outliar: ${outliar}`; | |
} | |
}); | |
document | |
.getElementById("moderator-next") | |
.addEventListener("click", function (event) { | |
event.preventDefault(); | |
this.setAttribute("disabled", true); | |
const gamePlaceholder = document.querySelector(".game-placeholder"); | |
gamePlaceholder.style.display = "flex"; | |
gamePlaceholder.nextElementSibling.style.display = "none"; | |
setTimeout(() => { | |
const players = parseInt( | |
document.getElementById("moderator-players").value | |
); | |
const { outliar, fakeOutliar } = generateOutliar( | |
Math.random, | |
players | |
); | |
for (element of document.querySelectorAll(".outliar")) { | |
element.textContent = outliar; | |
} | |
document.querySelector(".fake-outliar").textContent = fakeOutliar; | |
const progress = document.getElementById("progress"); | |
const children = Array.from({ length: players }).map((_, i) => { | |
const index = i + 1; | |
const li = document.createElement("li"); | |
const p = document.createElement("p"); | |
p.textContent = `${index} 号玩家睁眼。(指认 ${ | |
index !== outliar ? outliar : fakeOutliar | |
} 号玩家)${index} 号玩家闭眼。`; | |
li.appendChild(p); | |
return li; | |
}); | |
progress.replaceChildren(...children); | |
const gamePlaceholder = document.querySelector(".game-placeholder"); | |
gamePlaceholder.style.display = "none"; | |
gamePlaceholder.nextElementSibling.style.display = "block"; | |
this.removeAttribute("disabled"); | |
}, 500); | |
}); | |
function createIndexOptions() { | |
const players = parseInt(document.getElementById("players").value); | |
const index = document.getElementById("index"); | |
index.replaceChildren(); | |
const placeholder = document.createElement("option"); | |
placeholder.value = ""; | |
placeholder.textContent = "-"; | |
placeholder.disabled = true; | |
placeholder.selected = true; | |
index.appendChild(placeholder); | |
for (let i = 1; i <= players; i++) { | |
const option = document.createElement("option"); | |
option.value = i; | |
option.textContent = i; | |
index.appendChild(option); | |
} | |
} | |
document | |
.getElementById("players") | |
.addEventListener("change", createIndexOptions); | |
createIndexOptions(); | |
function toggleTab() { | |
const id = window.location.hash.slice(1); | |
if (!id) return; | |
document.querySelector(".tab.active").classList.remove("active"); | |
document.getElementById(id).classList.add("active"); | |
} | |
window.addEventListener("hashchange", toggleTab); | |
toggleTab(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment