Building a WPF Sudoku Game: Part 5 The AI Battle: Loading and Comparing AI Plugins

  Building Sudoku using Windows Presentation Foundation and XAML, Microsoft's new declarative programming language. This is the 5th article from a series of 5 articles and focusses on loading and comparing AI Plug-ins.

Difficulty: Easy
Time Required: 1-3 hours
Cost: Free
Software: Visual C# 2005 Express Edition .NET Framework 3.0 Runtime Components Windows Vista RTM SDK Visual Studio Extensions for the .NET Framework 3.0 November 2006 CTP
Hardware:
Download: Download (note: Tasos Valsamidis has an updated version that supports Expression Blend here)

Note: This article has been updated to work and compile with the RTM version of the Windows SDK. 

Welcome to the fifth and final part of my Windows Presentation Foundation tutorial! In this tutorial we’ll be wrapping up our Sudoku game by adding support for comparing multiple plug-ins, multiple threads, new notification messages, and a cool databound graph control. First, let’s take a look at the new interface we are trying to build:

On the left, there is a list of all the installed plug-ins, in the middle, our graphing control, and on the right the details for the current plug-in. To get this working we first need a way to enumerate all the plug-ins available to the app. The simplest way of doing this is to dump all the .dll files into a directory, which I’ve called “Solvers”. It’s pretty simple to get a list of the assemblies in the folder, using the directory class:

 

string[] plugins = Directory.GetFiles("Solvers\\", "*.dll");
foreach (string p in plugins)
{
LoadSolvers(p);
}

I’ve also added a new field in the Window1 class to hold the list of loaded plug-ins, which eventually ends up as the ItemsSource for the listbox:

 

ObservableCollection<ISudokuSolver> Solvers = 
new ObservableCollection<ISudokuSolver>();

We also need some other groundwork code to accept the new plug-in folder. First, we need to define that our application can load assemblies from a folder other than its base (where the .exe is) and the system folders. We do this by defining an app.config. Select the SudokuFX project, right click and select “Add new item…” then “Application Configuration File”. This file needs to be modified to specify the subdirectory as a valid assembly location:

 

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="Solvers"/>
</assemblyBinding>
</runtime>
</configuration>

On top of that, we also need to alter our new app domain, in a similar fashion:

 

AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
ads.PrivateBinPath = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path));
PermissionSet ps = new PermissionSet(null);
ps.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
SolverDomain = AppDomain.CreateDomain("New AD", null, ads, ps);

Then we need to populate our list with the proxy object we create:

 

Type[] ts = asm.GetTypes();
foreach (Type t in ts)
{
if (Array.IndexOf(t.GetInterfaces(), typeof(ISudokuSolver)) != -1)
{
Type container = typeof(SudokuSolverContainer);
SudokuSolverContainer ssc = SolverDomain.CreateInstanceAndUnwrap(
container.Assembly.FullName, container.FullName)
as SudokuSolverContainer;
ssc.Init(t);
Solvers.Add(ssc);
}
}

Finally, a simple datatemplate needs to be added to the listbox to display the Name property of the object, but you should be a pro at that by now. I also added a custom template to give the list items checkboxes, but that’s purely cosmetic and isn’t strictly required. Next, we can bind the right panel’s datacontext to the selected item in the left listbox, this way as the selection changes the info will update:

 

<StackPanel x:Name="InfoPanel" 
DataContext="{Binding ElementName=SolverList, Path=SelectedItem}"> <TextBlock Foreground="Black" Text="Solver Info:" FontWeight="Bold" FontSize="12"/> <TextBlock FontSize="8" Text=" "/> <TextBlock Foreground="Black" Text="{Binding Path=Name}"/> <TextBlock Foreground="Black" Text="{Binding Path=Author}"/> <TextBlock FontSize="8" Text=" "/> <TextBlock Foreground="Black" Text="{Binding Path=Description}"
HorizontalAlignment="Stretch" TextWrapping="WrapWithOverflow"/> </StackPanel>

