C#中的Konami Code

24

我想让一个C#应用程序实现Konami代码以显示复活节彩蛋。 http://en.wikipedia.org/wiki/Konami_Code

最佳的实现方式是什么?

这是一个标准的C# Windows窗体应用程序。


你需要提供更多细节。你正在做什么类型的应用程序,控制台,GDI+,WPF?只需将键盘按键读入缓冲区并测试匹配即可。 - smaclell
维基百科文章链接的有趣内容:http://www.pcworld.com/article/163936/espncom_the_konami_code_and_a_whole_lotta_ponies.html。显然,ESPN也变得可爱了! - Michael Myers
11个回答

29

在 Windows Forms 中,我会创建一个类,该类知道序列是什么,并保存您在序列中的位置状态。像这样的代码应该可以实现:

using System;
using System.Collections.Generic;
using System.Windows.Forms;

namespace WindowsFormsApplication3 {
    public class KonamiSequence {

        List<Keys> Keys = new List<Keys>{System.Windows.Forms.Keys.Up, System.Windows.Forms.Keys.Up, 
                                       System.Windows.Forms.Keys.Down, System.Windows.Forms.Keys.Down, 
                                       System.Windows.Forms.Keys.Left, System.Windows.Forms.Keys.Right, 
                                       System.Windows.Forms.Keys.Left, System.Windows.Forms.Keys.Right, 
                                       System.Windows.Forms.Keys.B, System.Windows.Forms.Keys.A};
        private int mPosition = -1;

        public int Position {
            get { return mPosition; }
            private set { mPosition = value; }
        }

        public bool IsCompletedBy(Keys key) {

            if (Keys[Position + 1] == key) {
                // move to next
                Position++;
            }
            else if (Position == 1 && key == System.Windows.Forms.Keys.Up) {
                // stay where we are
            }
            else if (Keys[0] == key) {
                // restart at 1st
                Position = 0;
            }
            else {
                // no match in sequence
                Position = -1;
            }

            if (Position == Keys.Count - 1) {
                Position = -1;
                return true;
            }

            return false;
        }
    }
}

要使用它,您需要在表单的代码中响应按键事件。像这样的东西应该就可以了:

    private KonamiSequence sequence = new KonamiSequence();

    private void Form1_KeyUp(object sender, KeyEventArgs e) {
        if (sequence.IsCompletedBy(e.KeyCode)) {
            MessageBox.Show("KONAMI!!!");
        }
    }

希望这足以为你提供所需的信息。对于WPF,你需要注意一些微小差异 (请参见编辑历史记录#1)。

编辑:更新为WinForms而非WPF。


你的状态表还不够完美(如果你输入了“Up-up-up”,你的代码会重新开始寻找“up-up”,而不是寻找“down-down”,但有限状态机是正确的选择。) - James Curran
2
使用特殊情况来修复状态机似乎不是解决这个问题的正确方法。然后,您需要在更改“秘密代码”时调整硬编码的“修复”。我已经提供了一个新的解决方案作为回答这个问题,它更简单、更轻量级。 - James
你的代码很混乱,应该只是将传入的键与预期的下一个键进行比较。如果这个测试失败了,就重置指针并再次进行比较。 - mP.
4
@James,这不是旨在成为生产就绪代码的 - 这不是SO的目的 - 我觉得这是一个很好的起点。我认为代码并不特别混乱 - 从所呈现的逻辑可以清楚地知道它在做什么。个人而言,我觉得你的更难理解。目标并不总是“整洁”,而可维护性和易于理解应该是最重要的。 - Sam Meldrum
嗨。我编写了一个解决方案,可以跟踪所有有效的输入序列。因此,它可以解决“up up up”问题以及答案中描述的更复杂的问题。我用JavaScript编写了它,但应该很容易地转换为C#。https://dev59.com/61wZ5IYBdhLWcg3wTOou#71309611 - Peaceful James
显示剩余3条评论

8

正确的顺序,与Konami自身实现方式相同:

  • 获取输入
  • 如果输入等于代码数组索引处的字节,则增加索引。
    • 否则,清除索引。
  • 如果索引大于代码长度,则代码正确。

以下是不正确的做法:

  • 累积一个按键缓冲区,然后逐字节进行字符串比较。效率低下,最好不要这样做。在表单上每次按键都会调用字符串解析例程,并且这些例程与采取一些简单步骤相比都很慢和笨重,而可以达到完全相同的效果。

  • 一个有限状态机,在代码中重复序列时会出错。

  • 一个有限状态机,硬编码了“特殊情况”。现在,你不能在一个地方进行修改。你必须改变代码字符串并添加新代码来处理你不合适的实现状态机。

  • 实例化一个List对象来保存像字符列表这样的简单内容。

  • 涉及String对象。

因此,以下是正确的做法:

using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public class KonamiSequence
    {
        readonly Keys[] _code = { Keys.Up, Keys.Up, Keys.Down, Keys.Down, Keys.Left, Keys.Right, Keys.Left, Keys.Right, Keys.B, Keys.A };

        private int _offset;
        private readonly int _length, _target;

        public KonamiSequence()
        {
            _length = _code.Length - 1;
            _target = _code.Length;
        }

        public bool IsCompletedBy(Keys key)
        {
            _offset %= _target;

            if (key == _code[_offset]) _offset++;
            else if (key == _code[0])  _offset = 2;  // repeat index

            return _offset > _length;
        }
    }
}

