Hangman on Silverlight

06 Feb 2008

หลังจากติดค้างพี่ป๊อกมานาน ก็ได้เวลาใช้หนี้ซักที

เรามาดูกันดีกว่าว่าไอ้เจ้า Hangman นี่มันเขียนยากมั้ย


วันที่ 25 ม.ค. 2550 เวลา 3 โมงครึ่ง

เปิดโปรแกรม Microsoft Expression Blend มาวาดนู่นวาดนี่ แล้วก็แก้ไปแก้มา ก็เลยได้เป็นโค้ดนี้

<Canvas Width="32" Height="32" Canvas.Left="0" Canvas.Top="16" x:Name="ACanvas" Cursor="Hand">
    <Rectangle Width="32" Height="32" Fill="#FF697BCA" RadiusX="3" RadiusY="3" RenderTransformOrigin="0.5,0.5" x:Name="RectangleA">
        <Rectangle.RenderTransform>
            <TransformGroup>
                <ScaleTransform ScaleX="1" ScaleY="1"/>
            </TransformGroup>
        </Rectangle.RenderTransform>
    </Rectangle>
    <TextBlock Text="A" Canvas.Left="10" Canvas.Top="6"/>
</Canvas>

อันนี้คือสร้าง Canvas ขึ้นมา ไปวางไว้ที่พิกัด Canvas.Top กับ Canvas.Left ที่กำหนด โดยกำหนดให้ชื่อว่า ACanvas โดยเจ้า Canvas นี้ก็จะประกอบด้วย ข้อความ 1 ข้อความแสดงข้อความว่า A อยู่ที่พิกัด (Left, Top) คือ (10,6) และ สี่เหลี่ยม 1 อัน ขนาด 32 x 32 สีฟ้า ชื่อ RectangleA

จากนั้นก็จัดการเขียนสคริปต์สร้างเป็นโค้ด XAML สำหรับตัวอักษร 26 ตัว แหะๆ ตรงนี้ขี้โกงด้วยการใช้โค้ด Python สร้างเอา

ทำตรงนี้ได้ซักพัก เพื่อนก็เอาโค้ดมาให้ช่วยดู แล้วก็เลยนั่งแก้โค้ด แล้วเย็นนั้นก็ไปกินข้าวกับเพื่อนต่อ


วันที่ 25 ม.ค. 2550 เวลา 5 ทุ่ม

กลับถึงบ้าน ทำธุระส่วนตัวเรียบร้อยก็ได้เวลาเปิดคอมแล้วก็ …. คร่อก หลับไปแว้วววว


วันที่ 26 ม.ค. 2550 เวลาตีห้า

สะดุ้งตื่นพร้อมกับนึกขึ้นได้ว่ายังเขียนไม่เสร็จเลยเอามาเขียนต่อ คราวนี้จัดการเพิ่ม animation และ storyboard ลงไป ดังนี้

<Canvas.Resources>
            <Storyboard x:Name="ABigger">
                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="RectangleA" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
                            <SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="2"/>
                    </DoubleAnimationUsingKeyFrames>
                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="RectangleA" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
                            <SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="2"/>
                    </DoubleAnimationUsingKeyFrames>
                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="RectangleA" Storyboard.TargetProperty="(UIElement.Opacity)">
                            <SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="0"/>
                    </DoubleAnimationUsingKeyFrames>
            </Storyboard>

อันนี้คือเราสร้าง Storyboard ขึ้นมา ชื่อว่า ABigger โดยข้างในจะประกอบด้วย animation ดังนี้
1. กำหนด ScaleX ของ RectangleA ไปเป็น 2 ในเวลา 0.5 วินาที
2. กำหนด ScaleY ของ RectangleA ไปเป็น 2 ในเวลา 0.5 วินาที
3. กำหนด Opacity ของ RectangleA ไปเป็น 0 ในเวลา 0.5 วินาที

ปรากฏว่าโอเค ก็เลยได้เวลาสคริปต์เจ้าเก่า จัดการโมซะ


วันที่ 26 ม.ค. 2550 เวลาตีห้าสี่สิบ

เริ่มเขียนตัวจัดการ event เลยเขียนตัวเล่น animation ก่อน

    private void buttonPressed(char p)
    {
            string canvasName = p + "Canvas";
            Canvas pressedCanvas = this.FindName(canvasName) as Canvas;
            pressedCanvas.Cursor = Cursors.Default;

            string animationName = p + "Bigger";
            Storyboard biggerAnimation = this.Resources.FindName(animationName) as Storyboard;
            biggerAnimation.Begin();


    }

ตรงนี้จะใช้สำหรับเวลาปุ่มโดนกด คือจะไปวิ่งทำให้ Canvas ของปุ่มนั้นไม่เปลี่ยนเคอเซอร์เวลาเอาเมาส์ไปชี้ แล้วก็จัดการเล่นแอนิเมชันของปุ่ม

