在回忆录中,原创游戏《西蒙》的共同发明人拉尔夫·H·贝尔于2014年12月6日星期六去世,享年92岁。纪念电子游戏机之父。安息吧,巴尔先生,您已经永远改变了我们的生活。太好了,但是现在我一团糟,我不确定从哪里开始清理(GitHub上的完整代码)。

根据规格,将有4个按钮。因此,我创建了一个enum并将其命名为SimonButton

public enum SimonButton
{
    Green,
    Red,
    Blue,
    Yellow
}


我创建了一个SimonSaysRound类,该类实现INotifyPropertyChanged并与UI绑定,主要用于提供玩家得分。 br />
public class SimonSaysRound : INotifyPropertyChanged
{
    private const int PointsForGoodMatch = 5;

    private readonly SimonButton[] _sequence;
    private int _matches;
    private int _score;

    public SimonSaysRound(IEnumerable<SimonButton> sequence, int score)
    {
        _sequence = sequence.ToArray();
        _score = score;
        _matches = 0;
    }

    public event EventHandler<SimonSaysScoreEventArgs> RoundCompleted;
    public void OnRoundCompleted()
    {
        var handler = RoundCompleted;
        if (handler != null)
        {
            var result = _matches == _sequence.Length;
            RoundCompleted(this, new SimonSaysScoreEventArgs(result, Score));
        }
    }

    public void Play(SimonButton button)
    {
        var success = _sequence[_matches] == button;
        if (success)
        {
            Score += PointsForGoodMatch;
            _matches++;
        }

        if (!success || _matches == _sequence.Length)
        {
            OnRoundCompleted();
        }
    }

    public int Round
    {
        get { return _sequence.Length; }
    }

    public int Score
    {
        get { return _score; }
        private set
        {
            if (value == _score) return;
            _score = value;
            OnPropertyChanged();
        }
    }

    public int Length { get { return _sequence.Length; } }
    public IEnumerable<SimonButton> Sequence { get { return _sequence; } }

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}


我通常不会像这样实现我的INotifyPropertyChanged-ReSharper建议了一些东西,我想看看它做了什么。 br />
当应用程序启动时,显示主窗口,并且动画显示一个“开始”按钮:



单击该按钮时,消息栏折叠,并且通过快速设置径向渐变的偏移值的动画(带有伴随的声音效果),按钮闪烁:中间的条展开,显示一条“回合完成”消息,向玩家显示其得分;当玩家单击/收起该消息时,游戏继续进行:单击该消息将关闭窗口,然后程序结束:



XAML / UI可以在此处查看;这篇文章是关于实际应用代码的:

public partial class App : Application
{
    private readonly MainWindow _mainWindow = new MainWindow();
    private SimonSaysRound _currentRound;

    private readonly IDictionary<SimonButton, string> _sounds;

    private readonly int _seed;

    public App()
    {
        _seed = new Random().Next();

        var folder = Path.GetDirectoryName(GetType().Assembly.Location);
        _sounds = Enum.GetValues(typeof (SimonButton))
                      .Cast<SimonButton>()
                      .ToDictionary(button => button,
                                    button => Path.Combine(folder ?? string.Empty, "Resources", button + ".wav"));
    }

    protected override void OnStartup(StartupEventArgs e)
    {
        _mainWindow.SimonButtonClicked += OnSimonButtonClick;
        _mainWindow.PlayNextRound += _mainWindow_PlayNextRound;
        _mainWindow.ShowDialog();
    }

    private void _mainWindow_PlayNextRound(object sender, EventArgs e)
    {
        PlayNextRound();
    }

    private void PlayNextRound()
    {
        var sequenceLength = 1;
        var score = 0;
        if (_currentRound != null)
        {
            sequenceLength = _currentRound.Length + 1;
            score = _currentRound.Score;
        }

        _currentRound = new SimonSaysRound(GenerateSequence(sequenceLength), score);
        _currentRound.RoundCompleted += _currentRound_RoundCompleted;
        _mainWindow.DataContext = _currentRound;
        _mainWindow.DataContext = _currentRound;
        PlaySequence();
    }