Now that that’s working, lets start building the graph control. Add a new user control to the project, but this time alter it to derive from ListBox. Wait? That control’s just a listbox? Yup, and not only that, you can throw almost any type of object in it. How does it know the height of the bars then? The height of each bar is relative to the other bars since the height of the control itself is finite. The answer is attached properties. Using attached properties you can dynamically add new properties to an object at runtime, the only requirement is that the object derives from DependencyObject, which in WPF means almost any object will work. In fact you’ve already used attached properties, DockPanel.Dock is an example of one. Each control in a DockPanel needs to remember where it’s docked but adding a property to each class would be redundant because being in a dockpanel is a special case. This way, we can use a property of DockPanel, but store a value on each object. Defining an attached property is very similar to declaring a dependency property:

 

public static readonly DependencyProperty BarHeightProperty = 
DependencyProperty.RegisterAttached("BarHeight", typeof(double),
typeof(DependencyObject), new PropertyMetadata(0.0));

but, instead of defining a property, we need to define static access methods:

 

public static double GetBarHeight(DependencyObject d)
{
return (double)d.GetValue(BarHeightProperty);
}

public static void SetBarHeight(DependencyObject d, double h)
{
d.SetValue(BarHeightProperty, h);
}

 

Now, we need to define the class we’re going to store in the list, this simple class just holds a reference to the solver used, how long it took to run, and whether it was successful in solving the sudoku:

 

public class SolverResult : DependencyObject
{
TimeSpan timeTaken;

public TimeSpan TimeTaken
{
get
{
return timeTaken;
}
set
{
timeTaken = value;
}
}

ISudokuSolver solver;

public ISudokuSolver Solver
{
get
{
return solver;
}
set
{
solver = value;
}
}

bool failed;

public bool Failed
{
get
{
return failed;
}
set
{
failed = value;
}
}
}

Next, we need to add some code to figure out the heights of the bars in the graph, essentially I just find the highest bar and assume that it fills the control vertically, and then set the properties on each item. This only needs to be done when the contents change so conveniently we can override the event handler:

 

protected override void OnItemsChanged(
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
long maxVal = 0;
foreach (SolverResult s in Items)
{
if (!s.Failed && s.TimeTaken.Ticks > maxVal) maxVal = s.TimeTaken.Ticks;
}

foreach (SolverResult s in Items)
{
if (s.Failed)
{
GraphControl.SetBarHeight(s, ActualHeight - 25);
}
else { double h = (double)s.TimeTaken.Ticks /
(double)maxVal * (ActualHeight - 25);
if (h > 10)
{
GraphControl.SetBarHeight(s, h);
}
else { GraphControl.SetBarHeight(s, 10); } } GraphControl.SetIndex(s, Items.IndexOf(s)); } }

The index property is another attached property I’ve defined; you’ll see where it’s used in just a bit. For now though, it’s all about the data templates:

 

<DataTemplate x:Key="BarTemplate">
<Grid x:Name="Bar" Margin="3,3,3,0" Width="50" VerticalAlignment="Bottom"
Height="{Binding Path=BarHeight}" Background="Red" >
<TextBlock x:Name="BarText" VerticalAlignment="Center"
Foreground="White" HorizontalAlignment="Center" 
Text="{Binding Path=TimeTaken}"> <TextBlock.LayoutTransform> <RotateTransform Angle="90"/> </TextBlock.LayoutTransform> </TextBlock> </Grid> <DataTemplate.Triggers> <DataTrigger Binding="{Binding Path=Failed}" Value="True"> <Setter TargetName="Bar" Property="Opacity" Value="0.5"/> <Setter TargetName="BarText" Property="Text" Value="Failed"/> </DataTrigger> </DataTemplate.Triggers> </DataTemplate>

Here we make containers to hold the bars and set their heights based on our new property. I also added some triggers to ghost out the bar if the solver failed to solve the sudoku grid.

As you can see, it works but yuck, what is this, 16-color Windows 3.1? We’ve got to make this look a little better. First, let’s throw in some kind of color scheme. Ok, but how do the colors map to the bars? Well, if you’ve read my previous article, you’ve probably already guessed: converters. We can link together our index value with our color scheme using a converter:

 

