面试问题:一款双人纸牌游戏
我在interviewing.io上邀请面试者编写一个双人卡牌游戏的模拟程序。这是一个引人入胜的问题,能够揭示很多关于他们能力的信息。它不涉及任何技巧,也不涉及任何随机算法知识。它适合任何技能水平的程序员,并且支持所有常用语言。
我想分享这个问题以及我的一些见解。
双人纸牌游戏
我在提出这个问题时,会先强调代码的设计和结构。我不需要一个可以运行的程序,但需要知道它的入口点以及如何使用。
以下是游戏模拟的描述。
- 这是一个双人纸牌游戏
- 游戏从一副牌开始
- 牌发给两位玩家
- 每回合:
- 两位玩家都翻开自己最上面的牌
- 持有较高牌值的玩家拿走这些牌并将其放入自己的得分堆(每张牌得 1 分)
- 这种情况持续到玩家没有剩余的牌
- 得分最高的玩家获胜
它被认为是一种模拟,因为玩家没有任何选择。我们不需要担心输入。
此描述中存在一些未解答的问题,或者可能存在一些含糊之处。其中一些并非有意为之,但效果良好。候选人必须识别并解决需求中的问题。
根据这个描述,我希望人们能够编写模拟游戏的代码:写出谁在一轮游戏中获胜。
设计
受访者提问是好事,但不一定能揭示问题。我希望他们至少能得到一些反馈,表明他们理解了问题。除此之外,我比较灵活,因为我见过几种不同的解决问题的方式。我唯一看到的确定的模式是过度设计的负面模式:试图把所有事情都计划好,并写大量的笔记。如果人们在这里花的时间太长,我会鼓励他们开始写代码。
这个问题的复杂性不需要太多的前期工作。除了想出几个类,比如 和Player
,Game
或者Card
,你就可以开始写代码了。最好的方法是用实际代码来概括结构,许多表现优秀的考生都这么做。代码通常是最好的伪代码:在规划时,只需省略细节并走捷径即可。
卡片
一些候选人会从底层开始:深入研究卡片的定义,或者创建一个Deck
类。我会问他们为什么要创建这些类,暗示这不是一个好方法。我鼓励他们从更高的层次思考,想想他们真正需要什么。
尤其是在时间紧迫的情况下,最好自上而下地思考,忽略细节,直到需要它们为止。也就是说,不要设计理论Deck
类的所有方面,因为它们可能不会用到。可以勾勒出类的草图,放置一些变量,但细节应该简洁。
此时我会避免给出过多的指导;能够用代码构建问题是编程的一项基本技能。只有当我觉得问题严重,而且这种方法无法让他们完成任务时,我才会提出意见。如果他们感到自信并且正在取得进展,我会尽量不强迫他们接受理想化的观点,而是让他们顺其自然。
一个关键问题应该在设计过程中提出,最迟在比较牌时也应该提出,那就是我们处理的是什么类型的牌。并非所有人都熟悉标准扑克牌,这很正常;他们会问这些牌是什么类型的。其他人则假设是同花牌,并询问如何比较花色。在所有情况下,我都将需求简化为一组从 1 到 N 的带编号的牌。对于这个需求,Card
完全可以不使用类,而只使用整数。不过,使用简单的类型也没什么问题Card
。
无论选择整数还是类,我都必须理解为什么做出这个决定。我尤其希望候选人能够告诉我他们为什么选择其中一个。代码也应该清晰易懂。像 这样的变量
int[] array
没什么用(是的,这种情况发生过一次)。用它们的逻辑值来命名,例如deck
。
游戏进度
我提到过编程应该自上而下。在完成初始设计和一些代码草图之后,我希望能够编写出主流程。我希望看到牌的发牌、回合的进行以及最终胜出的宣告。
先从粗略的结构开始,然后再填充细节,这很好。目前为止,我的经验是,不从宏观层面开始的人很难完成这个问题。
defn play_game = -> {
deal_cards()
take_turns()
declare_winner()
}
考虑到问题的简单性,如果之后可以重构为函数,也可以直接在函数中编写这些代码。由面试官主动发起重构,还是我提示,似乎反映了他们的经验水平。
自上而下设计的重要性在测试驱动开发和 YAGNI“你不会需要它”的概念等过程中得到了呼应。
常见问题
接下来就是填写细节了。这个问题有很多细节需要注意,但都不是太棘手,这对于面试来说还算不错。
玩家类
一个常见的错误是无法识别Player
类型。我们最终会得到这样的编码模式:
vector<int> player1_cards, player2_cards;
int player1_score, player2_score;
一系列匹配的变量名,例如player1_*
和player2_*
,表明你应该有一个Player
类型。大多数候选人都是自己得出这个结论的,要么Player
直接从一个类开始,要么重构它。其他人需要一些提示,但通常很快就能理解。少数人只是没理解,需要明确的指导。
提示别人而不直接给出答案是很困难的。在这种情况下,我喜欢尝试这样一些方法:“看看从玩家1和玩家2开始的那些变量。你发现有什么规律了吗?”,“有什么结构可以用来抽象这些变量吗?”,或者“你能不能用更好的方法把它们分组?”
参数和成员变量
应该有一个Game
类来管理模拟。类名并不重要,但它应该包含玩家以及游戏每个阶段的函数。这里考察的是应聘者的面向对象编程能力。
成员变量取代了函数重复传递参数的需要。所有函数都应该将玩家、分数和卡牌作为参数传递,但玩家应该作为实例的一部分。有些人会立即这样做,而另一些人则需要一些提示。
玩家详细信息几乎总是作为成员变量最好,但是在如何处理初始牌组和发牌方面存在一些变化。
再次强调,恰当的提示并非易事。我会尝试问一些问题,例如“是否有必要将玩家传递给所有函数?”“是否可以避免这种冗余?”,或者更直接地说:“这些玩家感觉就像属于这个类。” 说实话,我不记得我具体说了什么,但关键在于从概括性、甚至神秘性的角度入手。我需要说多少,以及需要说得多明确,很大程度上反映了受访者的技能。
神奇的数字和细节
代码中涉及的内容不多,但我不喜欢看到它们。例如,一个从 运行1..52
到创建卡片的循环使用了一个魔法数字。在添加到分数时也会2
出现一个:这有点微妙,但与我计划的扩展问题相关。
这看起来可能只是个小细节,但我注意到优秀的程序员对这类事情很敏感。他们会创建一个常量,给构造函数传入一个参数(可能带有默认值),或者干脆说:“是啊,这不太好,我应该之后再清理一下。”
到这个时候,我通常就能大致了解对方对编程语言的掌握程度了。与自然语言的比较似乎很公平,有些人说话流利,代码流畅、连贯。而有些人说话结结巴巴、忘词忘句、脑子里转述,代码也不太流畅。
虽然高级语法的考生表现不错,但也有人只用基本的命令式语法完成了题目。然而,真正的困难是不允许的:考生可以选择语言,所以对基础知识的期望是绝对的。
扩展问题
如果受访者已经解决了基本问题,并且我们至少还剩下 10 分钟,我会添加下一个要求:
- 扩展游戏以支持两个以上的玩家
这需要多少修改取决于他们最初采用的方法。如果他们没有提出一个Player
类,我会提出要求。如果代码比较稀疏,我也会要求重构成函数。代码需要处于良好的状态。否则,这个新要求就太令人望而生畏了。
粗略估计,大约有一半的人会问到这个问题。这使得它成为一个合适的筛选标准:我倾向于不让那些无法解决这个扩展问题的人通过。我会包容应届毕业生,但不会包容有行业经验的人。如果我估计他们无法完成这个问题,我通常不会问这个问题,或者有时我建议他们只扩展一个功能。我宁愿提前结束面试,也不愿增加额外的压力。
更多问题
改为 N 名玩家带来了一些新问题:
- 发牌不再能用 if-else 语句来选择玩家了。我有点惊讶,竟然有这么多人不知道如何
%
在循环中使用模数从数组中选择元素。 - 从列表中选择最大的牌不再是一个 if-else 问题。这对某些人来说是一个挑战,因为他们知道如何获取最大值,但并不总是知道那张牌的索引。
- 判断游戏是否结束的循环现在变得非常重要,尤其是在玩家持有的牌数可能不一致的情况下。我注意到,优秀的程序员会自然而然地创建一个辅助函数,或者避免在循环中出现复杂的条件。
程序员对语言的了解似乎在这里起着至关重要的作用。熟悉自己语言的人更容易进行这些更改。对于熟悉 Python 的候选人来说尤其如此,因为 Python 有很棒的结构可以简化这一过程。
完成这个问题可以肯定地表明面试者确实是一名优秀的程序员,或者至少是一名优秀的程序员。我甚至不在乎他们在重构过程中是否遇到了小问题,但通常情况下,他们不会遇到任何问题。这是一个奇怪的规律,能够走到这一步的人往往不会在这个问题上遇到困难。
超级奖励问题
如果受访者成功添加了 N 位玩家的支持,我还会再做一点修改。这比之前的要求更具挑战性:“不要假设卡牌具有唯一的价值”。具体如下:
- 消除卡片是唯一的假设(即采用标准卡组)
- 如果玩家拥有相同点数的牌,则抽一张额外的牌,重复此过程,直到其中一人拥有更高的牌
- 牌值最高的人拿走所有牌,每人得一分
- 只有打平的玩家才能继续抽牌(1 名或以上玩家可以不参加额外抽牌)
只有一个人做到了这一点,为这个面试问题设定了黄金标准。
只有时间压力
我喜欢这个问题的一点是它没有任何花招或随机的算法知识。节奏安排似乎也很好:面试官能够应对不同程度的问题。考虑到我见过的面试官遇到的困难,这道题似乎能很好地测试各方面的能力。
作为面试练习,试着用代码来回答这个问题。注意哪些部分让你感到困难,并观察你花了多长时间。
文章来源:https://dev.to/mortoray/interview-question-a-two-player-card-game-67i