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

Windows Phone 填字游戏

starIconstarIconstarIconstarIconstarIcon

5.00/5 (22投票s)

2012年6月30日

CPOL

6分钟阅读

viewsIcon

66709

downloadIcon

1410

了解如何利用在线网络资源创建 Windows Phone 填字游戏

目录

引言

在这篇文章中,我将解释如何使用 Windows Phone 框架访问互联网上的现有资源来创建一个新游戏——报纸风格的填字游戏。

互联网上充满了各种有用且免费的服务,我们只需要找到适合我们需求的服务即可。在我们的案例中,我们需要一些在线英语词典来提供填字游戏的线索。我希望下面的文章能对那些正在寻找类似应用程序的人有所帮助。

系统要求

要使用本文提供的填字游戏应用程序,您必须安装 Windows Phone SDK 7.1,您可以直接从 Microsoft 100% 免费下载。

选择谜题大小

有 3 种不同的谜题尺寸可供选择:4 x 4、7 x 7 和 10 x 10。用户可以从主菜单中选择其中一个,然后应用程序将根据用户选择的尺寸生成网络请求的查询字符串并处理 HTML 响应。

当用户选择不同尺寸时,菜单中显示的图片会相应改变,新尺寸会存储在一个局部变量中。

        private void RadioButton_Click(object sender, RoutedEventArgs e)
        {
            if (rbt4x4.IsChecked.Value)
                size = 4;
            else if (rbt7x7.IsChecked.Value)
                size = 7;
            else if (rbt10x10.IsChecked.Value)
                size = 10;

            var canPlay = true;

            btnNewGame.Visibility = canPlay ? Visibility.Visible : Visibility.Collapsed;
            btnPurchase.Visibility = canPlay ? Visibility.Collapsed : Visibility.Visible;

            imgSize.Source = new BitmapImage(new Uri(string.Format(@"Images/{0}x{0}.png", 
                size), UriKind.Relative));
        }

当用户点击“新游戏”按钮时,NavigationService 会被告知显示 Board.xaml 页面,该页面会接受之前选择的尺寸

        private void btnNewGame_Click(object sender, RoutedEventArgs e)
        {
            NavigationService.Navigate(
                new Uri(string.Format("/Board.xaml?StartMode=2&Size={0}", size), 
                UriKind.Relative));
        }

下载谜题 HTML

需要注意的是,填字游戏并非由本应用程序生成。要在您的应用程序中完成此操作,需要一个庞大的“单词列表”和一个优秀的算法。相反,我们将保持应用程序精简轻巧,并借助互联网来完成这项繁重的工作。这意味着谜题必须由其他地方(某个网站)生成。我选择的网站来自麻省理工学院 (MIT),由 Robert Morris 教授提供。

Morris 教授提供了一个非常棒的在线填字游戏生成器,您可以通过查询字符串传递空单元格。生成器将假定未提供的单元格是“被阻挡”的单元格,因此这些特定单元格将保持为空。

在我们的应用程序中,我们通过提供 3 种不同类型的查询字符串来调用填字游戏生成器,每种谜题大小(4x4、7x7 和 10x10)对应一个。

  1. 谜题大小:4x4。请求:http://pdos.csail.mit.edu/cgi-bin/theme-cword?r0c0=&r0c1=&r0c2= &r0c3=&r1c0=&r1c1=&r1c2=&r1c3=&r2c0=&r2c1=&r2c2=&r2c3=&r3c0=&r3c1=&r3c2=&r3c3=

  2. 谜题大小:7x7。请求:http://pdos.csail.mit.edu/cgi-bin/theme-cword?r0c0=&r0c1=&r0c2= &r0c3=&r0c4=&r1c0=&r1c1=&r1c2=&r1c3=&r1c5=&r1c6=&r2c0=&r2c1=&r2c2=&r2c3=&r2c5=&r2c6=&r3c0=&r3c3= &r3c4=&r3c5=&r3c6=&r4c1=&r4c2=&r4c3=&r4c4=&r4c5=&r4c6=&r5c0=&r5c1=&r5c2=&r5c3=&r5c4=&r5c5=&r5c6= &r6c0=&r6c1=&r6c2=&r6c3=&r6c5=&r6c6=

  3. 谜题大小:10x10。请求:http://pdos.csail.mit.edu/cgi-bin/theme-cword?r0c0=&r0c1= &r0c2=&r0c3=&r0c6=&r0c7=&r0c8=&r1c0= &r1c1=&r1c2=&r1c3=&r1c5=&r1c6=&r1c7=&r1c8=&r2c0=&r2c1=&r2c2=&r2c3=&r2c4=&r2c5=&r2c6=&r2c7=&r2c8= &r2c9=&r3c0=&r3c1=&r3c2=&r3c3=&r3c4=&r3c5=&r3c6=&r3c7=&r3c8=&r3c9=&r4c3=&r4c4=&r4c5=&r4c7=&r4c8= &r4c9=&r5c1=&r5c2=&r5c3=&r5c4=&r5c5=&r5c6=&r5c8=&r5c9=&r6c0=&r6c1=&r6c2=&r6c4=&r6c5=&r6c6=&r6c7= &r7c0=&r7c1=&r7c2=&r7c3=&r7c5=&r7c6=&r7c7=&r7c8=&r7c9=&r8c0=&r8c1=&r8c2=&r8c3=&r8c4=&r8c6=&r8c7= &r8c8=&r8c9=&r9c1=&r9c2=&r9c3=&r9c4=&r9c5=&r9c6=&r9c7=&r9c8=&r9c9=

