65.9K
CodeProject 正在变化。 阅读更多。
Home

Anagrams2 - 一个简单的 WPF 游戏应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (16投票s)

2012年10月14日

CPOL

21分钟阅读

viewsIcon

77371

downloadIcon

1479

我的Anagrams游戏移植到WPF。

引言

早在2008年4月,我就在CodeProject上发布了我的第一个.Net文章。

       Anagrams - 一款C#文字游戏[^]

这是一个简单的文字游戏,允许用户在一个打乱字母的单词中找到字谜,根据找到的单词奖励基于Scrabble的点数和额外时间。我沉浸在我的聪明才智中(我知道——这实际上不是一个词,这与本文描述的代码性质相比很讽刺),并为自己做得很好而祝贺自己。由于我目前的工作需要使用WPF进行编码,我有了重温Anagrams的冲动,并对其进行了急需的改进。

我必须承认,虽然我尝试使用某些模式和WPF实践,但我肯定没有做出巨大的努力来严格遵守它们的隐含准则。编程中的一切都是权衡,严格应用这类规则在软件开发中是完全没有立足之地的。鉴于此应用程序的性质和规模,这一想法更为适用。

特色技术

此应用程序中存在以下技术和代码元素

  • Visual Studio 12
  • WPF
  • MVVM模式
  • 调度计时器
  • 数据绑定
  • 值转换器
  • Linq
  • 培根

重要通知! *我认为*您必须先安装.Net 4.5才能运行此应用程序,或者通过删除`private`访问器来更改`GameDictionary.PercentRemaining`属性的`set`方法为`public`。

总体架构

此版本的游戏代码比原始版本少得多。我不确定这主要是由于我对.Net(及其最新功能)知识的增长,还是由于使用了MVVM/WPF。无论原因是什么,这都是一件好事。

单词从磁盘上的文本文件中加载(在旧版本中,每个单词大小都有一个文本文件),每次用户开始游戏时,都会为界面创建一个单词列表。此单词列表包含当前选定的游戏单词中可以找到的所有单词。这种模式可能是代码量减少的最大原因。在旧版本中,所有单词一直都可以用于游戏,我必须维护几个指示器、索引和其他东西,因为我(错误地)假设.Net的性能不佳,所以我“绕过了”那种被错误认为的问题。

模型

模型由一个逗号分隔的文本文件驱动,该文件包含游戏中可以使用所有单词(目前,单词数超过125,000)。程序运行时,加载数据文件,并为每个单词计算分数。单词由`AWord`类表示。

public class AWord
{
    public string Text   { get; private set; }
    public int    Points { get; private set; }
	public bool   Used   { get; protected set; }

    public AWord(string text)
    {
        this.Text   = text;
        this.Used = false;
        this.Points = Globals.CalcWordScore(this.Text);
    }

    public AWord(AWord word)
    {
        this.Text   = word.Text;
        this.Used   = word.Used;
        this.Points = word.Points;
    }
}

为了降低内存影响并提高性能,我在模型中包含了`Points`和`Used`属性。主要原因是,给定游戏使用的单词仅在当前游戏需要时才从模型中提取。我不想为每个游戏重新计算单词分数(尽管通常情况下,给定打乱的单词中可能只有300-400个可能单词)。`Used`属性是必需的,用于跟踪当前应用程序会话中用作游戏单词的单词,并且保持跟踪的唯一方法是将此属性放在模型中。这应该告诉您,MVVM只是一个指南,而不是一套严格的要求。有时,您必须打破规则才能使代码和数据在应用程序的上下文中可用。

单词列表由`MainDictionaryClass`表示。

//////////////////////////////////////////////////////////////////////////////////////
public class MainDictionary : List<AWord>
{
    public bool CanPlay          { get; set;         }
    public int  ShortestWordSize { get; private set; }
    public int  LongestWordSize  { get; private set; }

    //---------------------------------------------------------------------------------
    public MainDictionary()
    {
        this.ShortestWordSize = 65535;
        this.LongestWordSize  = 0;
        this.CanPlay          = LoadFile(Path.Combine
                                         (Path.GetDirectoryName
                                          (System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName), 
                                         "Anagrams2Words.txt"));
    }