现在,它非常快速,不需要处理字符串或实例化比数组更大的任何内容,修改代码只需修改数组,非常简单易懂。
构造函数中的字段初始化取代了硬编码为所需值的常量。如果使用常量,我们可以缩短6行代码左右,但这样有些浪费,但允许类尽可能容易地适应新的代码 - 您只需更改数组列表即可。此外,所有“bulk”都在实例化时处理,因此不会影响目标方法的效率。
仔细看后,这段代码甚至可以更简单。如果您在输入正确代码时重置该值,则不需要模数。
核心逻辑实际上可以变成一行代码。
_sequenceIndex =  (_code[_sequenceIndex] == key) ? ++_sequenceIndex : 0;

这是不正确的。 在此代码中,没有在它之前敲上一次向上箭头的正确代码“up,up,up,down,…”将不被识别。任何在开头重复的序列都是不正确的 - 如果正确的代码是UDUDLR,则按下UDUDUDLR将不被识别,尽管它包含了正确的代码。 - Philip Rieck
我已经看过你最新的编辑,但仍然不正确。发送UUDDLRLRBA会成功,就像应该的那样。发送UUUDDLRLRBA会导致失败,即使它包含了正确的代码。事实上,由于偏移量没有被重置,发送UBUUDDLRLRBA(其中包含了正确的代码)也不会被报告为正确。 - Philip Rieck
以上代码已经修订完毕,它是在C#中实现与Konami使用的原始机器码最接近的东西。根据您的不满,现在已经更改以解决额外的“向上”键事件问题。请注意,原始的Konami实现也存在同样的问题。 - James
真正的Konami密码在Konami游戏中的实现方式与我所演示的完全一样。在《魂斗罗》(Contra)中亲自尝试一下,多输入一个U就会使代码失效。 - James

5
将按键捕获到一个13(或者任何代码子集,因为您可能不想包括START键)个字符的列表/数组/字符串中,然后再正常处理它们。每次添加一个按键时,如果(且仅当)它是系列中的最后一个按键,则将缓冲区与正确的Konami代码进行匹配。
我的建议是,如果他们按箭头键,则将其映射到合理的字母...然后将B和A也映射为相应的字符,同时清除任何其他按键的缓冲区。
然后,将缓冲区作为字符串与“UUDDLRLRBABA”进行比较。

它可以解决破损的状态机问题,但效率较低。每次按键都会导致(codelength)次比较,并涉及字符串对象。 - James

4