แล้วค่อยเขียนตัว Event Handler แบบนี้

    void ACanvas_MouseLeftButtonDown(object sender, MouseEventArgs e)
    {
        buttonPressed('A');
    }

เพื่อบอกว่า ถ้าเกิดกดปุ่มนี้แล้วก็ให้ไปเรียกเมธอด buttonPressed ด้านบนพร้อมบอกด้วยว่ากดปุ่มไหน

แล้วก็ไปแก้หน้า XAML ให้เรียก event ที่ตรงกับที่สร้างไว้

<Canvas Width="32" Height="32" Canvas.Left="0" Canvas.Top="16" x:Name="ACanvas" Cursor="Hand" MouseLeftButtonDown="ACanvas_MouseLeftButtonDown">
    <Rectangle Width="32" Height="32" Fill="#FF697BCA" RadiusX="3" RadiusY="3" RenderTransformOrigin="0.5,0.5" x:Name="RectangleA">
        <Rectangle.RenderTransform>
            <TransformGroup>
                <ScaleTransform ScaleX="1" ScaleY="1"/>
            </TransformGroup>
        </Rectangle.RenderTransform>
    </Rectangle>
    <TextBlock Text="A" Canvas.Left="10" Canvas.Top="6"/>
</Canvas>

แล้วก็จัดการแก้โค้ด python ให้สร้างโค้ดออกมาให้ถูก


วันที่ 26 ม.ค. 2550 เวลาหกโมงห้านาที

จัดการเขียนโค้ดส่วนที่เป็นตัวตัดสินใจ ก็เลยแก้โค้ดตอนรันนิดหน่อยด้วยการสร้าง Hash ขึ้นมาเก็บว่าตัวไหนใช้ไปแล้วบ้าง พร้อมกับคำตอบที่ถูกต้อง

    private Dictionary<char, bool> usedCharMap = new Dictionary<char, bool>(); 
    private string answerKey = "BARCAMP BANGKOK";

แล้วก็สร้างเมธอด reset เพื่อใช้สำหรับเริ่มเกมใหม่

    private void initCharMap()
    {
        usedCharMap.Clear();
        for (int i = 0; i < 26; i++)
        {
            usedCharMap.Add((char)('A' + i), false);
        }
    }
    private int badCount = 0;
    private void reset()
    {
        badCount = 0;
        for (int i = 0; i < 26; i++)
        {
            string animationName = (char)('A' + i) + "Bigger";
            Storyboard biggerAnimation = this.Resources.FindName(animationName) as Storyboard;
            biggerAnimation.Stop();
        }
        initCharMap();           
    }

ตอน reset ก็จะจัดการหยุดแอนิเมชันทั้งหมดไปเลยด้วย พร้อมทั้งสร้าง hash ใหม่ด้วย
แล้วตอนอีเวนท์ Page Load ก็สั่ง reset ซะหนึ่งที
private void Page_Loaded(object sender, EventArgs args)
{
// Required to initialize variables. Needs to be done from loaded event so FindName works properly.
InitializeComponent();

        // Insert code required on object creation below this point.
        reset();
    }

แล้วก็เพิ่มข้อความที่ใช้บอกสถานะ และขีดที่บอกคำศัพท์เข้าไปใน XAML

<Canvas Width="48" Height="26" Canvas.Left="32" Canvas.Top="233" MouseLeftButtonDown="ResetButton_MouseLeftButtonDown">

    <Rectangle Fill="#FFFFFFFF" Stroke="#FF000000" Width="48" Height="20"/>

    <TextBlock Text="Reset" TextWrapping="Wrap"/>

</Canvas>

<TextBlock Canvas.Left="32" Canvas.Top="185" Text="TextBlock" TextWrapping="Wrap" x:Name="AnswerTextBlock" FontSize="20"/>

<TextBlock Width="63" Height="19" Canvas.Left="545" Canvas.Top="233" Text="" TextWrapping="Wrap" RenderTransformOrigin="0.5,0.5" Foreground="#FFFF0000" x:Name="statusTextBlock">
    <TextBlock.RenderTransform>
        <TransformGroup>
            <ScaleTransform ScaleX="1" ScaleY="1"/>
            <SkewTransform AngleX="0" AngleY="0"/>
            <RotateTransform Angle="0"/>
            <TranslateTransform X="0" Y="0"/>
        </TransformGroup>
    </TextBlock.RenderTransform>
</TextBlock>

วันที่ 26 ม.ค. 2550 เวลาหกโมงครึ่ง

โค้ดที่ตัดสินใจจริงๆยังไม่เสร็จก็เลยเขียนเมธอดที่ใช้สร้างตัวว่างๆสำหรับคำตอบขึ้นมา

    private void createBlindStringAndCheckMatch()
    {
        string newText = "";
        bool match = true;
        foreach (char i in answerKey.ToCharArray())
        {
            if (i == ' ')
            {
                newText += "  ";
            }
            else if (usedCharMap[i])
            {
                newText += i;
            }
            else
            {
                newText += "_";
                match = false;
            }
            newText += "  ";
        }
        allMatch = match;
        AnswerTextBlock.Text = newText;
        statusTextBlock.Text = "Wrong guess : " + badCount;
    }

