Саймон говорит: «Сделай мне симпатичную игру»

  

В воспоминаниях

     

Ральф Х. Бэр, соавтор оригинальной игры «Саймон», умер в субботу 6 декабря th 2014, на 92 . С его прохождением эта дружеская маленькая задача непреднамеренно стала мемориалом отцу игровых консолей. Покойся миром Бэром, ты изменил нашу жизнь навсегда.


Наконец-то я получил свою Simon Says для работы, поскольку я намеревался это к. Это здорово, но теперь у меня остался беспорядок, и я не уверен, с чего начать очистку (полный код на GitHub ).

В спецификациях должно было быть 4 кнопки. Поэтому я создал enum и назвал его SimonButton:

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

Я создал класс SimonSaysRound, который реализует INotifyPropertyChanged для привязки к пользовательскому интерфейсу, в основном для обеспечения оценки игрока.

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 предложил что-то, и я хотел посмотреть, что он сделал .. и мне это не нравится.

Когда приложение запускается, отображается главное окно, и анимация показывает кнопку «Пуск»:

Пуск!

Когда кнопка нажата, панель сообщений рушится, и кнопка быстро мигает с помощью значения смещения радиального градиента (с сопровождающими звуковыми эффектами):

Синий квадрант горит

Когда игрок входит в правильную последовательность, средняя полоса расширяется, появляется сообщение «round completed», которое показывает игроку их оценку; игра возобновляется, когда игрок нажимает /сворачивает сообщение:

Раунд 1 завершен! Готов?

Экран «Игра поверх» отображает счет игрока и количество завершенных раундов; щелчок сообщения закрывает окно, а затем программа заканчивается:

К сожалению, завершено 6 раундов. Ваша оценка: 115

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);
    }
}

И код для главного окна:

public partial class MainWindow : Window
{
    private readonly IDictionary<SimonButton, Border> _buttons;

    public MainWindow()
    {
        InitializeComponent();
        _buttons = new Dictionary<SimonButton, Border>
            {
                { SimonButton.Green, Green },
                { SimonButton.Red, Red },
                { SimonButton.Yellow, Yellow },
                { SimonButton.Blue, Blue }
            };

        RegisterName(MessageBar.Name, MessageBar);
        foreach (var button in _buttons)
        {
            RegisterName(button.Value.Name, button.Value);
        }

        DisableButtons();

        MouseDown += MainWindow_MouseDown;
        Activated += MainWindow_Activated;
    }

    private async void MainWindow_Activated(object sender, EventArgs e)
    {
        GameScoreLabel.Visibility = Visibility.Collapsed;
        GameButton.Text = "Start!";

        await AnimateMessageBand(36);
        Activated -= MainWindow_Activated;

        GameButton.MouseDown += GameButtonStartGame;
    }

    private void MainWindow_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Left)
        {
            DragMove();
        }
    }

    public event EventHandler<SimonButtonEventArgs> SimonButtonClicked;
    public async Task OnSimonButtonClicked(SimonButton button)
    {
        var handler = SimonButtonClicked;
        if (handler != null)
        {
            handler.Invoke(this, new SimonButtonEventArgs(button));
        }
    }

    public async Task OnGameOver()
    {
        GameButton.Text = string.Format("Oops! {0} rounds completed.", ((SimonSaysRound)DataContext).Round - 1);
        GameScoreLabel.Visibility = Visibility.Visible;
        await AnimateMessageBand(56);

        DisableButtons();

        GameButton.MouseDown += GameButtonEndGame;
    }

    private void DisableButtons()
    {
        Green.MouseDown -= Green_MouseDown;
        Red.MouseDown -= Red_MouseDown;
        Yellow.MouseDown -= Yellow_MouseDown;
        Blue.MouseDown -= Blue_MouseDown;
    }

    public void EnableButtons()
    {
        Green.MouseDown += Green_MouseDown;
        Red.MouseDown += Red_MouseDown;
        Yellow.MouseDown += Yellow_MouseDown;
        Blue.MouseDown += Blue_MouseDown;
    }

    public async Task OnRoundSuccessful()
    {
        DisableButtons();
        GameButton.Text = string.Format("Round {0} completed! Ready?", ((SimonSaysRound)DataContext).Round);
        GameScoreLabel.Visibility = Visibility.Visible;
        await AnimateMessageBand(56);
        GameButton.MouseDown += GameButtonNextRound;
    }

    private async Task AnimateMessageBand(double height)
    {
        var animation = new DoubleAnimation(height, new Duration(TimeSpan.FromMilliseconds(200)));

        Storyboard.SetTargetName(animation, MessageBar.Name);
        Storyboard.SetTargetProperty(animation, new PropertyPath("Height"));

        var story = new Storyboard();
        story.Children.Add(animation);
        await story.BeginAsync(MessageBar);

        story.Remove();
    }

    public async Task HighlightSimonButton(SimonButton button)
    {
        var border = _buttons[button];
        var animation = new DoubleAnimation(0, 0.75, new Duration(TimeSpan.FromMilliseconds(100)));

        Storyboard.SetTargetName(animation, button.ToString());
        Storyboard.SetTargetProperty(animation, new PropertyPath("Background.GradientStops[1].Offset"));

        var story = new Storyboard();
        story.Children.Add(animation);
        await story.BeginAsync(border);

        story.Remove();
    }

    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;
    }

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

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

    private async void GameButtonStartGame(object sender, MouseButtonEventArgs e)
    {
        await AnimateMessageBand(0);
        e.Handled = true;

        GameButton.MouseDown -= GameButtonStartGame;

        var handler = PlayNextRound;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
        EnableButtons();
    }

    public event EventHandler PlayNextRound;
    private async void GameButtonNextRound(object sender, MouseButtonEventArgs e)
    {
        await AnimateMessageBand(0);
        e.Handled = true;

        var handler = PlayNextRound;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }

        GameButton.MouseDown -= GameButtonNextRound;
        EnableButtons();
    }

    private void GameButtonEndGame(object sender, MouseButtonEventArgs e)
    {
        Close();
        e.Handled = true;
    }
}