    private IEnumerable<SimonButton> GenerateSequence(int length)
    {
        var random = new Random(_seed);
        for (var i = 0; i < length; i++)
        {
            yield return (SimonButton)random.Next(Enum.GetValues(typeof(SimonButton)).GetLength(0));
        }
    }

    private void _currentRound_RoundCompleted(object sender, SimonSaysRoundCompletedEventArgs e)
    {
        if (e.Success)
        {
            _mainWindow.OnRoundSuccessful();
        }
        else
        {
            _mainWindow.OnGameOver();
        }
    }

    private async Task PlaySequence()
    {
        foreach (var button in _currentRound.Sequence)
        {
            await OnSimonButtonClickAsync(null, new SimonButtonEventArgs(button));
            Thread.Sleep(300);
        }
    }

    private void OnSimonButtonClick(object sender, SimonButtonEventArgs e)
    {
        OnSimonButtonClickAsync(sender, e);
    }

    private async Task OnSimonButtonClickAsync(object sender, SimonButtonEventArgs e)
    {
        using (var player = new SoundPlayer(_sounds[e.Button]))
        {
            player.Play();
        }

        if (sender != null)
        {
            _currentRound.Play(e.Button);
        }

        await _mainWindow.HighlightSimonButton(e.Button);
    }
}


主窗口的代码: br />我使用了我在Stack Overflow上借用的这种扩展方法(必须对其进行一些调整),以使Storyboard动画可以等待并避免所有动画同时运行: >

评论

关于提交的一个小提示:提交消息“已修复”确实没有帮助。即使您自己开发,也要在提交消息中更具描述性。

@germi你是对的(但更糟糕的是;))

xkcd.com/1296

#1 楼


首先要问的是:为什么不使用MVVM? App类的内容看起来像应该在模型/视图模型级别上实现的东西。

这看起来像很多复制粘贴

private void Blue_MouseDown(object sender, MouseButtonEventArgs e)
{
    OnSimonButtonClicked(SimonButton.Blue);
    e.Handled = true;
}

private void Yellow_MouseDown(object sender, MouseButtonEventArgs e)
{
    OnSimonButtonClicked(SimonButton.Yellow);
    e.Handled = true;
}

....



您应该对按钮使用单个事件处理程序。例如,您可以在XAML中将按钮类型设置为FrameworkElement.Tag,并在后面的代码中将其作为((FrameworkElement)sender).Tag进行访问。如果要使用绑定而不是事件,也可以将其重构为命令。


我不喜欢EnableButtonsDisableButtons方法,它们感觉不对。我会将按钮的IsEnabled属性绑定到一些bool依赖属性(或viewmodel属性),并将其设置为true / false。然后,我将在事件处理程序中检查此属性。

您应该使用一些静态方法来执行此操作:

var handler = myEvent;
if (handler != null)
{
    handler(this, myArgs);
}


我不我不了解整个故事,但我不太明白为什么您需要等待动画。这只会使您的代码更复杂。另外,我认为在XAML中管理和定义情节提要比在代码隐藏中要容易得多,但这可能是个问题。


#2 楼


魔术数字,在App类中至少有一个:private async Task PlaySequence()应该将其提取为常数或更好的属性,以便可以调整速度(也称为睡眠)。
我看不到_seed变量的要点。是的,我知道您为什么要使用它,但是每次调用GenerateSequence()方法时,它都会迫使您重新创建IEnumerable<SimonButton>。考虑将序列的“新”项附加到序列的末尾。
每次也不必在创建随机数时调用Enum.GetValues(typeof(SimonButton)).GetLength(0)。将其提取到变量中并重用,它不会更改;-)
两次分配DataContext可能是复制和粘贴错误
整个GenerateSequence()方法应该存在于SimonSaysRound类中。您不需要访问ScoreLength属性。