    //---------------------------------------------------------------------------------
    protected bool LoadFile(string fileName)
    {
        bool success = false;
        try
        {
            using (FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                using (StreamReader reader = new StreamReader(stream))
                {
                    string words;
                    while (!reader.EndOfStream)
                    {
                        words = reader.ReadLine();
                        if (words.Length > 0)
                        {
                            string[] wordsplit = words.Split(' ');
                            for (int i = 0; i < wordsplit.Length; i++)
                            {
                                string text      = wordsplit[i].ToUpper();
                                ShortestWordSize = Math.Min(ShortestWordSize, text.Length);
                                LongestWordSize  = Math.Max(LongestWordSize, text.Length);
                                AWord item       = new AWord(text);
                                Add(item);
                            }
                        }
                    }
                    Globals.LongestWord  = LongestWordSize;
                    Globals.ShortestWord = ShortestWordSize;
                    success              = this.Count > 0;
                }
            }
        }
        catch (Exception e)
        {
            if (e != null) { }
        }
        return success;
    }

    //---------------------------------------------------------------------------------
    public List<AWord> GetWordsByLength(int length)
    {
        var list = (from item in this 
                    where item.Text.Length == length
                    select item).ToList<AWord>();
        return list;
    }
}

正如您所看到的,这里真的没有多少内容。用户无法在程序中更改单词列表,因此没有理由能够将其保存回硬盘。除了加载单词和计算其点值之外,还有什么可以做/说的?

ViewModel

ViewModel是游戏进行中大部分工作发生的地方。为了将文章篇幅控制在合理范围内,我没有包含代码中实际存在的注释。总的来说,我使用标准的WPF对象来允许界面反映ViewModel中的更改,即`INotifyPropertyChanged`和`ObservableCollection`。鉴于互联网上关于这些对象的文档数量,以及我所做的只是正常使用,这就是我唯一要说的关于这些对象的内容。

DisplayWord 类

此类表示当前游戏中的一个单词,并为当前游戏中使用的每个单词创建一个。它派生自`AWord`。属性如下:

  • string Foreground - 这是在主窗口的ListBox中显示单词时要使用的前景颜色。
  • string Scramble - 这是单词的打乱版本。除非这是当前游戏单词,否则此字符串将为空。
  • bool Found - 此属性指示用户在游戏中是否已找到此单词。当此属性为true时,单词的颜色将变为蓝色(正常找到的单词)或红色(如果此单词是原始单词)。
  • bool IsOriginalWord - 此属性指示此单词是当前游戏单词。

此类中唯一真正有趣的 M代码是`ScrambleIt`方法。正如您可能猜到的,它负责打乱单词。

//--------------------------------------------------------------------------------
public void ScrambleIt()
{
    StringBuilder scramble = new StringBuilder();
    do 
    {
        string temp = this.Text;
        do 
        {
            if (temp.Length > 1)
            {
                int index = (temp.Length > 1) ? Globals.RandomNumber(0, temp.Length-1) : 0;
                scramble.Append(temp[index]);
                temp = temp.Remove(index, 1);
            }
            else
            {
                scramble.Append(temp);
                temp = "";
            }
        } while (!string.IsNullOrEmpty(temp));
    } while (scramble.ToString() == this.Text);
    this.Scramble       = scramble.ToString();
    this.IsOriginalWord = true;
}

由于结果打乱的单词有可能与原始单词相同,因此单词会被重新打乱,直到它与原始单词不同为止。

GameStatistics 类

此类维护与当前进行中的游戏相关的统计信息,例如找到的特定长度的单词数量、获得的积分以及类似数据。这里有一些数学计算,但没有什么值得进一步注意的。我曾想在这里包含我当前的曲速反重力计算,但我不想让任何人感到困惑,或者惹恼那些认为这不可能的人)。

Settings 类

此类代表用户可以在“设置”窗口中更改的设置,并且是这些属性的ViewModel。它只包含set/get和`Save`方法。没什么花哨的,绝对不值得进一步讨论。

WordCounts 类

此类表示`WordCounItem`对象的列表,并在`GameStatistics`类中使用(并从中公开)。`WordCountItem`包含两个属性:

  • LetterCount - 此项正在跟踪的单词的大小
  • WordCount - 在当前游戏中找到的此大小的单词数

GameDictionary 类

此类管理游戏玩法,并为每场游戏创建一个,这样我们就不必在游戏之间重置单词和统计信息的属性了。为了开始,我们需要确定游戏单词将有多少个字母。这由游戏设置决定,默认情况下是使用随机数量的字母。

//--------------------------------------------------------------------------------
private int SetLetterCount()
{
	int letterCount = 0;
	int shortest = Math.Max(Globals.MainDictionary.ShortestWordSize, 6);
	int longest  = Globals.MainDictionary.LongestWordSize;
	switch (this.Settings.LetterPoolMode)
	{
		case LetterPoolMode.Random : 
			letterCount = Globals.RandomNumber(shortest, longest);
			break;

		case LetterPoolMode.Static :
			letterCount = Settings.LetterPoolCount;
			break;
	}
	return letterCount;
}

现在我们有了单词大小,我们可以从主词典中随机选择一个单词。