正如您在上面的链接中看到的,querystring 可能非常长且生成起来很麻烦。我们不只是请求一个硬编码的查询字符串,而是使用一对函数将二维字符串映射(由 0 和 1 组成,其中 0 代表空单元格)转换为预期的游戏 querystring

public class HtmlParser
    {
        .
        .
        .
        public void GetPuzzleHtml(int size, Action<string> onSuccess, 
        Action onFailure, Action<int> onProgressChanged)
        {
            var ret = string.Empty;
            var tileMap = string.Empty;

            switch (size)
            {
                case 4:
                    tileMap = string.Concat(
                        "0000",
                        "0000",
                        "0000",
                        "0000");
                    break;
                case 7:
                    tileMap = string.Concat(
                        "0000011",
                        "0000100",
                        "0000100",
                        "0110000",
                        "1000000",
                        "0000000",
                        "0000100");
                    break;
                case 10:
                    tileMap = string.Concat(
                        "0000110001",
                        "0000100001",
                        "0000000000",
                        "0000000000",
                        "1110001000",
                        "1000000100",
                        "0001000011",
                        "0000100000",
                        "0000010000",
                        "1000000000");
                    break;
            }

            var queryString = GetRequestQueryStringBySize(size, tileMap);
            
            var url = string.Format("http://pdos.csail.mit.edu/cgi-bin/theme-cword{0}", 
            queryString.AppendFormat("&t={0}", DateTime.Now.Millisecond));

            DownloadString
                (url,
                //onSuccess
                (html) =>
                {
                    onSuccess(html);
                },
                //onFailure
                () =>
                {
                    onFailure();
                },
                //progress
                (percentage) =>
                {
                    onProgressChanged(percentage);
                });
        }

请注意,上面的代码显示了对 WebRequest 类进行的异步调用。当响应准备就绪时,我们调用名为 endGetResponse 的操作,该操作将负责解析 HTML 响应(我们将在本文后面详细讨论此过程)。

GetRequestQueryStringBySize 接收谜题的尺寸和谜题的文本映射,并根据谜题生成页面预期的内容生成请求 QueryString

        private static StringBuilder GetRequestQueryStringBySize(int size, string tileMap)
        {
            var queryString = new StringBuilder();
            for (var row = 0; row < size; row++)
            {
                for (var col = 0; col < size; col++)
                {
                    var val = tileMap[row * size + col];
                    if (val == '0')
                    {
                        var prefix = "&";
                        if (string.IsNullOrEmpty(queryString.ToString()))
                        {
                            prefix = "?";
                        }
                        queryString.AppendFormat("{0}r{1}c{2}=", prefix, row, col);
                    }
                }
            }
            return queryString;
        }
    }

解析谜题 HTML

填字游戏生成器生成的 HTML 相当简单。但我们对 HTML 标签并不真正感兴趣,所以我们必须首先去除所有这些 tabletrtd 标记,这样我们就能得到构成填字游戏的字母。

    public class HtmlParser
    {
        const string msgFormat = "table[{0}], tr[{1}], td[{2}], code: {3}";
        const string table_pattern = "<table.*?>(.*?)</table>";
        const string tr_pattern = "<tr>(.*?)</tr>";
        const string td_pattern = "<td.*?>(.*?)</td>";
        const string code_pattern = "<code>(.*?)</code>";

        string html = string.Empty;
        WebRequest request;

        public HtmlParser(int size, Action<string> onSuccess, 
                          Action onFailure, Action<int> onProgressChanged
            )
        {
            GetPuzzleHtml(size, 
                //onSucess
                (html) =>
                {
                    this.html = html;
                    onSuccess(html);
                },
                //onFailure
                () =>
                {
                    onFailure();
                },
                //onProgressChanged
                (percentage) =>
                {
                    onProgressChanged(percentage);
                }
            );
        }

        public HtmlParser(string html)
        {
            this.html = html;
        }

        private List<string> GetContents(string input, string pattern)
        {
            MatchCollection matches = Regex.Matches(input, pattern, RegexOptions.Singleline);
            List<string> contents = new List<string>();
            foreach (Match match in matches)
                contents.Add(match.Value);

            return contents;
        }

        public string Parse()
        {
            List<string> tableContents = GetContents(html, table_pattern);
            StringBuilder ret = new StringBuilder();
            int tableIndex = 0;
            foreach (string tableContent in tableContents)
            {
                List<string> trContents = GetContents(tableContent, tr_pattern);
                int trIndex = 0;
                foreach (string trContent in trContents)
                {
                    List<string> tdContents = GetContents(trContent, td_pattern);
                    int tdIndex = 0;
                    foreach (string tdContent in tdContents)
                    {
                        Match code_match = Regex.Match(tdContent, code_pattern);
                        string code_value = code_match.Groups[1].Value.Replace(" ", "");
                        
                        if (string.IsNullOrEmpty(code_value))
                            code_value = " ";

                        ret.Append(code_value);
                        tdIndex++;
                    }
                    ret.Append("");
                    trIndex++;
                }
                tableIndex++;
            }

            var words = ret.ToString();

            return words;
        }
        .
        .
        .
    }

