TicTacToe

    • WPF

    Es gibt 1 Antwort in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      Ein kleines Übungs-Projekt für den MVVM-Pattern.
      Richtlinie ist, Daten und Oberfläche zu trennen - nur über Databindings miteinander verbunden. Daraus folgt, es gibt auch keinen CodeBehind, wo irgendwelche Control-Events zu behandeln wären.

      Das Datenmodell
      Ist denkbar einfach: Es gibt 9 Zellen, und jede Zelle kann frei sein, oder bereits von einem Spieler besetzt.
      Und dassis auch schon alles vom Datenmodell her.
      Dazu kommt noch das abwechselnde Ziehen und die Auswertung nach jedem Zug, ob schon einer gewonnen hat, wer, oder unentschieden. Aber dassis nicht eiglich Datenmodell, sondern Business-Logik.
      Also das Viewmodel der Zelle ist zum Weinen primitiv:

      VB.NET-Quellcode

      1. Imports System.ComponentModel
      2. <DebuggerDisplay("{Player}")> _
      3. Public Class BoardCell : Inherits NotifyPropertyChanged
      4. Private _IsWin As Boolean = False
      5. Public Property IsWin() As Boolean
      6. Get
      7. Return _IsWin
      8. End Get
      9. Set(ByVal value As Boolean)
      10. ChangePropIfDifferent(value, _IsWin, "IsWin")
      11. End Set
      12. End Property
      13. Private _Player As String = ""
      14. Public Property Player() As String
      15. Get
      16. Return _Player
      17. End Get
      18. Set(ByVal value As String)
      19. ChangePropIfDifferent(value, _Player, "Player")
      20. End Set
      21. End Property
      22. End Class
      2 Properties. Dabei ist IsWin nichtmal wirklich notwendig, das ist nur ein Gimmik, mit dessen Hilfe man die spielentscheidenden Zellen markieren kann, wenn einer gewonnen hat.

      Das Mainmodel ist auch nicht wirklich viel:

      VB.NET-Quellcode

      1. Private _Players As IEnumerator(Of String) = {"O", "X"}.GetEnumeratorX
      2. Private Const _NoPlayer As String = ""
      3. Public Property BoardCells() As BoardCell() = 9.Times(Function() New BoardCell).ToArray
      4. Public Property Move As New RelayCommand(Of BoardCell)( _
      5. Sub(cell)
      6. 'actually a move is very simple: a player occupies a cell
      7. cell.Player = _Players.YieldCyclic
      8. EvaluateMove()
      9. End Sub, _
      10. Function(cell) cell.NotNull AndAlso cell.Player = _NoPlayer)
      11. Private Sub EvaluateMove()
      12. Dim winner = GetWinner()
      13. Select Case winner
      14. Case Nothing : Return 'continue game
      15. Case _NoPlayer
      16. Msg("no Winner")
      17. Case Else
      18. Msg("Player ", winner, " is Winner")
      19. End Select
      20. For Each cll In BoardCells
      21. cll.Player = _NoPlayer
      22. cll.IsWin = False
      23. Next
      24. End Sub
      25. ''' <summary>
      26. ''' returns the winner. If winner is Nothing game continues. If winner is NoPlayer game is undecided.
      27. ''' </summary>
      28. Private Function GetWinner() As String
      29. GetWinner = Nothing
      30. Dim dimension = 3
      31. '...
      die Public Properties BoardCells und Move, wobei Move ein Command ist, also etwas, mit dem das Xaml eine Methode im ViewModel aufrufen kann.
      GetWinner() stelle ich hier nicht im Einzelnen vor, die Methode ist nämlich bischen länglicher, und nicht untricky.
      Aber vlt. wollterja den BoardCell-Konstruktor angugge:

      VB.NET-Quellcode

      1. Public Sub New()
      2. If IsProvisional Then
      3. For i = 0 To 6
      4. BoardCells(i).Player = _Players.YieldCyclic
      5. Next
      6. For i = 0 To 2
      7. BoardCells(i + 3).IsWin = True
      8. Next
      9. End If
      10. End Sub
      IsProvisional ist ein Feature meines MVVM-Frameworks, was mir die Möglichkeit gibt, zur Designzeit TestDaten zu generieren, die dann bei der Entwicklung des Xamls bereits im Designer angezeigt werden:

      Der Code zeigt noch paar annere vlt. eigentümlich erscheindende Extension-Methods, etwa das IEnumerator(Of T).YieldCyclic, mit dem hier vom _Players-Enumerator immer der jeweils nächste Player abgerufen wird, und am Ende gehts von vorne los (zyklisch) ;)
      Oder die Integer.Times()-Extension, mit deren Hilfe das MainModel.BoardCells-Array per Einzeiler initialisiert wird (Code-Listing#2).

      Jo, noch bischen Xaml?

      XML-Quellcode

      1. <Window x:Class="MainWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. Title="MainWindow" Height="350" Width="345"
      5. xmlns:my="clr-namespace:MyTicTacToe"
      6. xmlns:hlp="clr-namespace:System.Windows.Controls;assembly=HelpersSmallEd"
      7. DataContext="{StaticResource MainModel}">
      8. <Window.Resources>
      9. <BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
      10. </Window.Resources>
      11. <Grid>
      12. <Rectangle Fill="Green" RadiusX="20" RadiusY="20"/>
      13. <Grid Margin="20">
      14. <ItemsControl ItemsSource="{Binding Path=BoardCells}" >
      15. <ItemsControl.ItemsPanel>
      16. <ItemsPanelTemplate>
      17. <UniformGrid/>
      18. </ItemsPanelTemplate>
      19. </ItemsControl.ItemsPanel>
      20. <ItemsControl.ItemTemplate>
      21. <DataTemplate >
      22. <Button Command="{Binding Path=Move, Source={StaticResource MainModel}}" CommandParameter="{Binding}"
      23. Margin="3" FontSize="20" >
      24. <Grid>
      25. <Ellipse Width="30" Height="30" Stroke="Red" StrokeThickness="3"
      26. Visibility="{Binding Path=IsWin, Converter={StaticResource BooleanToVisibility}}" />
      27. <TextBlock Text="{Binding Path=Player}" HorizontalAlignment="Center" VerticalAlignment="Center" />
      28. </Grid>
      29. </Button>
      30. </DataTemplate>
      31. </ItemsControl.ItemTemplate>
      32. </ItemsControl>
      33. </Grid>
      34. </Grid>
      35. </Window>
      Also es ist ein ItemsControl, und ItemsSource sind die BoardCells (9 an der Zahl). Als ItemsPanelTemplate ist ein UniformGrid gesetzt - das layoutet die Zellen so schön quadratisch.
      Dann gibts noch das ItemTemplate, wo definiert wird, wie ein einzelnes Item (wir erinnern uns: die einzelne BoardCell) präsentiert wird: nämlich als Button, und Button-Content ist die rote Ellipse (ggfs. zur Auszeichnung als Winner-Cell) und der TextBlock, der den Player zeigt.
      Das Command des Buttons ist an MainModel.Move gebunden, und übergibt als Parameter den DataContext des Buttons - was war das nochmal? - ach ja: die (geklickste) BoardCell. ;)

      Vlt. guggemer das Move-Command nochmal genauer an:

      VB.NET-Quellcode

      1. Public Property Move As New RelayCommand(Of BoardCell)( _
      2. Sub(cell)
      3. 'actually a move is very simple: a player occupies a cell
      4. cell.Player = _Players.YieldCyclic
      5. EvaluateMove()
      6. End Sub, _
      7. Function(cell) cell.NotNull AndAlso cell.Player = _NoPlayer)
      Das enthält ja eine anonyme Sub, die den Zug ausführt, und ausserdem auch eine anonyme Function, deren Boolean-Rückgabewert aussagt, ob auf diese Zelle ühaupt ein Zug ausgeführt werden kann.
      Das fügt sich wunderbar in die Spiel-Logik von TicTacToe, denn auf diese Weise wird der Button automatisch disabled nachdem die Zelle per Klick belegt wurde.

      Weiterführendes
      Ein fortgeschritteneres TicTacToe ist auch in einer umfangreichen Codesammlung enthalten, die man sich hier downloaden kann: Microsoft All-In-One Code Framework
      Genau anhand dieses TicTacToes entwickelt sich das geniale Tutorial WPF Styles and Control Templates - also die ziehen da echt vom Leder :thumbsup:

      Anlass dieses Tuts ist im Grunde dieser Thread, wo ebenfalls ein TicTacToe vorgestellt wird, jedoch ohne MVVM, sondern ausschließlich per CodeBehind gesteuert.
      Das geeht auch, und ist auch nichtmal besonders komplex.
      Man hat halt viele Buttons, sorgsam in Grid-Zellen angeordnet, und in den Klick-Events muß man einiges an den Buttons rumschrauben, dass die Züge angezeigt werden.
      Ich denke aber, mit meiner "IsWin"-Ellipse, die die Gewinnzüge markiert, beginnen beim CodeBehind-Ansatz die Probleme. Weil ist glaub nicht lustig, per Code einen Button-Content zu generieren, der einen Kreis um einen Buchstaben anzeigt.
      Dateien
      • MyTicTacToe.zip

        (163,8 kB, 296 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 12 mal editiert, zuletzt von „ErfinderDesRades“ () aus folgendem Grund: Bug in GetWinner beim Erkennen von Unentschieden

      Erweiterung: ControlTemplate für den Button und Züge zählen

      Ich fand das uncool, dass die Buttons immer so disabled aussehen, wenn sie geklickst wurden. Funktional ist das ja sehr erwünscht, aber vonne Optik her uncool. Daher habe ich dem Button ein ControlTemplate angedreht, sodaß nix mehr von "Button" erkennbar ist, und trotzdem die ja sehr feine Command-Funktionalität erhalten bleibt:

      XML-Quellcode

      1. <ItemsControl.ItemTemplate>
      2. <DataTemplate >
      3. <Button Command="{Binding Path=Move, Source={StaticResource MainModel}}" CommandParameter="{Binding}"
      4. Margin="3" FontSize="30" FocusVisualStyle="{x:Null}">
      5. <Button.Template>
      6. <ControlTemplate>
      7. <Grid d:DataContext="{x:Type my:BoardCell}">
      8. <Border Background="Bisque" CornerRadius="15" />
      9. <Ellipse Width="50" Height="50" Stroke="Red" StrokeThickness="3"
      10. Visibility="{Binding Path=IsWin, Converter={StaticResource BooleanToVisibility}}" />
      11. <TextBlock Text="{Binding Path=Player}" HorizontalAlignment="Center" VerticalAlignment="Center" />
      12. <TextBlock Text="{Binding Path=MoveNumber}" Margin="10,7" FontSize="13" FontStyle="Italic"
      13. HorizontalAlignment="Right" VerticalAlignment="Bottom" Foreground="#FF0000BB" />
      14. </Grid>
      15. </ControlTemplate>
      16. </Button.Template>
      17. </Button>
      18. </DataTemplate>
      19. </ItemsControl.ItemTemplate>
      Also statt des Buttons sieht man jetzt ein Grid mit was drin. Dem Grid ist mit d:DataContext="{x:Type my:BoardCell}" mitgeteilt, welcher Typ DataContext gilt, denn innerhalb eines ControlTemplates setzt sich die DataContext-Vererbung im Designer nicht fort.
      Ist aber sehr wichtig fürs Binding-Picking im Xaml-Editor, dass der DataContext bekannt ist.
      Ja, also in dem Grid ist eine keksfarbene Border, die nur gegebenenfalls sichtbare rote Ellipse, sowie jetzt sogar 2 Textblöcke. Im neuen Datenmodell werden die gemachten Züge nämlich mitgezählt, und ebenfalls als BoardCell-Property veröffentlicht:

      VB.NET-Quellcode

      1. Imports System.ComponentModel
      2. Imports Microsoft.VisualBasic
      3. <DebuggerDisplay("{Player}")> _
      4. Public Class BoardCell : Inherits NotifyPropertyChanged
      5. Private _IsWin As Boolean = False
      6. Public Property IsWin() As Boolean
      7. Get
      8. Return _IsWin
      9. End Get
      10. Set(ByVal value As Boolean)
      11. ChangePropIfDifferent(value, _IsWin, "IsWin")
      12. End Set
      13. End Property
      14. Private _Player As String = ""
      15. Public Property Player() As String
      16. Get
      17. Return _Player
      18. End Get
      19. Set(ByVal value As String)
      20. ChangePropIfDifferent(value, _Player, "Player")
      21. End Set
      22. End Property
      23. Private _MoveNumber As Integer? = Nothing
      24. Public Property MoveNumber() As Integer?
      25. Get
      26. Return _MoveNumber
      27. End Get
      28. Set(ByVal value As Integer?)
      29. ChangePropIfDifferent(value, _MoveNumber, "MoveNumber")
      30. End Set
      31. End Property
      32. Public Sub Reset()
      33. IsWin = False
      34. MoveNumber = Nothing
      35. Player = ""
      36. End Sub
      37. End Class
      MoveNumber ist Nullable, kann also ein Integer sein, aber auch aussagen, dasses nicht gesetzt ist - was ja in unbesetzten TicTacToe-Zellen der Fall ist.
      Bilder
      • Shots00.Png

        23,01 kB, 464×387, 404 mal angesehen
      Dateien
      • MyTicTacToe04.zip

        (157,55 kB, 286 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ () aus folgendem Grund: Bug in GetWinner() beim Erkennen von Unentschieden