//--------------------------------------------------------------------------------
private AWord SelectNewWord(int count)
{
    AWord selectedWord = null;
    List<AWord> words = Globals.MainDictionary.GetWordsByLength(count);
    if (this.Settings.TrackUsedWords)
    {
        if (words.Count == 0)
        {
            words.Clear();
            if (this.Settings.LetterPoolMode == LetterPoolMode.Random)
            {
                count = SetLetterCount();
                selectedWord = SelectNewWord(count);
            }
            else
            {
                words = Globals.MainDictionary.GetWordsByLength(count);
                selectedWord = words.ElementAt(Globals.RandomNumber(0, words.Count-1));
            }
        }
        else // otherwise we have words to pick from that haven't yet been used
        {
            selectedWord = words.ElementAt(Globals.RandomNumber(0, words.Count-1));
        }
    }
    else // we're not tracking used words, so just pick one
    {
        selectedWord = words.ElementAt(Globals.RandomNumber(0, words.Count-1));
    }
    if (selectedWord != null)
    {
        selectedWord.Used = true;
    }
    return selectedWord;
}

接下来,我们需要找到可以从游戏单词中派生的所有单词。

//--------------------------------------------------------------------------------
public void FindPossibleWords(AWord selectedWord)
{
    this.Clear();
    if (selectedWord != null)
    {
        var possibleWords = (from item in Globals.MainDictionary
                             where Globals.Contains(selectedWord.Text, item.Text)
                             select item).ToList<AWord>();
        foreach(AWord word in possibleWords)
        {
            DisplayWord displayWord = new DisplayWord(word, (word.Text == selectedWord.Text));
            if (displayWord.IsOriginalWord)
            {
                this.GameWord = displayWord;
            }
            this.Add(displayWord);
            Debug.WriteLine("{0} words", this.Count);
        }
    }
}

最后,我们开始游戏。

//--------------------------------------------------------------------------------
public void ResetGame()
{
    this.WordCount = this.Count;
    this.Statistics.Reset();
    InitTimer();
    StartTimer();
    this.IsPlaying = true;
}

游戏开始后,此对象负责在提交单词或停止游戏时的家务管理。提交单词时,必须建立有效性,并授予(或扣除)积分。

//--------------------------------------------------------------------------------
public bool ValidAndScoreWord(string text)
{
    text       = text.ToUpper();
    int points = 0;
    bool valid = (!string.IsNullOrEmpty(text));
    DisplayWord foundWord = null;
    if (valid)
    {
        foundWord = (from item in this 
        where (item.Text == text && !item.Found) 
        select item).FirstOrDefault();
        valid = (foundWord != null);
    }
    if (valid)
    {
        foundWord.Found = true;
        foundWord.SetFoundColor();
        points += foundWord.Points;
        // the player can bonus points if he specifies the original word
        if (foundWord.IsOriginalWord)
        {
            // bonus points
            points += ORIGINAL_WORD_BONUS;
        }
        // OR he can get bonus points for specifying a word using all of the 
        // letters that isn't the original word.
        else
        {
            if (foundWord.Text.Length == GameWord.Text.Length)
            {
                // bonus points
                points += ALL_LETTERS_BONUS;
            }
        }
        this.Statistics.Update(foundWord.Text.Length, points);
        // determine bonus time
        if (Settings.TimerMode != TimerMode.NoTimer)
        {
            int wordRemainder;
            Math.DivRem(this.Statistics.WordCount.WordsFound, this.Settings.BonusWords, out wordRemainder);
            if (wordRemainder == 0)
            {
                this.SecondsRemaining += this.Settings.BonusTime;
            }
            if (foundWord.IsOriginalWord)
            {
                this.SecondsRemaining += 60;
            }
            else 
            {
                if (foundWord.Text.Length == GameWord.Text.Length)
                {
                    // bonus points
                    this.SecondsRemaining += 30;
                }
            }
            this.SecondsAtStart = this.SecondsRemaining;
        }
    }
    else
    {
        // deduct a point because the word was invalid (or already used)
        points--;
        this.Statistics.Update(0, points);
    }
    if (this.IsWinner)
    {
        StopTimer();
    }
    return (foundWord != null);
}

如果使用计时器(可在“设置”窗体中配置),则以下方法处理计时器滴答。

//--------------------------------------------------------------------------------
public void StartTimer()
{
    if (this.Settings.TimerMode != TimerMode.NoTimer)
    {
        m_timer.Tick += new EventHandler 
        (
            delegate(object s, EventArgs a) 
            { 
                this.SecondsRemaining--;
                this.PercentRemaining = ((double)this.SecondsRemaining / 
                                          Math.Max((double)this.SecondsAtStart, 1)) * 100d;
                if (Settings.PlayTickSound)
                {
                    m_soundPlayer.Play();
                }
                if (this.SecondsRemaining == 0)
                {
                    StopTimer();
                }
            }
        );
        m_timer.Start();
    }
    this.IsPlaying = true;
}