请注意,onHtmlReady 回调被传递给 HtmlParser 类,并在 HTML 准备好在我们的应用程序中渲染时执行。

    public HtmlParser(int size, Action<string> onHtmlReady)
    {
        GetPuzzleHtml(size, (html) =>
        {
            this.html = html;
            onHtmlReady(html);
        });
    }

请求词典网络服务

现在我们有了谜题,我们应该向用户提供线索。不幸的是,我们不能使用生成填字网格的同一个网站来为我们提供线索。在这种情况下,我们应该寻找某种“在线英语词典”。我找到了一个很好的词典,由 http://www.aonaware.com/ 托管,我相信寻找在线词典资源的读者也会觉得它非常有帮助。

如您所见,它是一个 SOAP 网络服务。该服务提供的唯一需要在应用程序中调用的方法是 DefineInDict,它需要:

  • 词典代码
  • 被搜索的单词

在“词典 ID”中,我们简单地使用 wn,这意味着“Word Net”。Word Net 是可用的词典之一,我认为与其他词典相比,它使用起来更简单。

这是词典网络服务请求的核心:对于填字网格中的每个拆分单词,我们都会发出一个请求,当网络服务返回特定单词的定义时,会调用回调方法 client_DefineInDictCompleted 并解析结果。

    public class DictionaryHelper
    {
        Dictionary<string, string> wordDict = new Dictionary<string, string>();
        public DictionaryHelper(Dictionary<string, string> wordDict)
        {
            this.wordDict = wordDict;
        }

        public void GetDictionaryEntries(Action<string, string, string> callback)
        {
            var client = new dictServiceRef.DictServiceSoapClient();

            client.DefineInDictCompleted += 
               new EventHandler<dictServiceRef.DefineInDictCompletedEventArgs>((s, e) =>
            {
                if (e.Error == null)
                {
                    client_DefineInDictCompleted((string)e.UserState, e.Result, callback);
                }
            });

            foreach (var word in wordDict)
            {
                try
                {
                    client.DefineInDictAsync("wn", word.Value, 
                             string.Format("{0}|{1}", word.Key, word.Value));
                }
                catch
                {
                }
            }
        }

        void client_DefineInDictCompleted(string keyAndValue, 
            WordDefinition wordDefinition, Action<string, string, string> callback)
        {
            var definitions = wordDefinition.Definitions;

            var dic = new Dictionary<string, string>();

            var split = keyAndValue.Split('|');
            var key = split[0];
            var value = split[1];
            if (definitions.Length == 0)
            {
                callback(key, value, "");
            }
            else
            {
                foreach (var definition in definitions)
                {
                    if (definition.Dictionary.Id == "wn")
                    {
                        var def = definition.WordDefinition.ToLower();
                        var postColon = def;

                        if (def.Split(':').Length > 1)
                            postColon = def.Split(':')[1];

                        var preSemicolon = postColon.Split(';')[0];
                        preSemicolon = preSemicolon
                            .Replace("\n", " ");

                        RegexOptions options = RegexOptions.None;
                        Regex regex = new Regex(@"[ ]{2,}", options);
                        preSemicolon = regex.Replace(preSemicolon, @" ");

                        var preOpenBrackets = preSemicolon.Split('[')[0];

                        var preNumber = preOpenBrackets.Split("2:".ToCharArray())[0];

                        preNumber = preNumber.Trim().Replace("\r\n", "");

                        var shortDefinition = preNumber;

                        if (def.StartsWith(string.Format("{0}\n     n", value.ToLower())))
                        {
                            shortDefinition += " (noun)";
                        }

                        callback(key, value, shortDefinition);
                        break;
                    }
                }
            }
        }
    }

