【可能是己亥年最有趣的比赛】【狼人杀模拟器】第一名题解
1509
发布于 未知归属地

昨天收到了奖品,2019 年限量版狼人杀。可惜只能和买了 8 年没拆封的《三国杀》放在一起——没人陪我玩。

我做完的时候,看到成功提交的数目是2,还一直以为自己是第二名,直到看到了“【可能是己亥年最有趣的比赛】最终排名&获奖名单公布”。可能在我之前的那个提交,就是官方出题人员的自验证吧。

单题排名

Q2: 狼人杀模拟器

  • 第一名: @yanqd0 共计时:1:58:39
  • 第二名: @neomao 共计时:2:17:21
  • 第三名: @wifiii 共计时:3:57:12

以上三名选手每人将获得限量版狼人杀卡牌一套,出题老师说想和你们一起玩狼人杀~

我想对出题老师说,其实我不会玩《狼人杀》。当时审题就花了十几分钟,没想到能按时做出来。

一方面,我用的是 Python,而不是 C++ 或 Java。Python 的表现力,非常适合快速解决这类业务逻辑复杂、但总体复杂度不高的问题。54 人尝试却只有 6 人通过,估计大多是栽在了编程语言的选择上。C++ 和 Java 很适合做常规算法题,而不适合快速实现这种小项目 demo。

另一方面,应该是简洁的抽象。我看了一下官方题解:《【可能是己亥年最有趣的比赛】— 狼人杀模拟器》,也是 Python,接近 200 行,而我只用了 100 行出头。因为官方其实只做了两层抽象:主要流程,及其实现细节;而我则基本做到了 DRY。这种比速度的竞赛,少10行就是少很多,少 100 行……

分析

首先是设计数据结构。作为入参的 playerscredibility 是不足以表达是否存活的。一种方案是把死人删除,但 players 可以删,credibility 却不行,因为死人的 credibility 也是有效信息,而二者是靠座位(下标)联系。我用的方案是 self.vilsself.wws 保存存活的村民与狼人的座位。这样,杀人就是删除元素,判断玩家存活 alive 和游戏结束条件 check 也比较方便实现。

其次是设计杀人的处理。晚上杀人是狼人杀村民 kill_vil,白天杀人是村民杀狼人 kill_wws(尽管可能杀错)。官方题解中,白天杀人是叫 vote,但不足以表达一种对称性。两种杀法,最终都会有共同操作,我定义为 kill,做真正的数据操作与公共处理。这当然不是一个好的设计,无论是从面相对象编程,还是从函数式编程的角度,但这种面向过程的简单设计,无论是思考还是实现都是最快的。

最后就是处理三种特殊情况bear 是最麻烦的,要么死在第一天晚上,要么死在第二天晚上,而且还会有后期效果,影响 credibilityhunteridiot 则简单多了,前者作用于 kill 中,后者作用于 kill_vil 中。hunter 杀人应该算第三种杀人,但基本可以复用 kill_ww,只是要处理 idiot 的情况——投票不能杀 idiot,而 hunter 可以。

如果换一个思维敏捷的高手,有《狼人杀》游戏经验以便于快速审题,并且用这个方法实现,应该可以在 1 小时内完成。

代码

Python
class Solution:
    def canVillagersWin(self, players: List[str], credibility: List[int]) -> bool:
        self.players = players
        self.creds = credibility
        self.bear = players.index('bear')
        self.vils = [i for i, player in enumerate(players) if player != 'ww']
        self.wws = [i for i, player in enumerate(players) if player == 'ww']
        self.bear_sides = None

        for i in range(6):
            for time in (self.night, self.day):
                end, status = time(i)
                if end:
                    return status
        return False

    def night(self, i):
        if i == 1 and self.bear in self.vils:
            self.kill(self.bear)
        else:
            self.kill_vil()
        if i == 0:
            self.bear_warn()
        return self.check()

    def day(self, _):
        self.kill_ww()
        return self.check()

    def kill_vil(self):
        index, cred = None, -math.inf
        for i in self.vils:
            if cred < self.creds[i]:
                index, cred = i, self.creds[i]
        self.creds[index] = 100
        self.kill(index)

    def kill_ww(self, hunter=False):
        index, cred = None, math.inf
        for i, j in enumerate(self.creds):
            if self.alive(i) and cred > j:
                index, cred = i, j

        if not hunter and self.players[index] == 'idiot':
            self.creds[index] = 100
            self.bear_check(i)
        else:
            self.kill(index)

    def kill(self, i):
        if self.players[i] == 'ww':
            self.wws.remove(i)
        else:
            self.vils.remove(i)

        self.bear_check(i)
        self.hunter_check(i)

    def bear_warn(self):
        if self.bear not in self.vils:
            return

        left = (self.bear - 1) % 12
        if not self.alive(left):
            left = (self.bear - 2) % 12
        right = (self.bear + 1) % 12
        if not self.alive(right):
            right = (self.bear + 2) % 12
        sides = (left, right)

        warn = any(self.players[i] == 'ww' for i in sides)
        if warn:
            self.bear_sides = sides
            for i in sides:
                self.creds[i] //= 2
                self.creds[i] = self.creds[i] or 1
        else:
            self.creds[left] = 100
            self.creds[right] = 100

    def bear_check(self, i):
        if not self.bear_sides or i not in self.bear_sides or self.creds[i] != 100:
            return
        for j in self.bear_sides:
            if j != i:
                self.creds[j] = 0

    def hunter_check(self, i):
        if self.players[i] == 'hunter':
            self.creds[i] = 100
            self.bear_check(i)
            self.kill_ww(hunter=True)

    def check(self):
        status = None
        if not self.wws:
            status = True
        elif len(self.wws) >= len(self.vils):
            status = False
        if status is None:
            return False, None
        else:
            return True, status

    def alive(self, i):
        return i in self.vils or i in self.wws

我写的不是代码,而是寂寞。

评论 (0)