所有其他方法用于开始、停止和解决游戏。

视图

与大多数WPF应用一样,最有趣的东西(也恰好是像我这样没有狂热追随WPF的人最头疼的东西)发生在XAML中。代码隐藏很少(这可能是应该的),主窗口的代码只有不到120行(不包括注释)。

主窗口

这是游戏实际玩的地方。此窗体有一些相对具有挑战性的方面。

进度条

ProgressBar用于显示剩余时间,包括剩余分钟/秒的文本显示和剩余百分比的图形表示。由于ProgressBar已经显示了图形内容,我的任务是添加文本显示,如下所示:

    <Border BorderBrush="{TemplateBinding BorderBrush}" 
            BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2">
        <Label x:Name="PART_TextDisplay" 
               Content="{Binding Path=SecondsRemaining, Converter={StaticResource SecondsRemaining}}" 
               FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Border>

为了显示剩余时间,我创建了`TimeRemainingConverter`转换器。`ProgressBar.Value`属性绑定到`GameDictionary.SecondsRemaining`属性。

游戏单词

游戏单词由两个叠加的控件组成。如果游戏正在进行,则显示游戏单词。否则,显示“游戏结束”控件。我这样做是为了更直观地显示游戏已过期(出于任何原因)。

    <TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,0,5" x:Name="textboxGameWord" 
             CharacterCasing="Upper" Text="" IsReadOnly="true" Focusable="False" 
             removed="Silver" FontWeight="Bold" BorderBrush="Black" />
    <Border Grid.Row="1" Grid.Column="1" Focusable="False" removed="Red" BorderBrush="Black" 
            BorderThickness="1" Margin="0,0,0,5" 
            Visibility="{Binding Path=IsPlaying, Converter={StaticResource isPlayingConverter}}" >
        <TextBlock Focusable="False" removed="Red" Foreground="Yellow" FontWeight="Bold" 
                   FontStyle="Italic" Text="GAME OVER!!" VerticalAlignment="Center" 
                   HorizontalAlignment="Center"/>
    </Border>

其中最有趣的部分是`IsPlayingVisibilityConverter`的使用。

用户单词

用户单词控件实际上有助于过滤机制工作。当用户输入时,会检查已找到的可能单词列表,以查看单词是否以用户输入的文本开头。这样做是为了帮助用户避免提交重复的单词(如果这样做,会导致游戏扣一分)。代码通过`TextChanged`事件在代码隐藏中处理,然后导致ViewModel设置`SatisfiesFilter`属性。这是使用`ICollectionView`的一种更WPFish的方法,但我选择在`GameDictionary` ViewModel对象中已执行的实现中使用一种更具上下文的方式。

列表框

我需要ListBox以自定义方式显示项,即某些项应具有特定颜色,并且只有在用户找到这些项时(或当用户单击“解决”按钮时)才显示这些项。因此,我简单地用以下内容替换了ListBoxItem样式中的ContentPresenter

<Grid >
    <StackPanel HorizontalAlignment="Left" Orientation="Horizontal" Margin="5,0,0,0">
        <TextBlock x:Name="PART_Word" Text="{Binding Path=Text}" FontStyle="Italic" />
        <TextBlock x:Name="PART_Adorner" Text="**" FontStyle="Italic" 
                   Visibility="{Binding Path=IsOriginalWord, Converter={StaticResource BoolToVisibility}}" />
    </StackPanel>
    <StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Margin="0,0,5,0">
        <TextBlock x:Name="PART_Points" Text="{Binding Path=Points}" FontStyle="Italic" />
    </StackPanel>
</Grid>

通常,项以斜体显示,但如果单词代表原始游戏单词,则会以双星号(“**”)装饰。我还将单词点数添加到了项中。

接下来,某个项仅在单词被“找到”时才可见。我使用了内置的WPF转换器来完成此操作。

    <Setter Property="Visibility" Value="{Binding Path=Found, Converter={StaticResource BoolToVisibility}}" />

最后,项的前景颜色将取决于单词的状态。如果单词未找到,则为灰色(在谜题解决后);如果找到,则为蓝色;如果找到且是原始游戏单词,则为红色。

    <Setter Property="Foreground" Value="{Binding Path=Foreground}" />

游戏结束统计信息分组框