请注意,我们解析定义是为了使呈现的线索对用户来说不会显得臃肿。让我们以单词“retrograde”为例,看看我们如何解析它。

现在看看我们如何在应用程序中显示简短的线索。

选择单词

一旦应用程序向词典发出所有请求,用户就可以自由选择单词并输入字母。为了方便单词选择,应用程序允许在谜题网格上进行“滑动”手势。然后,应用程序根据滑动起点和滑动终点的坐标组合来确定已选择的单词。

首先,我们必须订阅 System.Windows.Input.Touch.Touch 类的 FrameReported 事件,以便我们可以拦截和处理我们想要处理的手势(在本例中是滑动手势)。

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        AfterEnteredPage();
        Touch.FrameReported += new TouchFrameEventHandler(Touch_FrameReported);
    }

我们可以通过 FrameReported 事件的 TouchFrameEventArgs 参数获取所有手势信息。每个滑动手势至少生成三个不同的动作:TouchAction.DownTouchAction.MoveTouchAction.Up。然后我们收集这些点并调用一个名为 SelectSquaresByPosition 的函数来相应地选择单词。

        void Touch_FrameReported(object sender, TouchFrameEventArgs e)
        {
            try
            {
                var touchPoint = e.GetPrimaryTouchPoint(grdTileContainer);
                if (touchPoint.Action == TouchAction.Down)
                {
                    swipeDownPoint = touchPoint.Position;
                }
                else if (touchPoint.Action == TouchAction.Move)
                {
                    swipeMovePoint = touchPoint.Position;
                }
                else if (touchPoint.Action == TouchAction.Up)
                {
                    swipeUpPoint = touchPoint.Position;

                    if (swipeDownPoint != new Point(0, 0) &&
                        swipeMovePoint != new Point(0, 0))
                    {
                        boardViewModel.SelectSquaresByPosition(grdTileContainer.ActualWidth, 
                        grdTileContainer.ActualHeight, swipeDownPoint, swipeUpPoint);

                        swipeDownPoint =
                        swipeMovePoint =
                        swipeUpPoint = new Point(0, 0);

                        ShowClues();
                    }
                }
            }
            catch (ArgumentException ex)
            {
            }
        }

我尝试在游戏中创建一个“智能”滑动手势,首先只选择单词的空单元格(这样用户就不必重新输入所有字母)。当用户再次在同一个单词上滑动时,应用程序会选择所有单元格。再次滑动将只选择空单元格,依此类推。这些图片比文字更能说明这个概念。

首先,我们选择要选择的单词。

然后我们滑过那个单词。请注意,只有空方块被选中。

再次滑动,我们告知应用程序我们要选择整个单词(并且可能通过重新输入整个单词,而不仅仅是空单元格来更改它)。

这就是实现魔术的代码。请注意,我们首先仅在以下条件下选择空单元格:

  • 该单词中存在一些未选择的空方块,或者
  • 该单词中的所有方块均已被选中
        public void SelectSquaresByWordId(string wordId1)
        {
            var isSomeSquareUnSelected =
                squares
                    .Where(s => s.WordId.Split(',').Contains(wordId1)
                                && !s.IsSelected).Any();

            var isSomeEmptySquareUnSelected =
                squares
                    .Where(s => s.WordId.Split(',').Contains(wordId1)
                                && (s.UserLetter ?? "").Trim() == ""
                                && !s.IsSelected).Any();

            if (isSomeEmptySquareUnSelected || !isSomeSquareUnSelected)
            {
                squares
                    .ToList()
                    .ForEach(s =>
                    {
                        var split = s.WordId.Split(',');
                        s.IsSelected = split.Contains(wordId1) && 
                                       (s.UserLetter ?? "").Trim() == "";
                        squaresChangedCallback(squares,
                            new NotifyCollectionChangedEventArgs
                                      (NotifyCollectionChangedAction.Add,
                             s, s.Row * size + s.Column));
                    });
            }
            else
            {
                squares
                    .ToList()
                    .ForEach(s =>
                    {
                        var split = s.WordId.Split(',');
                        s.IsSelected = split.Contains(wordId1);
                        squaresChangedCallback(squares,
                            new NotifyCollectionChangedEventArgs
                                      (NotifyCollectionChangedAction.Add,
                             s, s.Row * size + s.Column));
                    });
            }

            var txt = "";

            if (clues.Count() > 0)
            {
                var selectedClue = clues.Where(c => c.WordId == wordId1);
                if (selectedClue.Any())
                {
                    var clue = selectedClue.First();
                    txt = clue.Definition;
                }
                TxtClue = txt.Trim();
            }
        }

最终思考

希望您喜欢这款应用程序和这篇文章。请随时在下方留下您的意见!

历史

  • 2012-06-30:初始版本
  • 2012-07-02:大小选择解释
  • 2012-07-03:单词选择解释
© . All rights reserved.