Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Home

tompaana edited this page · 4 revisions
Clone this wiki locally

Sudokumaster is a Sudoku mobile game developed with Silverlight for Windows Phone devices. The game is a logic-based, combinatorial number-placement puzzle with nine 3x3 grids each containing all the digits from 1 to 9. In the beginning only some of the numbers are placed in the grids and the player needs to figure out the correct positions for the missing numbers.

Screenshot

Instructions

An empty Sudoku game board is displayed after the application is started. The menu on bottom of the screen contains two buttons, new game and highscores. Press the new game -button to start the game. Tap on an empty cell on the grid, and a dialog pops up where you can select a number or clear the value of the cell. Only the empty cells and the cells player has set earlier (cells with white numbers) can be manipulated. The objective of the game is to fill the board with numbers between 1 and 9 according the following guidelines:

  • Number can appear only once on each row
  • Number can appear only once on each column
  • Number can appear only once on each region

A region is 3x3 square, and the board is divided into 3x3 regions identified by the lighter and darker cells.

Below the board are three icons and numbers besides them; number of moves the player has made, remaining empty cells and game time.

The game ends when all the cells are filled, and player's name is queried if a new high score was achieved.

Implementation notes

See also the porting story.

Starting a new game

When the application starts, the board grid of MainPage is populated with Cells in the MainPage.CreateGrid method, and the array of Cells is passed to GameLogic. GameLogic.GeneratePuzzle creates a random Sudoku board and assigns numbers to the Cells when a new game is started. The puzzle is generated in another thread to prevent it from blocking the UI thread;

    new Thread(() =>
    {
        Deployment.Current.Dispatcher.BeginInvoke(() =>
        {
            game.GeneratePuzzle();
            ...
        });
    }).Start();

A spinning circle animation is displayed while the puzzle is being generated. This is achieved with a transform animation in WaitNote.xaml;

    <Rectangle.Resources>
        <Storyboard x:Name="spinAnimation">
            <DoubleAnimation
                    Storyboard.TargetName="Transform" 
                    Storyboard.TargetProperty="Angle"
                    By="360" 
                    Duration="0:0:1.5"  
                    AutoReverse="False" 
                    RepeatBehavior="Forever" />
        </Storyboard>
    </Rectangle.Resources>
    <Rectangle.RenderTransform>
        <RotateTransform x:Name="Transform" CenterX="75" CenterY="75" Angle="0" />
    </Rectangle.RenderTransform>

The animation is started upon construction of the WaitNote;

    public WaitNote()
    {
        InitializeComponent();
        spinAnimation.Begin();
    }

Manipulating cells

When player touches an empty Cell, the Cell's event handler, Cell.UserControl_ManipulationCompleted, plays a sound effect, creates an instance of NumberSelection control and places it above the cell. All the sound effects in the game are played using XNA's SoundEffect;

    static public void PlaySound(string soundFile)
    {
        using (var stream = TitleContainer.OpenStream(soundFile))
        {
            var effect = SoundEffect.FromStream(stream);
            FrameworkDispatcher.Update();
            effect.Play();
        }
    }

NumberSelection.TextBlock_ManipulationCompleted is called if any one of the TextBlocks on the selection control are touched. The TextBlock's value is passed to GameLogic with SetNumberByPlayer method, and the selection dialog is closed. GameLogic.SetNumberByPlayer checks for conflicts (rows, columns, regions), and if there were conflicts, a sound is played and the blink animation of conflicting Cells is started with blinkAnimation.Begin(). Otherwise the value of the Cell is changed, and the game continues as usual.

The blinking red animation and the fade in/out animations of cells are defined in Cell.xaml. The cell has a initially transparent red grid on top of the text block. The animation is done by changing the opacity of the foreground grid;

    <Grid x:Name="LayoutRoot" Background="{x:Null}" Width="50" Height="50">
        <Grid x:Name="Foreground" Background="Red" Opacity="0">
            <Grid.Resources>
                <Storyboard x:Name="fadeInAnimation">
                    <DoubleAnimation Storyboard.TargetName="Foreground"
                        Storyboard.TargetProperty="Opacity" From="0.0"
                        To="1.0" Duration="0:0:1" />
                </Storyboard>

                <Storyboard x:Name="fadeOutAnimation">
                    <DoubleAnimation Storyboard.TargetName="Foreground"
                        Storyboard.TargetProperty="Opacity" From="1.0"
                        To="0.0" Duration="0:0:1" />
                </Storyboard>

                <Storyboard x:Name="blinkAnimation">
                    <DoubleAnimation Storyboard.TargetName="Foreground"
                        Storyboard.TargetProperty="Opacity" From="0.0"
                        To="1.0" AutoReverse="True" RepeatBehavior="2x"
                        Duration="0:0:0.5" />
                </Storyboard>
            </Grid.Resources>
        </Grid>

        <TextBlock Height="40" HorizontalAlignment="Center" Margin="0"
            Name="textValue" Text="0" VerticalAlignment="Center"
            FontSize="28" Width="50" Foreground="Black" FontWeight="Bold"
            TextAlignment="Center"/>
    </Grid>