大多数人现在应该都知道了,但我认为标准的WPFGroupBox控件设计不正确。当将组框的边框放在背景不透明的容器上时,它会显示不透明的内边框和外边框。由于我使用的是亮钢蓝色背景,这非常明显。这就是它看起来的样子:

为了纠正它,我不得不编辑标准模板来将这些边框改为透明。

<Border BorderBrush="Transparent" BorderThickness="{TemplateBinding BorderThickness}" 
        removed="{TemplateBinding Background}" Grid.ColumnSpan="4" Grid.Column="0" 
        CornerRadius="4" Grid.Row="1" Grid.RowSpan="3"/>
    <Border x:Name="Header" Grid.Column="1" Padding="3,1,3,0" Grid.Row="0" Grid.RowSpan="2">
        <ContentPresenter ContentSource="Header" RecognizesAccessKey="True" 
                          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
    </Border>
    <ContentPresenter Grid.ColumnSpan="2" Grid.Column="1" Margin="{TemplateBinding Padding}" 
                      Grid.Row="2" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
    <Border BorderBrush="Transparent" 
            BorderThickness="{TemplateBinding BorderThickness}" 
            Grid.ColumnSpan="4" CornerRadius="4" Grid.Row="1" Grid.RowSpan="3">
        <Border.OpacityMask>
            <MultiBinding ConverterParameter="7" 
                          Converter="{StaticResource BorderGapMaskConverter}">
                <Binding ElementName="Header" Path="ActualWidth"/>
                <Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}"/>
                <Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}"/>
            </MultiBinding>
        </Border.OpacityMask>
        <Border BorderBrush="{TemplateBinding BorderBrush}" 
                BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3">
            <Border BorderBrush="Transparent" 
                    BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2"/>
        </Border>
    </Border>
</Border>

问题解决了。

统计数据本身

特定大小的单词数需要使用ConverterParameter(这也是WordCounts/WordCountItem类存在的原因)。我需要能够指定单词的大小来检索计数,但每个绑定只能有一个转换器。幸运的是,我知道所需的单词大小,因此我能够使用ConverterParameter来帮助转换器做“正确的事情”。

<TextBlock Grid.Column="1" Grid.Row="2">
    <TextBlock.Text>
        <Binding Path="Statistics.WordCount" Converter="{StaticResource StatsWordCountConverter}" 
                 ConverterParameter="3" />
    </TextBlock.Text>
</TextBlock>
<TextBlock Grid.Column="1" Grid.Row="3" Margin="0,0,0,5" >
    <TextBlock.Text>
        <Binding Path="Statistics.WordCount" Converter="{StaticResource StatsWordCountConverter}" 
                 ConverterParameter="4" />
    </TextBlock.Text>
</TextBlock>
    ...
    ...
<TextBlock Grid.Column="1" Grid.Row="3" Margin="0,0,0,5" >
    <TextBlock.Text>
        <Binding Path="Statistics.WordCount" Converter="{StaticResource StatsWordCountConverter}" 
                 ConverterParameter="10" />
    </TextBlock.Text>
</TextBlock>

这样做的目的是解决眼前的问题,但不足以满足模型本身的设计。模型在最大字母数方面是自适应的,而此设计并未考虑到这一点。ViewModel本身做到了(从3到“最长单词大小”,有一个单词计数项),所以有一天,我可能会妥善处理这个问题。

设置窗口

此视图允许用户更改设置以调整游戏玩法。我遇到的唯一问题是单选按钮,因为它们的特性是一个被选中时,另一个会被取消选中。我不想解决这个问题,所以所有RadioButton的设置/获取都在代码隐藏中完成。我在互联网上看到的所有“解决方案”都只是围绕问题的技巧,我不想去弄它。

获胜窗口

如果用户真的赢得了游戏(找到了当前游戏中的所有单词),则会显示此窗口。我没有此窗口的截图,因为我从未真正赢得过一场比赛。微笑 | <img src= " src="https://codeproject.org.cn/script/Forums/Images/smiley_smile.gif" />

代码的其他方面

Globals 对象

此对象是需要在整个应用程序会话期间访问的对象的便捷容器。

Contains 方法

当游戏开始时,此方法将为所有主词典中小于或等于游戏单词大小的单词调用。我这样做是为了避免在实际游戏过程中出现任何延迟。

首先,我为容器(游戏单词)和文本(可能的单词)创建了一个排序字符列表。我意识到对字符进行排序在技术上是不必要的,但实际上,它可以在do/while循环中节省少量时间,因为循环可能会提前退出。需要过度优化吗?也许吧。