Я использую этот метод расширения, который я заимствовал на переполнении стека (должен был немного его подстроить), чтобы сделать Storyboard ожидают анимации и избегают одновременной работы всех анимаций:

public static class StoryboardExtensions
{
    public static Task BeginAsync(this Storyboard storyboard, FrameworkElement containingObject)
    {
        var source = new TaskCompletionSource<bool>();
        if (storyboard == null)
            source.SetException(new ArgumentNullException());
        else
        {
            EventHandler onComplete = null;
            onComplete = (sender, args) =>
            {
                storyboard.Completed -= onComplete;
                source.SetResult(true);
            };
            storyboard.Completed += onComplete;
            containingObject.Dispatcher.Invoke(() => storyboard.Begin(containingObject));
        }
        return source.Task;
    }
}
41 голос | спросил Mathieu Guindon 8 MonEurope/Moscow2014-12-08T09:30:35+03:00Europe/Moscow12bEurope/MoscowMon, 08 Dec 2014 09:30:35 +0300 2014, 09:30:35

3 ответа


17
  1. Ну, первые вопросы: почему вы не используете MVVM? Содержание вашего класса App выглядит как нечто, которое должно быть реализовано на уровне модели /модели представления.
  2. Это похоже на много копирование в скобки

    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;
    }
    
    ....
    

Для ваших кнопок должен использоваться отдельный обработчик событий. Например, вы можете установить тип кнопки в FrameworkElement.Tag в XAML и получить доступ к ней как отправителя ((FrameworkElement)sender).Tag в коде. Вы также можете реорганизовать их в команды, если вы должны использовать привязки вместо событий.

  1. Мне не нравятся ваши методы EnableButtons и DisableButtons, они чувствуют себя не так. Я бы привязал свойство IsEnabled ваших кнопок к некоторому свойству bool dependency (или свойству viewmodel) и вместо него установил значение true /false. И я бы затем проверил это свойство в обработчике событий.

  2. Для этого вы должны использовать некоторый статический метод:

    var handler = myEvent;
    if (handler != null)
    {
        handler(this, myArgs);
    }
    
  3. Я не знаю всей истории, но я не совсем понимаю, почему вам нужно ждать анимации. Это только делает ваш код более сложным. Кроме того, я считаю, что управление и определение раскадровки намного проще в XAML, чем в коде, но это может быть вопросом вкуса.

ответил Nikita B 8 MonEurope/Moscow2014-12-08T13:46:59+03:00Europe/Moscow12bEurope/MoscowMon, 08 Dec 2014 13:46:59 +0300 2014, 13:46:59
15
  • Магические числа, у вас есть хотя бы один из них в классе App: private async Task PlaySequence(), вы должны извлечь это либо в константу, либо лучше к собственности, так что скорость, ака меньше сна, может быть скорректирована.

  • Я не вижу точки переменной _seed. Да, я знаю, почему вы его используете, но это просто заставляет вас каждый раз вызывать метод GenerateSequence(), чтобы воссоздать IEnumerable<SimonButton>. Подумайте о добавлении «нового» элемента для последовательности в конец последовательности.

  • каждый раз при вызове Enum.GetValues(typeof(SimonButton)).GetLength(0) при создании случайного значения также не требуется. Извлеките его в переменную и повторно используйте, он не изменится ;-)
  • назначение DataContext два раза может быть копией & вставить ошибку
  • Весь метод GenerateSequence() должен находиться в классе SimonSaysRound. Вам не нужно будет обращаться к свойствам Score и Length.
  • 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 на уровне класса, делающего нулевую проверку устаревшей.

  • объединение начала игры и начало следующего раунда, подняв одно и то же событие, не различая их, используя разные аргументы событий, IMHO не очень хорошо.

  • у вас есть дубликат кода в методах GameButtonStartGame() и GameButtonNextRound(). Удалите один из методов и измените обработчик событий или лучше извлеките код в отдельный метод, который называется формой обоих обработчиков.

ответил Heslacher 8 MonEurope/Moscow2014-12-08T13:20:17+03:00Europe/Moscow12bEurope/MoscowMon, 08 Dec 2014 13:20:17 +0300 2014, 13:20:17
6

Именование несовместимо с остальной частью кода в заимствованном методе расширения:

    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));
ответил Mathieu Guindon 17 ThuEurope/Moscow2015-12-17T18:41:10+03:00Europe/Moscow12bEurope/MoscowThu, 17 Dec 2015 18:41:10 +0300 2015, 18:41:10

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132