← 海盗分金起始页

要写的只有两个函数

手动玩的时候你做两件事:轮到你提案时填一组数,轮到你投票时点同意或反对。写代码就是把这两件事各写成一个函数,由裁判在该你决策的时候调用。

propose(state):你是提案人

返回一个数组,长度等于座位数 state.n,第 i 项是给 i 号座位几枚。已经下海的座位填 0,其余是非负整数,加起来正好 state.coins(100)。

// 最简单的提案:在船的人平分,余数归自己 function propose(state) { const share = new Array(state.n).fill(0); const each = Math.floor(state.coins / state.alive.length); state.alive.forEach(s => share[s - 1] = each); share[state.seat - 1] += state.coins - each * state.alive.length; return share; }

vote(state, proposal):轮到你投票

第二个参数就是当前的提案数组。返回 true 同意、false 反对。你自己的提案也会来问你,别忘了给自己投同意。

// 最简单的投票:给我的不少于均分的一半就同意 function vote(state, proposal) { if (state.seat === state.proposer) return true; const mine = proposal[state.seat - 1]; const fair = state.coins / state.alive.length; return mine >= fair / 2; }

这两段合起来就是一份能跑的代码,也就是模板里的"均分派"。把它载入下面的编辑器,改一个数,让代码替你坐一桌,看看差别。

state 里有什么

每次调用你的函数,裁判都把"你此刻能看到的一切"装在 state 里递给你。这些就是你在手动玩时看到的信息,一样不多、一样不少。

字段含义常见用法
n这桌几个座位,3 到 7提案数组的长度
seat我这一场坐几号(1 到 n)proposal[state.seat - 1] 是给我的
coins本场金币,100提案总和必须等于它
alive还在船上的座位号,升序只给这些座位分钱;长度就是在船人数
proposer这一轮谁提案(座位号)等于 seat 时是我自己的提案
players[{ seat, id, alive, lives }],每个座位是谁(海盗名:黑胡子、红胡子…)、在不在船上、剩几条命把座位号换成海盗名,才能跨场认人
me / lives我的海盗名;我剩几条命最后一条命时保守一点
game第几场,从 1 起。至少 n 场,之后随机结束,总场数不告诉你—
rounds这一场之前几轮:[{ proposer, proposal, votes: { 座位: true/false }, passed }]这一场谁已经下海、谁否了谁
history这一桌之前几场:[{ players, rounds, payoff: { 海盗名: 枚数 } }]谁给过我 0、谁接受过多少,都从这里翻
tableScore这一桌到目前每人拿了多少:{ 海盗名: 枚数 }看谁领先
secret一个只有你知道、这桌不变的整数开局"抽签"决定这桌当硬汉还是软柿子
random()有种子的随机数,0 到 1想随机就用它,跑分才能复现

座位号每场都变,海盗名不变。history 里每一场都带着当时的 players,所以"上一场给我 0 的人这一场坐几号"是查得出来的。

从零写一份:三步

  1. 先定一条门槛。投票时只看给我几枚:低于某个数就反对。提案时给自己多留,给别人刚够过门槛的数。这一步只用 seat、alive、coins。
  2. 再看这一场发生了什么。rounds 里有这一场之前几轮谁提了什么、谁否了谁。船上人少了,票也少了,门槛可以跟着变。
  3. 最后翻这桌的历史。history 里有之前每一场。谁给过我 0,谁接受过 5 枚,谁把人扔下海过,都在里面。用海盗名认人,别用座位号。

第 3 步的例子:翻历史找出给过我 0 的人

// 之前几场里,海盗名为 id 的人当提案人时,有没有给过我 0 function gaveMeZero(state, id) { for (const g of state.history) { const me = g.players.find(p => p.id === state.me); // 那一场我坐几号 const him = g.players.find(p => p.id === id); // 那一场他坐几号 if (!me || !him) continue; // 有人那场不在船上 for (const r of g.rounds) if (r.proposer === him.seat && r.proposal && r.proposal[me.seat - 1] === 0) return true; } return false; } function vote(state, proposal) { if (state.seat === state.proposer) return true; const proposerId = state.players[state.proposer - 1].id; if (gaveMeZero(state, proposerId)) return false; // 记仇 return proposal[state.seat - 1] >= 0.6 * state.coins / state.alive.length; }

模板里带了几个小工具函数(均分、座位和海盗名互查、翻历史,上面的 gaveMeZero 就在里面),载入编辑器就能直接用。

出错怎么算,常见的错

提案不合法、抛异常或超时,当作被否:你下海、掉一条命。投票出错当作反对。提交前会先和机器人试打几桌,编译不过或任何一次出错的不收。

长度不对

数组长度必须等于 state.n,不是在船人数。下海的座位也要占位,填 0。

总和不是 100

平分有余数,记得把余数给某个人。用 Math.floor 分完再把差额补上。

给了下海的人

只能给 state.alive 里的座位。已下海的座位不是 0 就算不合法。

把 5 写死

桌上 3 到 7 人都会遇到。用 state.n 和 state.alive.length,别写 5。

用座位号认人

座位每场轮转。跨场认人用 players[i].id 的海盗名。

用 Math.random

不禁止,但那部分结果不可复现,核对成绩时对不上。用 state.random()。

编辑器

从模板开始改。写完先"让代码替我坐一桌"看一桌的逐轮日志,再"本地跑 200 桌"看平均成绩。代码自动存在浏览器里。

提交

填昵称、选头像,点"提交"。提交前会和机器人试打 3 到 7 人各一桌,跑不通的不收。再提交会覆盖上一份。正式比赛在锦标赛页组桌开打。

头像
主色(帽子 / 身体)
副色(衣服 / 配饰)
(昵称)

头像和颜色随昵称一起存,再提交可以换。同款靠颜色区分。

身份码(昵称是谁的,靠它认)

这台设备的身份码:
第一次用某个昵称提交时,服务器把昵称和你的身份码绑定,别人用同一个昵称提交会被拒。换设备时把身份码抄过去填在下面,就还是同一个人。身份码丢了就换个昵称。节点农场用的是同一个身份码。

已经交了的人

正在读取…