按照要求,这里提供一个类来解决“问题”,即输入序列过慢无法像“秘密代码”一样。 ;)

NES卡带中的原始代码将在帧例程中调用,因此将通过计算执行次数来跟踪时间。

由于我们被限制为事件驱动的面向对象编程,因此我们将不得不涉及事件。由于这些事件需要强制“到期”,因此我们将不得不涉及Timer对象。

using System;
using System.Windows.Forms;
using Timer=System.Timers.Timer;

namespace WindowsApplication1
{
    public class KonamiSequence
    {
        readonly Keys[] _code = { Keys.Up, Keys.Up, Keys.Down, Keys.Down, Keys.Left, Keys.Right, Keys.Left, Keys.Right, Keys.B, Keys.A };

        private int _sequenceIndex;

        private readonly int _codeLength;
        private readonly int _sequenceMax;

        private readonly Timer _quantum = new Timer();

        public KonamiSequence()
        {
            _codeLength = _code.Length - 1;
            _sequenceMax = _code.Length;

            _quantum.Interval = 3000; //ms before reset
            _quantum.Elapsed += timeout;
        }

        public bool IsCompletedBy(Keys key)
        {   
            _quantum.Start();      

            _sequenceIndex %= _sequenceMax;
            _sequenceIndex = (_code[_sequenceIndex] == key) ? ++_sequenceIndex : 0;

            return _sequenceIndex > _codeLength;
        }

        private void timeout(object o, EventArgs e)
        {
            _quantum.Stop();
            _sequenceIndex = 0;

        }
    }
}

2
我建议您实施一个搜索事件列表和一个“捕获”参考指针来引用该列表的元素。
从概念上讲,将捕获指针启动到搜索列表的第一个元素。如果下一个事件与搜索元素匹配,捕获指针就会递增到下一个元素。否则,它将被重置为开头。
如果指针超过了最后一个元素,则表示完全匹配。

1
这是一个相当简单和高效的解决方案:
public class KonamiSequence
{
    private static readonly Keys[] KonamiCode = { Keys.Up, Keys.Up, Keys.Down, Keys.Down, Keys.Left, Keys.Right, Keys.Left, Keys.Right, Keys.B, Keys.A };

    private readonly Queue<Keys> _inputKeys = new Queue<Keys>();

    public bool IsCompletedBy(Keys inputKey)
    {
        _inputKeys.Enqueue(inputKey);

        while (_inputKeys.Count > KonamiCode.Length)
            _inputKeys.Dequeue();

        return _inputKeys.SequenceEqual(KonamiCode);
    }
}

用例:

private readonly KonamiSequence _konamiSequence = new KonamiSequence();

private void KonamiForm_KeyDown(object sender, KeyEventArgs e)
{
    if (_konamiSequence.IsCompletedBy(e.KeyCode))
        MessageBox.Show("Konami!");
}

1
这里是另一种实现,基于James的答案和评论:
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public class KonamiSequence
    {
        private readonly Keys[] _code = { Keys.Up, Keys.Up, Keys.Down, Keys.Down, Keys.Left, Keys.Right, Keys.Left, Keys.Right, Keys.B, Keys.A };
        private int _index = 0;

        public bool IsCompletedBy(Keys key)
        {
            if (key == _code[_index]) {
                if (_index == _code.Length - 1) {
                    _index = 0;
                    return true;
                }
                ++_index;
            } else {
                _index = 0;
            }

            return false;
        }
    }
}
  • 没有缓存_code.Length请参阅本文),但注意它仅在从序列中键入密钥时访问。
  • 接受大小写为“UUUUUUUUUUDDLRLRBA”的情况。
  • 当键入错误的密钥时,当然会重置序列。