//--------------------------------------------------------------------------------
public static bool Contains(string container, string inText)
{
    // The idea is to simply run through the two strings and delete matching 
    // characters as they're found.  If the inText string ends up as an empty 
    // string, the word did in fact contain the specified text.
    bool   contains = false;
    List<char> containerList = new List<char>();
    containerList.AddRange(container);
    containerList.Sort();

    List<char> testList = new List<char>();
    testList.AddRange(inText);
    testList.Sort();

一旦我有了两个列表,我就可以遍历其中每一个,测试当前字符,如果找到,则从每个列表中删除它。如果迭代结束时测试列表为空,则该单词是一个有效的可能单词,因此我们返回true

    bool found = false;
    do
    {
        found = false;
        for (int i = 0; i < containerList.Count; i++)
        {
            if (testList[0] == containerList[i])
            {
                testList.RemoveAt(0);
                containerList.RemoveAt(i);
                found = true;
                break;
            }
        }
    } while (found && testList.Count > 0);
		
    contains = (testList.Count == 0);
    return contains;
}

最后说明 - 此方法可能已放入GameDictionary类中,但我只是太懒了,不想把它放进去。

CalcWordScore 方法

此方法计算指定字符串的得分(在Scrabble游戏中)。它利用了一个静态的点数数组,并且无论字母大小写如何,都能确定得分。

//--------------------------------------------------------------------------------
public static int CalcWordScore(string text)
{
    text = text.ToUpper();
    int points = 0;
    foreach(char character in text)
    {
        int charInt = Convert.ToInt16(character);
        points += m_wordPoints[charInt - ((charInt >= 97) ? 97 : 65)];
    }
    return points;
}

最后说明 - 此方法可能已放入AWord类中,但我只是太懒了,不想把它放进去。您开始看到模式了吗?

RandomNumber 方法

由于单词(有时是单词大小)是随机选择的,因此我需要使用.NetRandom类。为了使每次生成的随机数更具随机性,我必须在我需要使用它的时候实例化Random对象。所以,我创建了这个方法来完成繁重的工作。

//--------------------------------------------------------------------------------
public static int RandomNumber(int min, int max)
{
    return new Random().Next(min, max);
}

同样,此方法可以放入GameDictionary类中,但 blah,blah,blah....

IntToEnum 方法

我几年前就开发了这个方法,以保护我的代码免受手动编辑的数据引起的异常及其可能带来的问题。我甚至写了一篇技巧文章 - 从可疑数据源设置枚举器(适用于C#和VB)[^]。如果您对它的作用感兴趣,可以阅读技巧文章。

如何玩游戏

游戏玩法很简单。只需启动应用程序。首先您会看到此消息框:

如果一切正常,您将被告知“准备好您的腰带”。如果找不到字典文件,您将被告知,游戏将无法在此点继续。(字典文件必须位于应用程序文件夹中。)

假设您自己的个人电子世界尚未陷入混乱(字典文件已找到),请单击“确定”,您将看到:

单击“新游戏”按钮开始游戏。届时,将显示一个随机选择的打乱字母的单词,您可以立即在“用户单词”字段中键入。

只需键入一个单词,然后按回车键。如果单词有效,它将显示在列表中,并附带该单词获得的积分(不包括可能获得的任何奖励积分)。当您输入单词时,窗口顶部/右侧的区域将显示有多少个单词是可能的,有多少个您已经找到,以及您已经获得了多少积分。在“新游戏”和“解决”按钮下方,您将看到一些随着您玩游戏而更新的统计数据。

当您找到所有可能的单词时,单击“解决”按钮,列表将更新以显示所有可能的单词。这是您单击“解决”按钮时的窗体外观。请注意列表中最后一个单词后面跟着两个星号 - 这是为游戏打乱的原始单词。讽刺的是,这个单词是COMPILING

下面是一些在游戏中找到的单词的示例。

一些细节

  • 您找到的单词将显示为蓝色。
  • 您未找到的单词将显示为灰色。
  • 如果您找到了原始单词,它将显示为红色。它还会显示两个星号(无论您是否找到它),以指示它是当前游戏最初打乱的单词。

如果您正在玩计时游戏,窗口顶部的计时器将倒计时到零,此时游戏结束。奖金时间可以在“设置”窗口中配置,并且可以在玩家每次找到指定数量的单词时获得。

最终评论

正如我在本文开头所说,这是一个简单的游戏。原始版本是我第一个完全独立设计和拥有的.Net应用程序,是用Windows Forms编写的。此版本包含的代码量少得多,并且模型/ViewModel也更简单。

玩了一会儿游戏后,我决定它确实需要某种声音提示来指示游戏已过期。也许有一天……

更改 - 2012年10月18日

在玩游戏的过程中,我被遇到的一些事情所困扰。当然,这些都是非常小的事情,但它们仍然惹恼了我。

我刚才提交了什么词?

一些打乱的单词允许找到大量的单词,当列表框开始填充单词时,您开始想知道您输入的单词是否被接受了。问题是,您需要花费时间滚动列表才能找到它(而且计时器正在倒计时,而您正在寻找单词 - 滴答,滴答)。

这个更改对ViewModel和XAML产生了中等影响。首先,我必须向DisplayWord类添加一个属性来指示该单词是最后一个找到的单词。

:

//................................................................................
public bool IsLastWordFound
{
    get { return m_isLastWordFound; }
    set
    {
        m_isLastWordFound = value;
        RaisePropertyChanged("IsLastWordFound");
    }
}

接下来,我向GameDictionary类添加了一个新属性来保留对最后一个找到单词的引用。这样我就可以避免在每次提交单词时都搜索列表来查找标记为最后一个找到的单词。如您所见,设置最后一个找到的单词也会取消之前找到的最后一个单词的最后一个单词状态。

//................................................................................
public DisplayWord LastWordFound 
{ 
    get { return m_lastWordFound; }
    set
    {
        if (m_lastWordFound != null)
        {
            m_lastWordFound.IsLastWordFound = false;
        }
        m_lastWordFound = value;
        if (m_lastWordFound != null)
        {
            m_lastWordFound.IsLastWordFound = true;
        }
        // we don't need to be notified that the last word  has changed here.
    }
}

最后,我在XAML的ListBoxItem样式中添加了一对Path元素作为最后一个找到的单词指示符。

<Path Stretch="Uniform"  Stroke="DarkGreen" Fill="DarkGreen" Data="M 0,0 7.5,7.5 0,15 0,0" Grid.Column="0" 
      Visibility="{Binding Path=IsLastWordFound, Converter={StaticResource BoolToVisibility}}" />
<StackPanel Grid.Column="1" HorizontalAlignment="Left" Orientation="Horizontal" Margin="5,0,0,0">
    <TextBlock x:Name="PART_Word" Text="{Binding Path=Text}" FontStyle="Italic" />
    <TextBlock x:Name="PART_Adorner" Text="**" FontStyle="Italic" Visibility="{Binding Path=IsOriginalWord, 
                Converter={StaticResource BoolToVisibility}}" />