SimonSaysRound也可以摆脱SimonButton[],因为它可以改用IEnumerable<>。这也将导致Length属性过时。

通过实现上述几点来重构SimonSaysRound类将导致

public class SimonSaysRound : INotifyPropertyChanged
{
    private const int PointsForGoodMatch = 5;
    private readonly int _buttonCount = Enum.GetValues(typeof(SimonButton)).GetLength(0);

    private IEnumerable<SimonButton> _sequence = Enumerable.Empty<SimonButton>();
    private IEnumerator<SimonButton> _enumerator;

    private int _matches=0;
    private int _score=0;
    private int _currentlength = 0;

    private Random _random = new Random();

    /// <summary>
    /// Use this method, to start a new game.
    /// </summary>
    public void Reset()
    {
        _matches = 0;
        _score = 0;
        _currentlength = 0;
        _sequence = Enumerable.Empty<SimonButton>();
    }

    public event EventHandler<SimonSaysScoreEventArgs> RoundCompleted;
    public void OnRoundCompleted()
    {
        var handler = RoundCompleted;
        if (handler != null)
        {
            var result = _matches == _currentlength;
            RoundCompleted(this, new SimonSaysScoreEventArgs(result, Score));
        }
    }

    public void Play(SimonButton button)
    {
        _enumerator.MoveNext();
        var success = _enumerator.Current == button;
        if (success)
        {
            Score += PointsForGoodMatch;
            _matches++;
        }

        if (!success || _matches == _currentlength)
        {
            OnRoundCompleted();
        }
    }

    public int Round
    {
        get { return _currentlength; }
    }

    public int Score
    {
        get { return _score; }
        private set
        {
            if (value == _score) return;
            _score = value;
            OnPropertyChanged();
        }
    }

    public IEnumerable<SimonButton> Sequence { get { return _sequence; } }

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

    public void PlayNextRound()
    {
        _matches = 0;
        _currentlength += 1;
        _sequence = _sequence.Concat(GenerateNextSequenceItem());
        _enumerator = _sequence.GetEnumerator();

    }

    private IEnumerable<SimonButton> GenerateNextSequenceItem()
    {
        yield return (SimonButton)_random.Next(_buttonCount);
    }      
}


以及在调用方

private void PlayNextRound()
{
    if (_currentRound == null)
    {
        _currentRound = new SimonSaysRound();
    }
    _currentRound.PlayNextRound();
    _currentRound.RoundCompleted += _currentRound_RoundCompleted;

    _mainWindow.DataContext = _currentRound;

    PlaySequence();
}


您应该考虑在类级别初始化_currentRound,从而使null检查过时。恕我直言,通过引发同一事件而不通过使用不同的事件参数来区分它们是不好的。
GameButtonStartGame()GameButtonNextRound()方法中的代码重复。要么删除其中一种方法,然后更改事件处理程序,要么更好地将代码提取到一个单独的方法中,这两个处理程序都将其称为。<​​br />

#3 楼

命名与借用扩展方法中的其余代码不一致:


    EventHandler onComplete = null;
    onComplete = (sender, args) =>
    {
        storyboard.Completed -= onComplete;
        source.SetResult(true);
    };
    storyboard.Completed += onComplete;
    containingObject.Dispatcher.Invoke(() => storyboard.Begin(containingObject));



OnSimonButtonClicked和您使用的每个OnEventName方法曾经写过的调用了处理程序handler


    var handler = SimonButtonClicked;
    if (handler != null)
    {
        handler.Invoke(this, new SimonButtonEventArgs(button));
    }



这样会更一致-请注意= null分配是多余的,已被删除:

    EventHandler handler;
    handler = (sender, args) =>
    {
        storyboard.Completed -= handler;
        source.SetResult(true);
    };
    storyboard.Completed += handler;
    containingObject.Dispatcher.Invoke(() => storyboard.Begin(containingObject));