[ValueConversion(typeof(int), typeof(Color))]
public class BarColorConverter : IValueConverter
{
static Color[] BarColors = {
Colors.SteelBlue,
Colors.Green,
Colors.Firebrick,
Colors.DarkSlateGray,
Colors.Orange,
Colors.Khaki
};

public object Convert(object value, Type targetType, object parameter,

CultureInfo culture)
{
int v = (int)value;
return BarColors[v % BarColors.Length];
}

public object ConvertBack(object value, Type targetType, object parameter,

CultureInfo culture)
{
Color v = (Color)value;
return Array.IndexOf<Color>(BarColors, v);
}
}

Now, if we want to get fancy, it’s also possible to define this converter so that when you use it from XAML you can defined the color scheme similar to a gradient, but this way works too. After defining an instance of our converter in the resources section of our control, we can use it like this:

 

<Grid.Background>
<SolidColorBrush
Color="{Binding Path=Index, Converter={StaticResource BarColorConverter}}"/>
</Grid.Background>


That’s better, about a Windows Forms level of look and feel, but this isn’t Windows Forms. We need to give the bars a more complex look based on their base color. Using converters we can setup a binding that will do this for us. First, let’s define a new converter:

 

[ValueConversion(typeof(Color), typeof(Color))]
public class ColorLightnessConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null) return null;
int param = int.Parse(parameter.ToString());
Color src = (Color)value;
Color ret = new Color();
ret.A = src.A;
ret.R = (byte)Math.Max(Math.Min(src.R + param, 255), 0);
ret.G = (byte)Math.Max(Math.Min(src.G + param, 255), 0);
ret.B = (byte)Math.Max(Math.Min(src.B + param, 255), 0);
return ret;
}

public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null) return null;
int param = int.Parse(parameter.ToString());
Color src = (Color)value;
Color ret = new Color();
ret.A = src.A;
ret.R = (byte)Math.Max(Math.Min(src.R - param, 255), 0);
ret.G = (byte)Math.Max(Math.Min(src.G - param, 255), 0);
ret.B = (byte)Math.Max(Math.Min(src.B - param, 255), 0);
return ret;
}
}

This converter allows us to lighten or darken a databound color as it passes through the binding. It also makes use of the parameter functionality on converters to allow us to specify how much we alter the color from XAML. After we’ve added the converter to our resource section can use it. First, we need to move output from out other converter, the base color of the bar into somewhere we can easily access; the Tag property is a great place to store random stuff like this so let’s use it:

 

Tag="{Binding Path=Index, Converter={StaticResource BarColorConverter}}"

Now we can reference the tag and pass it through our second converter:

 

<Rectangle RadiusX="3" RadiusY="3" StrokeThickness="2" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
<Rectangle.Stroke>
<SolidColorBrush Color="{Binding ElementName=Bar, Path=Tag, 
Converter={StaticResource ColorLightnessConverter}, ConverterParameter=-64}"
/> </Rectangle.Stroke> <Rectangle.Fill> <LinearGradientBrush SpreadMethod="Repeat" MappingMode="Absolute"
StartPoint="0,0" EndPoint="1,1"> <LinearGradientBrush.Transform> <ScaleTransform ScaleX="20" ScaleY="20"/> </LinearGradientBrush.Transform> <LinearGradientBrush.GradientStops> <GradientStop
Color="{Binding ElementName=Bar, Path=Tag,
Converter={StaticResource ColorLightnessConverter},
ConverterParameter=-32}"
Offset ="0"/> <GradientStop
Color="{Binding ElementName=Bar, Path=Tag,
Converter={StaticResource ColorLightnessConverter},
ConverterParameter=-32}"
Offset ="0.499"/> <GradientStop Color="{Binding ElementName=Bar, Path=Tag}" Offset ="0.501"/> <GradientStop Color="{Binding ElementName=Bar, Path=Tag}" Offset ="1"/> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Rectangle.Fill> </Rectangle>