</StackPanel>
<TextBlock Grid.Column="2" x:Name="PART_Points" Text="{Binding Path=Points}" FontStyle="Italic" Margin="0,0,5,0" />
<Path Stretch="Uniform" Stroke="DarkGreen" Fill="DarkGreen" Data="M 15,0 15,15 7.5,7.5 15,0" Grid.Column="3" 
      Visibility="{Binding Path=IsLastWordFound, Converter={StaticResource BoolToVisibility}}" />

此功能更改的最后一部分是将最后一个找到的单词滚动到视图中。我只是在指定方法中添加了以下一行:

//--------------------------------------------------------------------------------
private void buttonSubmit_Click(object sender, RoutedEventArgs e)
{
    ...
    ...
	this.wordList.ScrollIntoView(CurrentGameDictionary.LastWordFound);
}

游戏单词背景颜色

我有时不使用计时器玩游戏,这使得游戏单词的背景颜色与游戏单词上方的ProgressBar控件的颜色过于相似。因此,我将游戏单词控件的背景颜色更改为LightSteelBlue

游戏单词双倍间距

在确定给定游戏中可用的字母时,我遇到了一些小问题,所以我决定我想看看是否可以更改字符串的字偶间距。如果您希望找到一种明确的方法来实现这一点,我很抱歉让您失望,但我退缩了,并且只是编写了一个新的转换器类(请参阅源代码中的DoubleSpaceConverter),它在字母之间插入空格。

新游戏按钮

在游戏过程中,我有时会意外地点击“新游戏”按钮,而实际上我希望先点击“解决”按钮。为了解决这个问题,我在MainWindow.Xaml.cs的指定方法中添加了以下一行:

//--------------------------------------------------------------------------------
private void UpdateButtons()
{
    this.buttonNewGame.IsEnabled = ((CurrentGameDictionary == null) || (CurrentGameDictionary != null && !CurrentGameDictionary.IsPlaying));
    ...
    ...
}

最后说明

本节提供的屏幕截图也说明了我几天前做的1 / NN的事情。

2012年11月4日 - 修复和功能

大多数程序员都无法安于现状,我也是如此。我玩这个游戏很多,我想要一些累积统计数据。我还想修复滚动条问题。