Game over

MainPage's gameTimer calls UpdateStatus on every second. It updates the statistics on UI (moves, empty cells, time), and calls GameEnds method when there are no more empty Cells left. GameEnds method stops the timer, starts the blink animation of all cells, sets the game state properly and displays the GameOver dialog.

GameOver's constructor checks if a new high score is made with Highscores.IsNewHighscore, and displays the text block for player's name and position in the high scores list on the control if a new high score is made. Highscore.AddNewHighscore is called when the user presses enter. AddNewHighscore inserts the score into the high scores list and saves the list to isolated storage using XmlSerializer.

    public static List<HighscoreItem> scores;

    ...

    static public void Save()
    {
        IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication();

        IsolatedStorageFileStream stream = store.CreateFile(highscoresFilename);
        StreamWriter writer = new StreamWriter(stream);
        XmlSerializer serializer = new XmlSerializer(scores.GetType());
        serializer.Serialize(writer, scores);

        writer.Close();
        stream.Close();
    }

The high score list is loaded at application startup in App.cs. The Load method is similar to Save, and the list is loaded with serializer.Deserialize.

High scores

HighScores page contains a listbox, where each item contains four columns. The ItemTemplate is defined in Highscores.xaml;

    <ListBox HorizontalAlignment="Center" Name="HighscoreList" FontSize="26" Width="460" Margin="0,160,0,0" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid Name="ListBoxItemGrid" HorizontalAlignment="Stretch" Width="{Binding Width, ElementName=HighscoreList}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="10*" />
                        <ColumnDefinition Width="55*" />
                        <ColumnDefinition Width="25*" />
                        <ColumnDefinition Width="10*" />
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Column="0" Text="{Binding index}" FontSize="28" Foreground="White" />
                    <TextBlock Grid.Column="1" Text="{Binding name}" FontSize="28" Foreground="White" />
                    <TextBlock Grid.Column="2" Text="{Binding time}" FontSize="28" Foreground="White" />
                    <TextBlock Grid.Column="3" Text="{Binding moves}" FontSize="28" Foreground="White" />
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

The high scores list is binded to the ListBox in the constructor. Any modifications to high scores list will be visible on UI after this;

    public static List<HighscoreItem> scores;
    ...

    public Highscores()
    {
        InitializeComponent();
        HighscoreList.ItemsSource = scores;
    }

Tombstoning

The MainPage listens for the deactivation event, and writes the game state into isolated storage using BinaryWriter when the application is deactivated. The game state is restored when the main page is created, if the state file exists.

    public MainPage()
    {
        ...
        PhoneApplicationService.Current.Deactivated +=  new EventHandler<DeactivatedEventArgs>(App_Deactivated);
        RestoreState();
    }
    ...
    void App_Deactivated(object sender, DeactivatedEventArgs e)
    {
        StoreState();
    }

Class diagram

The application consists of the following classes. Not all classes appear in this diagram but these are the ones important for understanding the structure of the application.

Class diagram

  • MainPage: Main page of the application, the game view.
  • Highscores: Highscores (or top times) page. Contains a list of 20 best times/moves.
  • Gamelogic: Game board generation, logic for the game.
  • Cell: Represents a single cell on the game board.
  • GameOver: Dialog which is shown when the game ends.
  • NumberSelection: Dialog which is shown when the player taps on a cell.
  • WaitNote: Spinning circle animation which is displayed while generating a new puzzle.

Sounds

All the sounds used in the application have been downloaded from The Freesound Project. The following samples were used in the application:

yawfle:

  • 7040__yawfle__050816_chair_04.wav
  • 7043__yawfle__050816_chair_07.wav

jobro:

  • 60443__jobro__tada1.wav
Something went wrong with that request. Please try again.