จัดการสร้าง blind string ด้วยการวนไปครบทุกตัว ตัวไหนที่ตอบถูกก็แสดงขึ้นมา ส่วนตัวที่ยังตอบไม่ถูกก็ใช้เป็นขีดล่างแทน


วันที่ 26 ม.ค. 2550 เวลาหกโมงสี่สิบ

ใส่เงื่อนไขการชนะ แล้วก็แก้ไขตอนที่กดปุ่มไปนิดหน่อยว่าถ้าตอบผิดเกิน 10 ครั้งก็ให้หยุดเล่น

    private void buttonPressed(char p)
    {
        if (!allMatch && badCount < 10 && !usedCharMap[p])
        {
            string canvasName = p + "Canvas";
            Canvas pressedCanvas = this.FindName(canvasName) as Canvas;
            pressedCanvas.Cursor = Cursors.Default;

            string animationName = p + "Bigger";
            Storyboard biggerAnimation = this.Resources.FindName(animationName) as Storyboard;
            biggerAnimation.Begin();

            usedCharMap[p] = true;
            if ((answerKey.IndexOf(p) < 0))
            {
                badCount++;
            }
            createBlindStringAndCheckMatch();
            checkWinLoseCondition();
        }
    }

    private void checkWinLoseCondition()
    {
        if (allMatch)
        {
            statusTextBlock.Text = "You win";
        }
        else if (badCount >= 10)
        {
            statusTextBlock.Text = "You lose";
        }
    }

วันที่ 26 ม.ค. 2550 เวลาเจ็ดโมง

ตอนนี้ก็ถึงเวลาว่าจะทำยังไงให้คำมันเปลี่ยนเองทุกครั้งที่ reset

ตอนแรกใช้วิธีดึงจาก Text file แต่ไม่ผ่าน

จากนั้นก็เปลี่ยนไปเรียก Web Service ก็เลยสร้าง Web Service ขึ้นมา ชื่อว่า WordService

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
[System.Web.Script.Services.ScriptService]
public class Service1 : System.Web.Services.WebService
{
    private static List<string> _wordList;
    private static List<string> WordList
    {
        get
        {
            if (_wordList == null)
            {
                _wordList = new List<string>();
                FileStream fs = File.OpenRead(HttpContext.Current.Server.MapPath("~/App_data/full.dic"));
                StreamReader reader = new StreamReader(fs);
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    _wordList.Add( line.Substring(0, line.IndexOf("\t")).ToUpper());
                }
            }

            return _wordList;
        }
    }
    [WebMethod]
    [ScriptMethod]
    public string GetRandomWord()
    {
        Random r = new Random();
        return WordList[r.Next(WordList.Count)];
    }
}

จัดการเพิ่ม Web Reference ไปยัง Word Service แล้วก็สร้างเป็นคลาสสำหรับดึงข้อมูลมาดังนี้

public class DictHelper
{
    public string GetRandomWord()
    {
        return new dic.Service1().GetRandomWord();
    }
}

แล้วก็เลยแก้ให้มาเรียกใช้ส่วนนี้ซะ

    private void reset()
    {
        badCount = 0;
        for (int i = 0; i < 26; i++)
        {
            string animationName = (char)('A' + i) + "Bigger";
            Storyboard biggerAnimation = this.Resources.FindName(animationName) as Storyboard;
            biggerAnimation.Stop();
        }
        initCharMap();

        answerKey = dic.GetRandomWord();

        createBlindStringAndCheckMatch();
        statusTextBlock.Text = "";
    }

วันที่ 25 ม.ค. 2550 เวลาเจ็ดโมงสี่สิบ

เย้ เย เสร็จซะที ง่ายจัง

Comments

หรูสุดๆ มีปุ่มตัวอักษรเด้งดึ๋งด้วย
แต่ผมว่า มันน่าจะทำเป็น component หรือ widget ได้นะ
แล้วค่อยเอามาสร้างเป็น ปุ่ม a-z อีกที

ไว้ผมจะดูใน flex บ้างว่าสามารถสร้าง effect แบบที่คุณทำได้ไหม

Posted by pphetra | Feb 7th, 2008 at 4:05 pm | Reply

@pphetra ทำเป็น component ได้แน่ๆครับ แต่คราวก่อนผมลองแล้วมันไม่สามารถ (จริงๆแล้วคือผมไม่สามารถ)

Posted by wiennat | Feb 7th, 2008 at 10:52 pm | Reply

I think this article done a great job.What a best way to describe your view. Thanks for sharing with us. Really like your informative article. Hopefully we will get more interesting topic from you in future.

Posted by warhammer online gold | Oct 14th, 2008 at 9:27 pm | Reply

Post new comment

The content of this field is kept private and will not be shown publicly.