0
我正在寻找同样的东西,然后我想出了一个非常简单的代码,它可以正常工作。 在表单上将Keypreview设置为True,并声明一个名为“konami”的字符串。
Private Sub frm_KeyPress(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyUp
    Dim i As String = "UpUpDownDownLeftRightLeftRightBA"
    If (e.KeyCode.ToString = "Up") And (konami <> "Up") Then konami = ""
    konami = konami & e.KeyCode.ToString
    'Debug.Print(konami)
    If konami = i Then '' << INSERT YOUR MESSAGE HERE >>  ''
    If e.KeyCode.ToString = "Return" Then konami = ""
    If konami.Length > 60 Then konami = ""
End Sub

0

我知道这是一个老问题,但我在VB中也开始了同样的旅程。我为此创建了一个类:

Public Class Konami
    ' Here is the pattern to match
    Property KonamiOrder As List(Of Keys) = New List(Of Keys) From {Keys.Up, Keys.Up, Keys.Down, Keys.Down, Keys.Left, Keys.Right, Keys.Left, Keys.Right, Keys.B, Keys.A}

    ' Just calling these out ahead of time
    Property sequence As List(Of Boolean)
    Property ix As Integer = 0

    ' Hey new object, better set the important bits
    Public Sub New()
        me.reset()
    End Sub

    ' Reset on pattern failure, or completion
    Public Function reset() As Boolean
        Me.sequence = New List(Of Boolean) From {False, False, False, False, False, False, False, False, False, False}
        ix = 0

    End Function


    ' Here's where all the action happens
    Public Function checkKey(keycode As Keys)
        'Check to see what they pressed
        If sequence(ix) = False And keycode = KonamiOrder(ix) Then
            ' Hurray, they pressed the right key, better keep track of it
            sequence(ix) = True
            ix += 1
        Else
            ' Nope, reset
            Me.reset()
        End If

        'Is the code complete and correct?
        If sequence.Contains(False) Then
            ' Nope, send back failure
            Return False
        Else
            'Yep, reset so it can be used again and send back a success
            Me.reset()
            Return True
        End If
    End Function
End Class

这只是一个示例表单的代码后台,演示了konami类的用法。

Public Class Form1
    Private oKonami As New Konami

    Private Sub Form1_KeyUp(sender As Object, e As KeyEventArgs) Handles Me.KeyUp
        ' Send the Key press on its way, and get some logic going
        If oKonami.checkKey(e.KeyCode) Then
            ' Congrats, pattern match
            MsgBox("Konami Code Entered")
        End If
    End Sub

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
        ' This will intercept the key events on this form
        Me.KeyPreview = True
    End Sub
End Class

https://github.com/the1337moderator/KonamiCodeforVB.net


虽然这个链接可能回答了问题,但最好包括答案的必要部分并提供链接作为参考。仅有链接的答案如果链接页面发生更改可能会变得无效。 - bummi

0

我已经阅读了所有的答案,并发现序列初始值的重复输入是实现中常见的问题。以下是一个简单的实现,没有遇到初始问题的重复。没有特殊情况,没有真正硬编码的内容,类中指定的整数仅用于默认值。

public partial class KonamiCode {
    public bool IsCompletedBy(int keyValue) {
        for(var i=sequence.Count; i-->0; ) {
            if(sequence[i]!=keyValue) {
                if(0==i)
                    count=0;

                continue;
            }

            if(count!=i)
                continue;

            ++count;
            break;
        }

        var isCompleted=sequence.Count==count;
        count=isCompleted?0:count;
        return isCompleted;
    }

    public KonamiCode(int[] sequence=default(int[])) {
        this.sequence=
            sequence??new[] { 38, 38, 40, 40, 37, 39, 37, 39, 66, 65 };
    }

    int count;
    IList<int> sequence;
    public static readonly KonamiCode Default=new KonamiCode();
}

1
如果我的作弊码是 "a a b a a c",我输入 "a a b a a b",那我只需输入 "a a c" 就能完成有效序列吗?我认为这个代码无法实现。 - Peaceful James
@PeacefulJames,你说得对,那种情况是有意防止的。你可能想考虑另一个答案,它允许重复的初始输入。 - Ken Kin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接