Here, I’ve created a gradient and border outline based on the base color by darkening it with our converter. This gradient adds the striped appearance to the graph bars so the MappingMode property is important, since it specifies that our gradient is sized relative to the screen not the area it is filling. This prevents the stripes from stretching in an ugly way as the bars change height. Once you do this though, the gradient now becomes 1 point long, hence the scale to 20 points. After adding some more glassy overlays you can see that the final effect is much better, pretty cool for doing everything in databinding eh?

Finally, we can force a special style onto our ListBoxItems, the class that wraps each item in list to turn it into a control:

 

<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="VerticalContentAlignment" Value="Bottom"/>
<Style.Triggers>
<Trigger Property="IsVisible" Value="true">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0" To ="1"
Storyboard.TargetProperty="Opacity" Duration="0:0:0.5"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>

This way bars hang at the bottom like they are supposed to and as an added bonus, we can throw in an animation to make the bars fade in as they appear.

Now that the bar graph works, how do we get it to display useful info? Well the most obvious way it to write a method like this:

 

void BenchmarkClick(object sender, RoutedEventArgs e)
{
SolverResult s = new SolverResult();
s.Solver = SolverList.SelectedItem as ISudokuSolver;
int?[,] arr = Board.GameBoard.ToArray();
BenchButton.IsEnabled = false;
long tick = DateTime.Now.Ticks;
s.Failed = !s.Solver.Solve(ref arr);
s.TimeTaken = TimeSpan.FromTicks((DateTime.Now.Ticks - tick));
Graph.Items.Add(s);
BenchButton.IsEnabled = true;
}

The works but there is a problem: solving a grid could take more than a few seconds and this method blocks, causing the UI to freeze until it returns. This is a bad thing. Your users will hate you and your more technical users will point and laugh (you don’t believe me, but I’ve seen it). How can we fix this? The answer is threads! Unfortunately, it’s not that easy, there are significant caveats when writing multithreaded code (which are way beyond the scope of this article). Essentially, for our purposes, the problem lies in that you can’t interact with the UI objects from another thread! This makes it difficult to say, re-enable the button or update the graph. Luckily, the .NET Framework comes to the rescue with the BackgroundWorker class, although this doesn’t completely solve our problem it provides an event which is fired when out background task (our other thread) is complete. Since this event handler runs in our UI thread we can easily interact with those objects. To make things a little more user-friendly I’ve also added a new hidden panel over the solver information controls that displays a “please wait” message, which we can show while our process is running. The new code looks like this:

 


BackgroundWorker solverWorker;

void BenchmarkClick(object sender, RoutedEventArgs e)
{
SolverResult s = new SolverResult();
s.Solver = SolverList.SelectedItem as ISudokuSolver;
int?[,] arr = Board.GameBoard.ToArray();
BenchButton.IsEnabled = false;
InfoPanel.Visibility = Visibility.Hidden;
WaitPanel.Visibility = Visibility.Visible;
long tick = DateTime.Now.Ticks;
solverWorker = new BackgroundWorker();
solverWorker.DoWork += new DoWorkEventHandler(delegate(object dwsender, DoWorkEventArgs dwe)
{
s.Failed = !s.Solver.Solve(ref arr);
s.TimeTaken = TimeSpan.FromTicks((DateTime.Now.Ticks - tick));
});
solverWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(
delegate(object rwcsender, RunWorkerCompletedEventArgs rwce)
{
Graph.Items.Add(s);
InfoPanel.Visibility = Visibility.Visible;
WaitPanel.Visibility = Visibility.Hidden;
BenchButton.IsEnabled = true;
});
solverWorker.RunWorkerAsync();
}

If you’re confused, the delegate(object dwsender, DoWorkEventArgs dwe){…} syntax defines an anonymous delegate, a new .NET 2.0 feature. Anonymous delegates are single-use nameless methods. This way we can define our event handlers on the fly without cluttering up our class with extra methods that aren’t meant to be called. Also, since anonymous delegates have access to the scope in which they are defined, we can avoid creating lots of temporary variables to hold the objects local to our BenchmarkClick method. On top of that, the program execution conceptually flows in a linear fashion if you ignore the definitions around the delegates, so anonymous delegates also help the function of the method remain clear. In action, this looks like this, and it allows you keep playing sudoku as the solver runs: (It does run slow on my machine though, because much CPU time is spent on the absolutely critical pulsating background featureJ)