修复

  • 列表框滚动条 - 当我最初发布这篇文章时,列表框中的滚动条出现问题。在用户找到足够的单词以至于需要滚动条之前,这个问题并未被注意到。它按预期出现,但滚动条的滑块在您开始拖动它之前不会改变大小。

    这终于让我受不了了,所以我去寻找一个解决方案。我以前从未见过这个问题,所以我想这可能与游戏词典中的所有单词都被添加到列表框中,并且它们被折叠直到用户“找到”它们有关。为什么这会混淆列表中的ScrollViewer,这只能猜测。无论如何,修复方法是在描述列表框的XAML中添加以下属性:
     
    ScrollViewer.CanContentScroll="False"
    完成这些之后,ScrollViewer就不再像醉汉水手那样行为了。
     
  • 用户单词焦点 - 如果您在系统上执行了其他操作导致应用程序窗口失去焦点,有时会在用户单词文本框中无法正确显示插入符号。
     

新统计功能

由于我玩游戏很多,我想要一种保存累积统计数据的方法,以便我可以看到随着时间的推移,我在游戏中的表现有多差。由于有相当多的指标需要保留,我决定将统计信息显示更改为TabControl。当前游戏选项卡显示与以前相同的统计信息,而累积选项卡显示 - 嗯 - 累积统计信息。这里有一些屏幕截图可以给您一个想法。

 

 

 

 

 

最后,对程序流程和功能进行了一些更改,以适应新的统计数据。

 

  • 当您开始新游戏时,选项卡控件会自动切换到**当前游戏**页面。
     
  • 当您赢得或解决正在运行的游戏,或者当运行的游戏时间到期时,选项卡控件会自动选择**累积**页面。
     
  • 累积统计数据可以从**累积**选项卡或**设置**窗口重置。
     
  • 累积统计数据默认开启,但可以在**设置**窗口中开启/关闭。
     

另一个主要变化是.Net类名被包含在词典中。我编写了一个引用了所有.Net 4.5程序集的WinForms应用程序,并从中提取了所有类名。然后,它从这些发现的类名中删除非数字字符,并创建一个包含3-10个字符的类名的文本文件。该文件创建在相应的文件夹中,并且默认由Anagrams2应用程序加载。标准Scrabble词典已经涵盖了数百个类名,因此类名版本的单词不包含在最终的整体词典中。加载此附加文件会由于正在进行的过滤而使Anagrams2应用程序的初始化时间增加几秒钟。如果您不想加载额外文件,只需将其删除即可。在**设置**窗口中,您可以选择在游戏中使用类名。

涉及发现类名的有趣代码如下:

//--------------------------------------------------------------------------------
private void BuildWordListFromClassNames()
{
    Assembly[] assemblies = null;
    try
    {
        // I tried to do this in a single Linq statement, but the first from was 
        // throwing exceptions about not being able to access some of the 
        // assemblies, so I broke it out into separate statements
        assemblies = (from assembly in AppDomain.CurrentDomain.GetAssemblies().AsParallel() 
        select assembly).ToArray<Assembly>();
    }
    catch (Exception)
    {
        // we don't care about exceptions (we take what reflect deigns to give us) 
    }
    try
    {
        if (assemblies != null)
        {
            foreach (Assembly assembly in assemblies)
            {
                var types = (from type in assembly.GetTypes().AsParallel()
                where CheckWordlength(MassageName(type.Name.ToUpper()).Length)
                select MassageName(type.Name.ToUpper())).ToArray<string>();

                if (types.Length > 0)
                {
                    foreach(string word in types)
                    {
                        if (!classnames.Contains(word))
                        {
                            classnames.Add(word);
                        }
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Oops!");
    }
    classnames.Sort();
    this.label2.Text = classnames.Count.ToString();
    this.listBox1.Items.AddRange(classnames.ToArray());
}

历史

  • 2012年10月14日 - 原始版本
  • 2012年10月15日 - 在写文章时,我注释掉了用于消除非透明边框的GroupBox样式,以便获得它的截图。我今天早上玩游戏时注意到它仍然被注释掉了。我取消了xaml的注释,重新编译,并再次上传了源代码。
  • 2012年10月15日 - 更改#2 - 我在游戏统计信息分组框中添加了每个字母数的可能单词数量。这需要更改几个GameStatisticsWordCountsWordCountItem的构造函数,以及处理数据的转换器。结果是信息显示为:3-letter words: 0 / NN,其中NN是3个字母单词的可能数量。我还将ListBox字体更改为Consolas。ZIP文件已重新上传 - 再次。
  • 2012年10月15日 - **重要通知!** *我认为*您必须先安装.Net 4.5才能运行此应用程序,或者通过删除`private`访问器来更改`GameDictionary.PercentRemaining`属性的`set`方法为`public`。
  • 2012年10月18日 - 添加了一些可用性更改(详见上文)。
  • 2012年11月4日 - 添加了一些功能并修复了一些错误(详见上文)。
© . All rights reserved.