I’ve also added similar threading code the board generation routines and the “I give up” button and a little “x” button to the graph that allows you the clear the current results. Finally, as a finishing touch let’s update the extremely dated-looking message box:


I don’t know about you, but this dinky non-xp-themed message box doesn’t exactly scream “awesome!” at me. To at least partially fix this, I’ve added a new grid the covers the entire window and is placed in front. By default it’s completely invisible, by setting to opacity to 0, and doesn’t block input events, by setting the IsHitTestVisible property to false. Then when it’s enabled, it springs into action and fades in, tinting the window black and disabling its controls:

 

<Grid IsHitTestVisible="False" IsEnabled="False" x:Name="MessageLayer" 
Opacity="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Background="#B0000000"> <Grid.Style> <Style> <Style.Triggers> <Trigger Property="Grid.IsEnabled" Value="True"> <Trigger.EnterActions> <BeginStoryboard> <Storyboard> <DoubleAnimation To="1" Storyboard.TargetProperty="Opacity" Duration="0:0:0.25"/> <BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="IsHitTestVisible" BeginTime="0:0:0.25"> <DiscreteBooleanKeyFrame KeyTime="0" Value="True"/> </BooleanAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </Trigger.EnterActions> <Trigger.ExitActions> <BeginStoryboard> <Storyboard> <DoubleAnimation To="0" Storyboard.TargetProperty="Opacity" Duration="0:0:0.25"/> <BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="IsHitTestVisible" BeginTime="0:0:0.25"> <DiscreteBooleanKeyFrame KeyTime="0" Value="False"/> </BooleanAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </Trigger.ExitActions> </Trigger> </Style.Triggers> </Style> </Grid.Style> </Grid>

Then, in the grid, I placed one of the custom styled expanders defined in our resources section and filled it with a textblock, button and glassy icon. Also I added another hidden expander, which contains a “please wait” message. After defining some new methods to make using these controls easy:

 

void ShowMessage(string m)
{
MessageText.Text = m;
MessageExpander.Visibility = Visibility.Visible;
WaitExpander.Visibility = Visibility.Hidden;
MessageLayer.IsEnabled = true;
}

void ShowWait()
{
MessageExpander.Visibility = Visibility.Hidden;
WaitExpander.Visibility = Visibility.Visible;
MessageLayer.IsEnabled = true;
}

 

and an event handler for the close button on the expander

 

void MessageClosed(object sender, RoutedEventArgs e)
{
MessageLayer.IsEnabled = false;
}

 

We just need to replace MessageBox.Show with ShowMessage to show a fake window:


Hey, as I said before, at least it’s more colorful.

At this point, with the work we’ve done so far and a few extra lines of code you can find in the download, we have a fully functional Sudoku game. I hope you’ve enjoyed this tutorial series and have some great ideas about a cool WPF app you can build! We’ve only covered the basics and this is just the tip of the iceberg when it comes to XAML and WinFX. There’s lots of cool stuff left like 3D graphics, video and multimedia, databinding to XML, loading XAML at runtime, and browser-based applications to name a few and that’s just WPF! If you’re itching to code more and want to build on the app, some missing things you might want to work on are:

  • More plug-ins! It supports plug-ins for a reason! Write a better solver that doesn’t take forever to run
  • Better message windows that you can actually move around and the ability to dock and move the UI containers
  • Separate the styles and logic more cleanly and implement skins or color schemes
  • Add sound effects or more animations
  • Re-skin ALL the controls for a cooler look and feel
  • Add an internet highscores feature or leaderboards

Remember, if anybody asks, you’re not writing a game, you’re “updating your workplace skills to match evolving technology”

原文地址:https://www.cnblogs.com/ericfine/p/876525.html