-
-
Save nikonthethird/4e410ac3c04ea6633043a5cb7be1d717 to your computer and use it in GitHub Desktop.
#Requires -Version 5.1 | |
Using Assembly PresentationCore | |
Using Assembly PresentationFramework | |
Using Assembly WindowsBase | |
Using Namespace System.Collections.Generic | |
Using Namespace System.Collections.ObjectModel | |
Using Namespace System.Globalization | |
Using Namespace System.Reflection | |
Using Namespace System.Windows | |
Using Namespace System.Windows.Controls | |
Using Namespace System.Windows.Data | |
Using Namespace System.Windows.Input | |
Using Namespace System.Windows.Markup | |
Using Namespace System.Windows.Media | |
Using Namespace System.Windows.Threading | |
Set-StrictMode -Version Latest | |
# PowerShell 5 classes are created when the script is parsed, so before the import statements above | |
# are executed. This means that classes cannot contain any types not loaded by default. | |
# Therefore, classes have been written with certain parts commented out: | |
# <# Class TestClass : BaseClass #> { | |
# Static [Type] $StaticProperty | |
# <# Constructor #> Function TestClass() { } | |
# <# [ReturnType] #> Function MemberFunction() { } | |
# } | |
# Once the assemblies are loaded, the part below checks if $PSCommandPath is set (which points to the | |
# currently executing file). If it is, the code of the script itself is read, the two regexes remove the | |
# comments from the classes, and executes the whole script using Invoke-Expression (which resets | |
# $PSCommandPath inside). | |
If (-not [String]::IsNullOrEmpty($PSCommandPath)) { | |
( | |
# Load the code of the script. | |
# Remove the class declaration comments. | |
# '<# Class A : B, C, D #> {' => 'Class A: B, C, D {' | |
# Remove constructor and member comments. | |
# '<# Static Constructor #> Function A()' => 'Static A()' | |
# '<# [A] #> Function B()' => '[A] B()' | |
(Get-Content $PSCommandPath -Encoding UTF8) ` | |
-creplace '^\s*<#\s*(Class\s+\w+(\s*:\s*\w+(\s*,\s*\w+)*)?)\s*#>', '$1' ` | |
-creplace '^\s*<#\s*((Static\s+)?Constructor|((Hidden\s*)?(Static\s*)?\[[\w\[\]]+\])?)\s*#>\s*Function', '$2$3' | |
) ` | |
-join [Environment]::NewLine ` | |
| Invoke-Expression | |
Exit | |
} | |
# Declares the possible game modes. They can be selected from the menu before restarting a game. | |
# The 'BoardWidth' defines how many fields in the X direction are shown. | |
# The 'BoardHeight' defines how many fields in the Y direction are shown. | |
# The 'MineCount' defines how many mines are randomly placed among the fields. | |
Set-Variable easyMode ([PSCustomObject] @{ | |
Name = 'Easy' | |
BoardWidth = 10 | |
BoardHeight = 10 | |
MineCount = 10 | |
}) | |
Set-Variable mediumMode ([PSCustomObject] @{ | |
Name = 'Medium' | |
BoardWidth = 16 | |
BoardHeight = 16 | |
MineCount = 40 | |
}) | |
Set-Variable hardMode ([PSCustomObject] @{ | |
Name = 'Hard' | |
BoardWidth = 30 | |
BoardHeight = 16 | |
MineCount = 99 | |
}) | |
# The view model of the main window. An instance of this class is set as the | |
# DataContext of the window, which means data binding expressions of the window | |
# will target the properties of the view model. | |
<# Class ViewModel : DependencyObject #> { | |
# 2D array that contains a map of all the fields. The first coordinate is X, the | |
# second coordinate is Y. This is used to help identify the neighbors of fields. | |
Hidden [Field[,]] $BoardMap = (New-Object 'Field[,]' 0, 0) | |
# Timer that counts the elapsed seconds of a running game. | |
Hidden [DispatcherTimer] $Timer | |
# The game title shown in the window and dialogs. | |
[String] $Title = 'PowerShell Minesweeper' | |
# The number of fields of the game board in the X direction. | |
# Contains the board width of the current game mode. | |
# The game grid columns of the window are bound to this property. | |
Static [DependencyProperty] $BoardWidthProperty = [DependencyProperty]::Register( | |
'BoardWidth', [Int32], [ViewModel] | |
) | |
# The number of fields of the game board in the Y direction. | |
# Contains the board height of the current game mode. | |
# The game grid rows of the window are bound to this property. | |
Static [DependencyProperty] $BoardHeightProperty = [DependencyProperty]::Register( | |
'BoardHeight', [Int32], [ViewModel] | |
) | |
# The current number of unmarked mines on the game board. | |
# The mine display in the top right corner of the window is bound to this property. | |
Static [DependencyProperty] $MineCountProperty = [DependencyProperty]::Register( | |
'MineCount', [Int32], [ViewModel] | |
) | |
# The number of seconds since the game started. | |
# This value is increased by the DispatcherTimer. | |
# The time display in the top right corner of the window is bound to this property. | |
Static [DependencyProperty] $ElapsedSecondsProperty = [DependencyProperty]::Register( | |
'ElapsedSeconds', [Int32], [ViewModel] | |
) | |
# Observable collection containing all the fields on the board. | |
# The game grid of the window is bound to this property. | |
# The fields in the game grid are bound to the elements of this property. | |
Static [DependencyProperty] $BoardProperty = [DependencyProperty]::Register( | |
'Board', [ObservableCollection[Field]], [ViewModel] | |
) | |
# Determines if the game is currently active. A game is active when the player has not | |
# revealed a mine and there are still empty fields hidden (i.e. the game is not over). | |
# When this property is set to $false, the timer is stopped. | |
# The visibility of the game status display in the top center of the window is bound | |
# to this property. | |
Static [DependencyProperty] $IsGameActiveProperty = [DependencyProperty]::Register( | |
'IsGameActive', [Boolean], [ViewModel], [PropertyMetadata]::new($false, { | |
Param ([ViewModel] $this, [DependencyPropertyChangedEventArgs] $e) | |
If (-not $e.NewValue) { | |
$this.Timer.Stop() | |
} | |
}) | |
) | |
# Whether the game has been won or not. This property is set when checking the winning | |
# conditions in [Field]::Reveal and is bound to the game status display in the top center | |
# of the window and determines whether to show a check mark or a cross. | |
Static [DependencyProperty] $HasGameBeenWonProperty = [DependencyProperty]::Register( | |
'HasGameBeenWon', [Boolean], [ViewModel] | |
) | |
# The width and height of a displayed field in the game board. | |
# The Width and Height properties of the fields as well as the zoom control in the top | |
# left of the window are bound to this property. | |
Static [DependencyProperty] $FieldSizeProperty = [DependencyProperty]::Register( | |
'FieldSize', [Double], [ViewModel], | |
(New-Object PropertyMetadata 40.0) | |
) | |
# The current mode of the game. Also declares that the default mode is 'Easy'. | |
# This property is set by the handlers of the mode switches in the main menu and | |
# read by handler of the new game menu item. | |
Static [DependencyProperty] $ModeProperty = [DependencyProperty]::Register( | |
'Mode', [PSCustomObject], [ViewModel], | |
(New-Object PropertyMetadata $easyMode) | |
) | |
# The constructor of the ViewModel initializes the board to an empty collection. | |
<# Constructor #> Function ViewModel() { | |
$this.SetValue([ViewModel]::BoardProperty, (New-Object 'ObservableCollection[Field]')) | |
} | |
# Starts a new game by discarding the current game state and setting up the game | |
# according to the given mode. | |
<# [Void] #> Function StartNewGame([PSCustomObject] $mode) { | |
$this.SetValue([ViewModel]::IsGameActiveProperty, $false) | |
[ObservableCollection[Field]] $board = $this.GetValue([ViewModel]::BoardProperty) | |
[Int32] $oldBoardWidth = $this.GetValue([ViewModel]::BoardWidthProperty) | |
[Int32] $oldBoardHeight = $this.GetValue([ViewModel]::BoardHeightProperty) | |
# Copy the mode values into their corresponding properties on the ViewModel. | |
$this.SetValue([ViewModel]::BoardWidthProperty, $mode.BoardWidth) | |
$this.SetValue([ViewModel]::BoardHeightProperty, $mode.BoardHeight) | |
$this.SetValue([ViewModel]::MineCountProperty, $mode.MineCount) | |
# Adjust the board. | |
# To reduce the number of views that have to be recreated, check which fields can | |
# be removed or have to be added according to the given mode and the currently active one. | |
[List[Field]]::new($board).ToArray() ` | |
| Where-Object { | |
# A field has to be removed when one of its coordinates is larger than the | |
# current mode's board dimensions. | |
$PSItem.X -cge $mode.BoardWidth -or $PSItem.Y -cge $mode.BoardHeight | |
} ` | |
| ForEach-Object { | |
Write-Debug "ViewModel::StartNewGame: Removing field at $($PSItem.X), $($PSItem.Y)" | |
# Remove all out-of-bounds fields from the board. | |
$board.Remove($PSItem) | |
} | |
ForEach($y In 0 .. ($mode.BoardHeight - 1)) { | |
ForEach($x In 0 .. ($mode.BoardWidth - 1)) { | |
# Add all fields that have coordinates larger than the previous mode's dimensions. | |
If ($x -cge $oldBoardWidth -or $y -cge $oldBoardHeight) { | |
Write-Debug "ViewModel::StartNewGame: Adding field at $x, $y" | |
$board.Add((New-Object Field $this, $x, $y)) | |
} | |
} | |
} | |
# Create new board map and reset the fields to their default state (not revealed, etc.). | |
# The board map is required in the next section. | |
$this.BoardMap = New-Object 'Field[,]' $mode.BoardWidth, $mode.BoardHeight | |
ForEach ($field In $board) { | |
$field.Reset() | |
$this.BoardMap[$field.X, $field.Y] = $field | |
} | |
# Update the neighbors of each field. | |
# This is done by going through every field, reading its coordinates and reading its | |
# neighboring fields from the board map. | |
ForEach ($field In $board) { | |
[List[Field]] $neighbors = New-Object 'List[Field]' | |
ForEach ($dy In -1 .. 1) { | |
ForEach ($dx In -1 .. 1) { | |
[Boolean] $xInBounds = $field.X + $dx -cge 0 -and $field.X + $dx -clt $mode.BoardWidth | |
[Boolean] $yInBounds = $field.Y + $dy -cge 0 -and $field.Y + $dy -clt $mode.BoardHeight | |
If ($xInBounds -and $yInBounds -and ($dx -cne 0 -or $dy -cne 0)) { | |
$neighbors.Add(($this.BoardMap[($field.X + $dx), ($field.Y + $dy)])) | |
} | |
} | |
} | |
# Store the field's neighbors inside its neighbors property. | |
$field.SetValue([Field]::NeighborsProperty, $neighbors.ToArray()) | |
} | |
# Randomly distribute mines on the board. | |
$this.PlaceMines($mode.MineCount) | |
# Reset the game status. | |
$this.SetValue([ViewModel]::ElapsedSecondsProperty, 0) | |
$this.SetValue([ViewModel]::HasGameBeenWonProperty, $false) | |
$this.SetValue([ViewModel]::IsGameActiveProperty, $true) | |
} | |
# Randomly place the given number of mines across the board. | |
<# [Void] #> Function PlaceMines([Int32] $mineCount) { | |
[Random] $random = New-Object Random | |
[Int32] $boardWidth = $this.GetValue([ViewModel]::BoardWidthProperty) | |
[Int32] $boardHeight = $this.GetValue([ViewModel]::BoardHeightProperty) | |
1 .. $mineCount ` | |
| ForEach-Object { | |
[Boolean] $minePlaced = $false | |
Do { | |
# Randomly select a field from the board. | |
[Field] $field = $this.BoardMap[$random.Next(0, $boardWidth), $random.Next(0, $boardHeight)] | |
# Declare the field a mine if it is not one already. | |
If (-not $field.GetValue([Field]::IsMineProperty)) { | |
Write-Debug "ViewModel::PlaceMines: Placing mine at $($field.X), $($field.Y)." | |
$field.SetValue([Field]::IsMineProperty, $true) | |
$minePlaced = $true | |
} | |
} Until ($minePlaced) | |
} | |
} | |
# ScriptBlock that is executed when the player clicks the 'Start New Game' menu item. | |
[ScriptBlock] $OnStartNewGame = { | |
Param ([ViewModel] $this) | |
# Check if a game is currently underway and ask the player if it should be aborted. | |
If ($this.Timer.IsEnabled) { | |
[MessageBoxResult] $questionResult = [MessageBox]::Show( | |
'Do you really want to start a new game?', | |
$this.Title, | |
[MessageBoxButton]::YesNo, | |
[MessageBoxImage]::Question | |
) | |
If ($questionResult -cne [MessageBoxResult]::Yes) { | |
# The player does not want to abort the game. | |
Return | |
} | |
} | |
$this.StartNewGame($this.GetValue([ViewModel]::ModeProperty)) | |
} | |
# ScriptBlock that is executed when the player clicks the 'Easy' menu item. | |
[ScriptBlock] $OnSetModeEasy = { | |
Param ([ViewModel] $this) | |
$this.SetValue([ViewModel]::ModeProperty, $easyMode) | |
} | |
# ScriptBlock that is executed when the player clicks the 'Medium' menu item. | |
[ScriptBlock] $OnSetModeMedium = { | |
Param ([ViewModel] $this) | |
$this.SetValue([ViewModel]::ModeProperty, $mediumMode) | |
} | |
# ScriptBlock that is executed when the player clicks the 'Hard' menu item. | |
[ScriptBlock] $OnSetModeHard = { | |
Param ([ViewModel] $this) | |
$this.SetValue([ViewModel]::ModeProperty, $hardMode) | |
} | |
} | |
# The view model of a field in the game grid. Contained in the [ViewModel]::Board property. | |
<# Class Field : DependencyObject #> { | |
# Reference to the parent view model of the window. | |
[ViewModel] $ViewModel | |
# The X coordinate of the field on the game board. | |
# The Grid.Column property of the field on the game grid is bound to this property. | |
[Int32] $X | |
# The Y coordinate of the field on the game board. | |
# The Grid.Row property of the field on the game grid is bound to this property. | |
[Int32] $Y | |
# Determines whether this field contains a mine. | |
# The Text and Background properties of the field on the window are | |
# bound to this property. | |
Static [DependencyProperty] $IsMineProperty = [DependencyProperty]::Register( | |
'IsMine', [Boolean], [Field] | |
) | |
# Determines whether the contents of the field should be shown to the player. | |
# The Text and Background properties of the field on the window are | |
# bound to this property. Furthermore, when this property is set to $true, | |
# the Timer will be started if it was not active already. | |
Static [DependencyProperty] $IsRevealedProperty = [DependencyProperty]::Register( | |
'IsRevealed', [Boolean], [Field], [PropertyMetadata]::new($false, { | |
Param ([Field] $this, [DependencyPropertyChangedEventArgs] $e) | |
If ($e.NewValue) { | |
$this.ViewModel.Timer.Start() | |
} | |
}) | |
) | |
# Contains the neighboring fields of this field. | |
# The Text property of the field on the window is bound to this property. | |
Static [DependencyProperty] $NeighborsProperty = [DependencyProperty]::Register( | |
'Neighbors', [Field[]], [Field] | |
) | |
# When the player clicks on the game fields on the window and holds the mouse button down, | |
# the field under the cursor (if only one mouse button is down) or the neighboring fields | |
# (if both mouse buttons are down) are highlighted. This property determines whether the | |
# current field should be shown highlighted if it has not been revealed yet. | |
# The Background property of the field on the window is bound to this property. | |
Static [DependencyProperty] $IsHighlightedProperty = [DependencyProperty]::Register( | |
'IsHighlighted', [Boolean], [Field] | |
) | |
# Contains the currently pressed mouse buttons and whether a click has already been | |
# performed. Once both buttons have been down and one has been released, releasing the | |
# other mouse button must not perform another click. Therefore, the ClickAllowed flag | |
# is used to track this. | |
Static [DependencyProperty] $MouseStateProperty = [DependencyProperty]::Register( | |
'MouseState', [MouseState], [Field], | |
[PropertyMetadata]::new([MouseState]::NoButtonDown, { | |
Param ([Field] $this, [DependencyPropertyChangedEventArgs] $e) | |
# Highlight this field if only one button is down. | |
$this.SetValue( | |
[Field]::IsHighlightedProperty, | |
($e.NewValue -band [MouseState]::BothButtonsDown) -ceq [MouseState]::LeftButtonDown -xor ` | |
($e.NewValue -band [MouseState]::BothButtonsDown) -ceq [MouseState]::RightButtonDown | |
) | |
# Highlight the neighbors if all buttons are down. | |
$this.GetValue([Field]::NeighborsProperty) ` | |
| ForEach-Object { | |
$PSItem.SetValue( | |
[Field]::IsHighlightedProperty, | |
($e.NewValue -band [MouseState]::BothButtonsDown) -ceq [MouseState]::BothButtonsDown | |
) | |
} | |
}) | |
) | |
# Contains the current mark of the field. Normally a field is not marked, but if the player | |
# right-clicks once, it will be marked as a mine with [FieldMark]::MineMark, if the player | |
# right-clicks again, it will be marked with a question mark with [FieldMark]::QuestionMark | |
# and another right-click removed the mark again. | |
# If a mark is placed on a field, the timer will be started if it was not already. | |
Static [DependencyProperty] $MarkProperty = [DependencyProperty]::Register( | |
'Mark', [FieldMark], [Field], [PropertyMetadata]::new([FieldMark]::NoMark, { | |
Param ([Field] $this, [DependencyPropertyChangedEventArgs] $e) | |
If ($e.NewValue -cne [FieldMark]::NoMark) { | |
$this.ViewModel.Timer.Start() | |
} | |
}) | |
) | |
<# Constructor #> Function Field([ViewModel] $viewModel, [Int32] $x, [Int32] $y) { | |
$this.ViewModel = $viewModel | |
$this.X = $x | |
$this.Y = $y | |
} | |
# Reset the field state by making it empty, hidden and unmarked. | |
<# [Void] #> Function Reset() { | |
$this.SetValue([Field]::IsMineProperty, $false) | |
$this.SetValue([Field]::IsRevealedProperty, $false) | |
$this.SetValue([Field]::MarkProperty, [FieldMark]::NoMark) | |
} | |
# Reveal this field. The skipChecks parameter is used when it is not required | |
# to check for game-ending conditions (for example while revealing neighbors). | |
# This function is executed by the left-click handler. | |
<# [Void] #> Function Reveal([Boolean] $skipChecks) { | |
If ( | |
-not $this.ViewModel.GetValue([ViewModel]::IsGameActiveProperty) -or ` | |
$this.GetValue([Field]::IsRevealedProperty) -or ` | |
$this.GetValue([Field]::MarkProperty) -cne [FieldMark]::NoMark | |
) { Return } | |
#If the game just started and the click was on a mine, move the mine. | |
[Boolean] $isMine = $this.GetValue([Field]::IsMineProperty) | |
If (-not $skipChecks -and -not $this.ViewModel.Timer.IsEnabled -and $isMine) { | |
Write-Debug 'Field::Reveal: First click was on mine.' | |
# Place one more mine on the board without setting this field as empty, | |
# to ensure that this field is not marked as a mine again. | |
$this.ViewModel.PlaceMines(1) | |
# Declare this field empty. | |
$isMine = $false | |
$this.SetValue([Field]::IsMineProperty, $false) | |
} | |
# Reveal the field. | |
$this.SetValue([Field]::IsRevealedProperty, $true) | |
# Reveal neighbors if the field is empty. | |
[Field[]] $neighbors = $this.GetValue([Field]::NeighborsProperty) | |
[Int32] $mineCount = @( | |
$neighbors ` | |
| Where-Object { | |
$PSItem.GetValue([Field]::IsMineProperty) | |
} | |
).Count | |
If (-not $isMine -and $mineCount -ceq 0) { | |
$neighbors ` | |
| ForEach-Object { | |
# Reveal the neighbors without checking the state. | |
$PSItem.Reveal($true) | |
} | |
} | |
# End the game if a mine was revealed. | |
If (-not $skipChecks -and $isMine) { | |
Write-Debug 'Field::Reveal: Revealed a mine. Game over.' | |
$this.ViewModel.SetValue([ViewModel]::IsGameActiveProperty, $false) | |
Return | |
} | |
#End the game if only the mines are remaining. | |
If (-not $skipChecks) { | |
[ObservableCollection[Field]] $board = $this.ViewModel.GetValue([ViewModel]::BoardProperty) | |
# Count the number of fields that are empty and have not been revealed yet. | |
[Int32] $hiddenEmptyFieldCount = @( | |
$board ` | |
| Where-Object { | |
-not $PSItem.GetValue([Field]::IsRevealedProperty) -and ` | |
-not $PSItem.GetValue([Field]::IsMineProperty) | |
} | |
).Count | |
If ($hiddenEmptyFieldCount -ceq 0) { | |
# There are no more empty fields that are not revealed remaining. | |
# This means there are only mines left, the player has won. | |
$board ` | |
| Where-Object { | |
-not $PSItem.GetValue([Field]::IsRevealedProperty) | |
} ` | |
| ForEach-Object { | |
$PSItem.SetValue([Field]::MarkProperty, [FieldMark]::MineMark) | |
} | |
Write-Debug 'Field::Reveal: Only mines remaining. Game won.' | |
$this.ViewModel.SetValue([ViewModel]::HasGameBeenWonProperty, $true) | |
$this.ViewModel.SetValue([ViewModel]::IsGameActiveProperty, $false) | |
$this.ViewModel.SetValue([ViewModel]::MineCountProperty, 0) | |
} | |
} | |
} | |
# Switches the mark on the current field if it is hidden. | |
# No mark => Mine mark => Question mark => No mark => ... | |
# This function is executed by the right-click handler. | |
<# [Void] #> Function ToggleMark() { | |
If ( | |
-not $this.ViewModel.GetValue([ViewModel]::IsGameActiveProperty) -or ` | |
$this.GetValue([Field]::IsRevealedProperty) | |
) { Return } | |
# Switch to the next mark. | |
[FieldMark] $newMark = ($this.GetValue([Field]::MarkProperty) + 1) % [FieldMark]::MaximumMark | |
$this.SetValue([Field]::MarkProperty, $newMark) | |
# When the player marks a mine, the mine count display has to be adjusted. | |
[Int32] $delta = | |
If ($newMark -ceq [FieldMark]::MineMark) { | |
# Marking a mine decreases the mine count by one. | |
-1 | |
} ElseIf ($newMark -ceq [FieldMark]::QuestionMark) { | |
# Removing the mine mark (by switching to a question mark) increases the mine count again. | |
1 | |
} Else { | |
# Switching to any other mark does not affect the mine count. | |
0 | |
} | |
$this.ViewModel.SetValue( | |
[ViewModel]::MineCountProperty, | |
$this.ViewModel.GetValue([ViewModel]::MineCountProperty) + $delta | |
) | |
} | |
# When clicking with both mouse buttons on an empty field, its neighbors will be revealed | |
# if all mines in the neighbors have been marked. | |
# This function is executed by the both-click handler. | |
<# [Void] #> Function RevealOthers() { | |
If ( | |
-not $this.ViewModel.GetValue([ViewModel]::IsGameActiveProperty) -or ` | |
-not $this.GetValue([Field]::IsRevealedProperty) | |
) { Return } | |
# Count the number of mines and mine marks in the neighbors. | |
[Field[]] $neighbors = $this.GetValue([Field]::NeighborsProperty) | |
[Int32] $mineCount = 0 | |
[Int32] $mineMarkCount = 0 | |
$neighbors ` | |
| ForEach-Object { | |
If ($PSItem.GetValue([Field]::IsMineProperty)) { | |
$mineCount++ | |
} | |
If ($PSItem.GetValue([Field]::MarkProperty) -ceq [FieldMark]::MineMark) { | |
$mineMarkCount++ | |
} | |
} | |
Write-Debug "Field::RevealOthers: Mines = $mineCount, Flags = $mineMarkCount" | |
If ($mineCount -ceq $mineMarkCount) { | |
# The mine and mine mark count agree, so we can reveal the neighboring fields. | |
$neighbors ` | |
| Where-Object { | |
$PSItem.GetValue([Field]::MarkProperty) -ceq [FieldMark]::NoMark | |
} ` | |
| ForEach-Object { | |
$PSItem.Reveal($false) | |
} | |
} | |
} | |
# ScriptBlock that is executed when the user presses the left mouse button over a field. | |
[ScriptBlock] $OnMouseLeftButtonDown = { | |
Param ([Field] $this) | |
[MouseState] $mouseState = $this.GetValue([Field]::MouseStateProperty) | |
# Set the flag that indicates whether a click is allowed only when the right button was up. | |
[MouseState] $clickAllowed = | |
If ($mouseState -ceq [MouseState]::NoButtonDown) { | |
[MouseState]::ClickAllowed | |
} Else { | |
[MouseState]::NoButtonDown | |
} | |
$this.SetValue( | |
[Field]::MouseStateProperty, | |
$mouseState -bor [MouseState]::LeftButtonDown -bor $clickAllowed | |
) | |
} | |
# ScriptBlock that is executed when the user releases the left mouse button over a field. | |
[ScriptBlock] $OnMouseLeftButtonUp = { | |
Param ([Field] $this) | |
[MouseState] $mouseState = $this.GetValue([Field]::MouseStateProperty) | |
# Ensure that a click is currently allowed. | |
If ($mouseState -band [MouseState]::ClickAllowed) { | |
If (($mouseState -band [MouseState]::BothButtonsDown) -ceq [MouseState]::BothButtonsDown) { | |
# If it is, and both buttons have been pressed, try to reveal the field's neighbors. | |
Write-Debug "Field::OnMouseLeftButtonUp: Click L+R on $($this.X), $($this.Y)." | |
$this.RevealOthers() | |
} ElseIf ($mouseState -band [MouseState]::LeftButtonDown) { | |
# If it is, but only the left button was pressed, try to reveal the field. | |
Write-Debug "Field::OnMouseLeftButtonUp: Click L on $($this.X), $($this.Y)." | |
$this.Reveal($false) | |
} | |
} | |
# Remove the left mouse button flag since it was just released and also the click allowed flag | |
# since a click was just performed if it was allowed. | |
$this.SetValue( | |
[Field]::MouseStateProperty, | |
$mouseState -band -bnot ([MouseState]::LeftButtonDown -bor [MouseState]::ClickAllowed) | |
) | |
} | |
# ScriptBlock that is executed when the user presses the right mouse button over a field. | |
[ScriptBlock] $OnMouseRightButtonDown = { | |
Param ([Field] $this) | |
[MouseState] $mouseState = $this.GetValue([Field]::MouseStateProperty) | |
# Set the flag that indicates whether a click is allowed only when the left button was up. | |
[MouseState] $clickAllowed = | |
If ($mouseState -ceq [MouseState]::NoButtonDown) { | |
[MouseState]::ClickAllowed | |
} Else { | |
[MouseState]::NoButtonDown | |
} | |
$this.SetValue( | |
[Field]::MouseStateProperty, | |
$mouseState -bor [MouseState]::RightButtonDown -bor $clickAllowed | |
) | |
} | |
# ScriptBlock that is executed when the user releases the right mouse button over a field. | |
[ScriptBlock] $OnMouseRightButtonUp = { | |
Param ([Field] $this) | |
[MouseState] $mouseState = $this.GetValue([Field]::MouseStateProperty) | |
# Ensure that a click is currently allowed. | |
If ($mouseState -band [MouseState]::ClickAllowed) { | |
If (($mouseState -band [MouseState]::BothButtonsDown) -ceq [MouseState]::BothButtonsDown) { | |
# If it is, and both buttons have been pressed, try the reveal the field's neighbors. | |
Write-Debug "Field::OnMouseRightButtonUp: Click L+R on $($this.X), $($this.Y)." | |
$this.RevealOthers() | |
} ElseIf ($mouseState -band [MouseState]::RightButtonDown) { | |
# If it is, but only the right button was pressed, try to toggle the mark on the field. | |
Write-Debug "Field::OnMouseRightButtonUp: Click R on $($this.X), $($this.Y)." | |
$this.ToggleMark() | |
} | |
} | |
# Remove the right mouse button flag since it was just released and also the click allowed flag | |
# since a click was just performed if it was allowed. | |
$this.SetValue( | |
[Field]::MouseStateProperty, | |
$mouseState -band -bnot ([MouseState]::RightButtonDown -bor [MouseState]::ClickAllowed) | |
) | |
} | |
# ScriptBlock that is executed when the mouse pointer has entered the current field. | |
[ScriptBlock] $OnMouseEnter = { | |
Param ([Field] $this, [Border] $sender, [MouseEventArgs] $e) | |
# If the mouse has entered while the left button was down, set the left button flag. | |
[MouseState] $leftState = | |
If ($e.LeftButton -ceq [MouseButtonState]::Pressed) { | |
[MouseState]::LeftButtonDown | |
} Else { | |
[MouseState]::NoButtonDown | |
} | |
# If the mouse has entered while the right button was down, set the right button flag. | |
[MouseState] $rightState = | |
If ($e.RightButton -ceq [MouseButtonState]::Pressed) { | |
[MouseState]::RightButtonDown | |
} Else { | |
[MouseState]::NoButtonDown | |
} | |
# Update the mouse state of the current field. | |
# Note that the click allowed flag is not set, since it should not be possible to drag | |
# the mouse across the field and still click, because this is how the player expects to | |
# abort a click. | |
$this.SetValue( | |
[Field]::MouseStateProperty, | |
$leftState -bor $rightState | |
) | |
} | |
# ScriptBlock that is executed when the mouse pointer has left the current field. | |
[ScriptBlock] $OnMouseLeave = { | |
Param ([Field] $this) | |
# Reset the mouse state to indicate no button is pressed. | |
$this.SetValue([Field]::MouseStateProperty, [MouseState]::NoButtonDown) | |
} | |
} | |
# The main window declared as XAML. | |
[Window] $window = [XamlReader]::Parse((@' | |
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
xmlns:ps="clr-namespace:;assembly=" | |
Title="{Binding Title}" | |
SizeToContent="WidthAndHeight" | |
ResizeMode="NoResize" | |
> | |
<Window.Resources> | |
<!-- An instance of the ScriptBlockConverter defined below. It is used to | |
convert between types using a ScriptBlock. --> | |
<ps:ScriptBlockConverter x:Key="ScriptBlock" /> | |
<!-- Style that changes the color of the mine number in a field. --> | |
<Style x:Key="FieldTextStyle" TargetType="{x:Type TextBlock}"> | |
<Style.Triggers> | |
<Trigger Property="Text" Value="0"> | |
<Setter Property="Visibility" Value="Hidden" /> | |
</Trigger> | |
<Trigger Property="Text" Value="1"> | |
<Setter Property="Foreground" Value="RoyalBlue" /> | |
</Trigger> | |
<Trigger Property="Text" Value="2"> | |
<Setter Property="Foreground" Value="ForestGreen" /> | |
</Trigger> | |
<Trigger Property="Text" Value="3"> | |
<Setter Property="Foreground" Value="Firebrick" /> | |
</Trigger> | |
<Trigger Property="Text" Value="4"> | |
<Setter Property="Foreground" Value="MediumBlue" /> | |
</Trigger> | |
<Trigger Property="Text" Value="5"> | |
<Setter Property="Foreground" Value="Brown" /> | |
</Trigger> | |
<Trigger Property="Text" Value="6"> | |
<Setter Property="Foreground" Value="DarkCyan" /> | |
</Trigger> | |
<Trigger Property="Text" Value="7"> | |
<Setter Property="Foreground" Value="DarkSlateGray" /> | |
</Trigger> | |
<Trigger Property="Text" Value="8"> | |
<Setter Property="Foreground" Value="DimGray" /> | |
</Trigger> | |
</Style.Triggers> | |
</Style> | |
<!-- Brush used as the background of a field when a mine has been | |
revealed by the player (i.e. BOOM!). --> | |
<RadialGradientBrush x:Key="ExplodedMineBrush"> | |
<GradientStop Color="Crimson" Offset="0.5" /> | |
<GradientStop Color="Transparent" Offset="1.0" /> | |
</RadialGradientBrush> | |
<!-- Brush used as the background of a field when a mine has been | |
marked as such by the player. --> | |
<RadialGradientBrush x:Key="DefusedMineBrush"> | |
<GradientStop Color="LimeGreen" Offset="0.5" /> | |
<GradientStop Color="LightGray" Offset="1.0" /> | |
</RadialGradientBrush> | |
<!-- Brush used when the player marked an empty field as a mine. --> | |
<RadialGradientBrush x:Key="EmptyMineMarkBrush"> | |
<GradientStop Color="LightPink" Offset="0.5" /> | |
<GradientStop Color="LightGray" Offset="1.0" /> | |
</RadialGradientBrush> | |
<!-- Style applied to the controls and displays at the top | |
of the window. It makes them appear black and round. --> | |
<Style x:Key="RoundedBorderStyle" TargetType="{x:Type Border}"> | |
<Setter Property="Padding" Value="2 2 5 2" /> | |
<Setter Property="Background" Value="Black" /> | |
<Setter Property="Control.Foreground" Value="LightGray" /> | |
<Setter Property="BorderBrush" Value="Black" /> | |
<Setter Property="BorderThickness" Value="2" /> | |
<Setter Property="CornerRadius" Value="{ | |
Binding ActualHeight, | |
RelativeSource={RelativeSource Self}, | |
Converter={StaticResource ScriptBlock}, | |
ConverterParameter= | |
Param ([Double] $height) | |
$height / 2.0 | |
}" /> | |
<Setter Property="TextBlock.FontSize" Value="20" /> | |
</Style> | |
</Window.Resources> | |
<Window.CommandBindings> | |
<!-- Binding for the "Start New Game" menu item. --> | |
<CommandBinding Command="New" Executed="{ps:RunScriptBlock OnStartNewGame}" /> | |
</Window.CommandBindings> | |
<DockPanel> | |
<Menu DockPanel.Dock="Top"> | |
<MenuItem Header="_Game"> | |
<!-- Clicking this menu item will abort the current game and start a new one. --> | |
<MenuItem Header="Start _New Game" Command="New" /> | |
<Separator /> | |
<!-- Clicking this menu item will set the game mode to "Easy". --> | |
<MenuItem Header="_Easy" | |
Click="{ps:RunScriptBlock OnSetModeEasy}" | |
IsChecked="{ | |
Binding Mode, | |
Converter={StaticResource ScriptBlock}, | |
ConverterParameter=' | |
Param ([PSCustomObject] $mode) | |
$mode.Name -ceq "Easy" | |
' | |
}" | |
/> | |
<!-- Clicking this menu item will set the game mode to "Medium". --> | |
<MenuItem Header="_Medium" | |
Click="{ps:RunScriptBlock OnSetModeMedium}" | |
IsChecked="{ | |
Binding Mode, | |
Converter={StaticResource ScriptBlock}, | |
ConverterParameter=' | |
Param ([PSCustomObject] $mode) | |
$mode.Name -ceq "Medium" | |
' | |
}" | |
/> | |
<!-- Clicking this menu item will set the game mode to "Hard". --> | |
<MenuItem Header="_Hard" | |
Click="{ps:RunScriptBlock OnSetModeHard}" | |
IsChecked="{ | |
Binding Path=Mode, | |
Converter={StaticResource ScriptBlock}, | |
ConverterParameter=' | |
Param ([PSCustomObject] $mode) | |
$mode.Name -ceq "Hard" | |
' | |
}" | |
/> | |
</MenuItem> | |
</Menu> | |
<!-- Contains the zoom control, game status, mine and time display. --> | |
<DockPanel DockPanel.Dock="Top" Margin="5"> | |
<!-- Contains the slider used to zoom in or out. --> | |
<Border DockPanel.Dock="Left" Style="{StaticResource RoundedBorderStyle}"> | |
<StackPanel Orientation="Horizontal"> | |
<TextBlock FontFamily="Consolas" Margin="0 0 5 0">🔍</TextBlock> | |
<Slider Value="{Binding FieldSize}" Width="50" Minimum="30" Maximum="60" | |
IsSnapToTickEnabled="True" TickFrequency="10" VerticalAlignment="Center" | |
/> | |
</StackPanel> | |
</Border> | |
<!-- Shows the number of elapsed seconds since game start. --> | |
<Border DockPanel.Dock="Right" Style="{StaticResource RoundedBorderStyle}" Margin="10 0 0 0"> | |
<StackPanel Orientation="Horizontal"> | |
<TextBlock FontFamily="Consolas" Margin="0 0 5 0">⌚</TextBlock> | |
<TextBlock FontFamily="Consolas" Text="{Binding ElapsedSeconds, StringFormat={}{0:D4}}" /> | |
</StackPanel> | |
</Border> | |
<!-- Shows the number of unmarked mines. --> | |
<Border DockPanel.Dock="Right" Style="{StaticResource RoundedBorderStyle}" Margin="10 0 0 0"> | |
<StackPanel Orientation="Horizontal"> | |
<TextBlock FontFamily="Consolas" Margin="0 0 5 0">⛭</TextBlock> | |
<TextBlock FontFamily="Consolas" Text="{ | |
Binding MineCount, | |
Converter={StaticResource ScriptBlock}, | |
ConverterParameter=' | |
Param ([Int32] $mineCount) | |
[Math]::Max($mineCount, 0).ToString("D2") | |
' | |
}" /> | |
</StackPanel> | |
</Border> | |
<!-- Displays a check mark if the game was won, or a cross if the game was lost. --> | |
<TextBlock VerticalAlignment="Center" TextAlignment="Center" | |
FontWeight="Bold" FontFamily="Consolas" FontSize="20" | |
Text="{ | |
Binding HasGameBeenWon, | |
Converter={StaticResource ScriptBlock}, | |
ConverterParameter=' | |
Param ([Boolean] $hasGameBeenWon) | |
If ($hasGameBeenWon) { "✓" } Else { "✗" } | |
' | |
}" | |
Foreground="{ | |
Binding HasGameBeenWon, | |
Converter={StaticResource ScriptBlock}, | |
ConverterParameter=' | |
Param ([Boolean] $hasGameBeenWon) | |
If ($hasGameBeenWon) { [Brushes]::Green } Else { [Brushes]::Crimson } | |
' | |
}" | |
Visibility="{ | |
Binding IsGameActive, | |
Converter={StaticResource ScriptBlock}, | |
ConverterParameter=' | |
Param ([Boolean] $isGameActive) | |
If ($isGameActive) { [Visibility]::Collapsed } Else { [Visibility]::Visible } | |
' | |
}" | |
/> | |
</DockPanel> | |
<!-- Contains the game board. --> | |
<Border BorderBrush="Black" BorderThickness="1" Margin="5"> | |
<ItemsControl ItemsSource="{Binding Board}"> | |
<ItemsControl.ItemsPanel> | |
<ItemsPanelTemplate> | |
<!-- The panel of the game board is a grid, then the individual fields can be laid out | |
using their X and Y coordinates bound to the Grid's Row and Column properties. --> | |
<Grid ps:GridEx.Columns="{Binding BoardWidth}" | |
ps:GridEx.Rows="{Binding BoardHeight}"> | |
</Grid> | |
</ItemsPanelTemplate> | |
</ItemsControl.ItemsPanel> | |
<ItemsControl.ItemContainerStyle> | |
<!-- Style that sets the Row and Column property of the field. This might come as a surprise | |
that it has to be set here and not on the first child of the template below. The reason | |
is that the immediate child of the Grid are not the DataTemplate children, they are | |
wrapped inside an extra item. <Grid><Item>Stuff from the template</Item></Grid>. So you | |
see why the Row and Column property has to go on the Item. --> | |
<Style> | |
<Setter Property="Grid.Column" Value="{Binding X}" /> | |
<Setter Property="Grid.Row" Value="{Binding Y}" /> | |
</Style> | |
</ItemsControl.ItemContainerStyle> | |
<ItemsControl.ItemTemplate> | |
<DataTemplate> | |
<!-- The view for a field. This template is bound to the [Field] class. | |
It consists of nothing but a Border with a TextBlock child. | |
All mouse actions are performed on the Border. --> | |
<Border Width="{Binding ViewModel.FieldSize}" | |
Height="{Binding ActualWidth, RelativeSource={RelativeSource Self}}" | |
MouseLeftButtonDown="{ps:RunScriptBlock OnMouseLeftButtonDown}" | |
MouseLeftButtonUp="{ps:RunScriptBlock OnMouseLeftButtonUp}" | |
MouseRightButtonDown="{ps:RunScriptBlock OnMouseRightButtonDown}" | |
MouseRightButtonUp="{ps:RunScriptBlock OnMouseRightButtonUp}" | |
MouseEnter="{ps:RunScriptBlock OnMouseEnter}" | |
MouseLeave="{ps:RunScriptBlock OnMouseLeave}" | |
BorderBrush="Black" | |
BorderThickness="1" | |
> | |
<Border.Background> | |
<MultiBinding Converter="{StaticResource ScriptBlock}" | |
ConverterParameter="{x:Static ps:ScriptBlocks.FieldBackground}" | |
> | |
<Binding Path="IsMine" /> | |
<Binding Path="IsRevealed" /> | |
<Binding Path="Mark" /> | |
<Binding Path="IsHighlighted" /> | |
<Binding Path="ViewModel.IsGameActive" /> | |
<Binding Source="{StaticResource ExplodedMineBrush}" /> | |
<Binding Source="{StaticResource DefusedMineBrush}" /> | |
<Binding Source="{StaticResource EmptyMineMarkBrush}" /> | |
</MultiBinding> | |
</Border.Background> | |
<!-- The TextBlock is wrapped inside a Viewbox so that it scales to fit the | |
available field space without having to mess around with FontSize. --> | |
<Viewbox> | |
<TextBlock Style="{StaticResource FieldTextStyle}"> | |
<TextBlock.Text> | |
<MultiBinding Converter="{StaticResource ScriptBlock}" | |
ConverterParameter="{x:Static ps:ScriptBlocks.FieldText}" | |
> | |
<Binding Path="IsMine" /> | |
<Binding Path="IsRevealed" /> | |
<Binding Path="Neighbors" /> | |
<Binding Path="Mark" /> | |
<Binding Path="ViewModel.IsGameActive" /> | |
</MultiBinding> | |
</TextBlock.Text> | |
</TextBlock> | |
</Viewbox> | |
</Border> | |
</DataTemplate> | |
</ItemsControl.ItemTemplate> | |
</ItemsControl> | |
</Border> | |
</DockPanel> | |
</Window> | |
'@ ` | |
-creplace 'clr-namespace:;assembly=', "`$0$([ViewModel].Assembly.FullName)")) | |
[ViewModel] $viewModel = New-Object ViewModel | |
$viewModel.Timer = New-Object DispatcherTimer -Property @{ | |
Interval = New-TimeSpan -Seconds 1 | |
} | |
$window.DataContext = $viewModel | |
# Executed once a second when the game is started. | |
# Increases the number of elapsed seconds. | |
$viewModel.Timer.add_Tick({ | |
If ($viewModel.GetValue([ViewModel]::IsGameActiveProperty)) { | |
[Int32] $elapsedSeconds = $viewModel.GetValue([ViewModel]::ElapsedSecondsProperty) | |
$viewModel.SetValue([ViewModel]::ElapsedSecondsProperty, $elapsedSeconds + 1) | |
} | |
}) | |
# Executed when the application window was loaded. | |
# Starts a new game with the default mode. | |
$window.add_Loaded({ | |
$viewModel.StartNewGame($viewModel.GetValue([ViewModel]::ModeProperty)) | |
}) | |
#Start the game. | |
[Application] $application = New-Object Application | |
$application.ShutdownMode = [ShutDownMode]::OnMainWindowClose | |
$application.Run($window) | Out-Null | |
# Contains ScriptBlocks used for data binding in the game window. | |
<# Class ScriptBlocks #> { | |
# Bound to the Text property of a field in the game window. | |
Static [ScriptBlock] $FieldText = { | |
Param ( | |
[Boolean] $isMine, | |
[Boolean] $isRevealed, | |
[Field[]] $neighbors, | |
[FieldMark] $mark, | |
[Boolean] $isGameActive | |
) | |
# Handle marked fields (visible when game active). | |
If ($isGameActive -and -not $isRevealed) { | |
If ($mark -ceq [FieldMark]::MineMark) { | |
Return '⛿' | |
} ElseIf ($mark -ceq [FieldMark]::QuestionMark) { | |
Return '?' | |
} | |
} | |
# Handle revealed fields (also visible when game over). | |
If (-not $isGameActive -or $isRevealed) { | |
If ($isMine) { | |
Return '⛭' | |
} | |
# Count the number of mines in the neighboring fields. | |
Return "$(@( | |
$neighbors ` | |
| Where-Object { | |
$PSItem.GetValue([Field]::IsMineProperty) | |
} | |
).Count)" | |
} | |
Return [String]::Empty | |
} | |
# Bound to the Background property of a field in the game window. | |
Static [ScriptBlock] $FieldBackground = { | |
Param ( | |
[Boolean] $isMine, | |
[Boolean] $isRevealed, | |
[FieldMark] $mark, | |
[Boolean] $isHighlighted, | |
[Boolean] $isGameActive, | |
[RadialGradientBrush] $explodedMineBrush, | |
[RadialGradientBrush] $defusedMineBrush, | |
[RadialGradientBrush] $emptyMineMarkBrush | |
) | |
# Handle revealed fields. | |
If ($isRevealed) { | |
# If a mine has been revealed, show it as exploded. | |
If ($isMine) { | |
Return $explodedMineBrush | |
} | |
Return [Brushes]::Transparent | |
} | |
# Handle game-over situation. | |
If (-not $isGameActive) { | |
# Handle fields that are marked as mines. | |
If ($mark -ceq [FieldMark]::MineMark) { | |
# If the player has successfully marked a mine as such, show it as defused. | |
If ($isMine) { | |
Return $defusedMineBrush | |
} | |
# If the player has marked an empty field as a mine, show the mistake. | |
Return $emptyMineMarkBrush | |
} | |
# If the field was not touched by the player. | |
Return [Brushes]::LightGray | |
} | |
# Handle fields that are highlighted by a mouse press. | |
If ($isHighlighted) { | |
Return [Brushes]::ForestGreen | |
} | |
# Otherwise the game is on, and the field has not been touched. | |
Return [Brushes]::PaleGreen | |
} | |
} | |
# Value converter that uses ScriptBlocks to compute the converted value. | |
<# Class ScriptBlockConverter : IValueConverter, IMultiValueConverter #> { | |
# To avoid repeatedly creating the same ScriptBlocks over and over again, | |
# stored them in a Hashtable for quick lookup. | |
Hidden Static [Hashtable] $ScriptBlockCache = (New-Object Hashtable) | |
# Create a ScriptBlock from the parameter passed into the converter. | |
<# Hidden [ScriptBlock] #> Function GetScriptBlock([Object] $parameter) { | |
# If a ScriptBlock was already provided (via {x:Static}), use it. | |
If ($parameter -is [ScriptBlock]) { | |
Return $parameter | |
} | |
[String] $parameterString = $parameter -as [String] | |
If (-not [String]::IsNullOrWhitespace($parameterString)) { | |
# Check if the ScriptBlock is already cached. | |
If ([ScriptBlockConverter]::ScriptBlockCache.ContainsKey($parameterString)) { | |
Return [ScriptBlockConverter]::ScriptBlockCache[$parameterString] | |
} | |
# Otherwise create a new ScriptBlock from the code and cache it. | |
[ScriptBlock] $scriptBlock = [ScriptBlock]::Create($parameterString) | |
[ScriptBlockConverter]::ScriptBlockCache.Add($parameterString, $scriptBlock) | |
Return $scriptBlock | |
} | |
Throw [InvalidOperationException] "Missing ScriptBlock parameter." | |
} | |
# Handles single-value conversions. | |
<# [Object] #> Function Convert([Object] $value, [Type] $targetType, [Object] $parameter, [CultureInfo] $culture) { | |
[Object] $result = $this.GetScriptBlock($parameter).InvokeReturnAsIs($value) | |
# ScriptBlocks almost always return PSObjects. Unwrap them. | |
If ($result -is [PSObject]) { | |
$result = $result.PSObject.BaseObject | |
} | |
Return $result | |
} | |
<# [Object] #> Function ConvertBack([Object] $value, [Type] $targetType, [Object] $parameter, [CultureInfo] $culture) { | |
Throw [NotSupportedException] | |
} | |
# Handles multi-value conversions. | |
<# [Object] #> Function Convert([Object[]] $values, [Type] $targetType, [Object] $parameter, [CultureInfo] $culture) { | |
[Object]$result = $this.GetScriptBlock($parameter).InvokeReturnAsIs($values) | |
# ScriptBlocks almost always return PSObjects. Unwrap them. | |
If ($result -is [PSObject]) { | |
$result = $result.PSObject.BaseObject | |
} | |
Return $result | |
} | |
<# [Object[]] #> Function ConvertBack([Object] $value, [Type[]] $targetTypes, [Object] $parameter, [CultureInfo] $culture) { | |
Throw [NotSupportedException] | |
} | |
} | |
# Markup Extension {ps:RunScriptBlock} that can be used to bind events to ScriptBlocks. | |
<# Class RunScriptBlockExtension : MarkupExtension #> { | |
Hidden Static [PropertyInfo] $SingleWorkerProperty | |
Hidden Static [MethodInfo] $AttachToRootItemMethod | |
Hidden Static [MethodInfo] $RawValueMethod | |
[PropertyPath] $Path | |
# The extension uses private API to parse a property path. | |
# This static constructor fetches the pieces using reflection. | |
<# Static Constructor #> Function RunScriptBlockExtension() { | |
# A PropertyPath uses an internal type called PropertyPathWorker to do the parsing. | |
# The worker is exposed through the internal property SingleWorker. | |
[RunScriptBlockExtension]::SingleWorkerProperty = [PropertyPath].GetProperty( | |
'SingleWorker', [BindingFlags]::NonPublic -bor [BindingFlags]::Instance | |
) | |
# The worker has a method AttachToRootItem, which is used to give the worker the root | |
# item for the path to resolve. | |
[RunScriptBlockExtension]::AttachToRootItemMethod = | |
[RunScriptBlockExtension]::SingleWorkerProperty.PropertyType.GetMethod( | |
'AttachToRootItem', [BindingFlags]::NonPublic -bor [BindingFlags]::Instance | |
) | |
# The worker has another method RawValue, which returns the item the path has been resolved | |
# to relative to the root item. | |
[RunScriptBlockExtension]::RawValueMethod = | |
[RunScriptBlockExtension]::SingleWorkerProperty.PropertyType.GetMethod( | |
'RawValue', [BindingFlags]::NonPublic -bor [BindingFlags]::Instance, $null, [Type]::EmptyTypes, @() | |
) | |
} | |
# Constructor that is invoked when the extension is created. | |
# The path given is the property path that should be resolved. | |
# E.g. {ps:RunScriptBlock A.B.C} the path is the string "A.B.C". | |
<# Constructor #> Function RunScriptBlockExtension([String] $path) { | |
$this.Path = New-Object PropertyPath $path | |
} | |
# Create an event handler for the given event. | |
<# [Object] #> Function ProvideValue([IServiceProvider] $provider) { | |
# Fetch the service that gives use information about the expected result. | |
[IProvideValueTarget] $provideValueTarget = $provider.GetService([IProvideValueTarget]) | |
# In our case we can assume the extension is only bound to event targets. | |
[EventInfo] $eventInfo = $provideValueTarget.TargetProperty | |
# Create a delegate for the expected event handler, and execute the | |
# HandleEvent method below when it occurs. | |
Return [Delegate]::CreateDelegate( | |
$eventInfo.EventHandlerType, | |
$this, | |
[RunScriptBlockExtension].GetMethod('HandleEvent') | |
) | |
} | |
# Method executed by the event handler that executes the actual ScriptBlock. | |
<# Hidden [Void] #> Function HandleEvent([Object] $sender, [RoutedEventArgs] $e) { | |
# Get the data context of the sender. This is the root element of the property path. | |
[Object] $dataContext = ($sender -as [FrameworkElement]).DataContext | |
# Fetch the path worker of our path. | |
[Object] $worker = [RunScriptBlockExtension]::SingleWorkerProperty.GetMethod.Invoke($this.Path, @()) | |
# Give the data context to the worker. | |
[RunScriptBlockExtension]::AttachToRootItemMethod.Invoke($worker, @($dataContext)) | |
# Get the resolved ScriptBlock from the worker. | |
[ScriptBlock] $scriptBlock = [RunScriptBlockExtension]::RawValueMethod.Invoke($worker, @()) | |
$scriptBlock.InvokeReturnAsIs($dataContext, $sender, $e) | |
} | |
} | |
# Declares attached properties for Grid. | |
<# Class GridEx #> { | |
# Attached property that can be used to automatically create ColumnDefinitions for | |
# a Grid using only a number. | |
# <Grid ps:GridEx.Columns="5">...</Grid> | |
Static [DependencyProperty] $ColumnsProperty = [DependencyProperty]::RegisterAttached( | |
'Columns', [Int32], [GridEx], [PropertyMetadata]::new(-1, { | |
Param ([DependencyObject] $object, [DependencyPropertyChangedEventArgs] $e) | |
[Grid] $grid = $object -as [Grid] | |
[Int32] $columns = $e.NewValue | |
If ($grid -cne $null -and $columns -cge 0) { | |
Write-Debug 'GridEx::ColumnsProperty: Clearing columns.' | |
$grid.ColumnDefinitions.Clear() | |
If ($columns -cgt 0) { | |
1 .. $columns ` | |
| ForEach-Object { | |
Write-Debug "GridEx::ColumnsProperty: Adding column $PSItem." | |
$grid.ColumnDefinitions.Add((New-Object ColumnDefinition -Property @{ | |
Width = [GridLength]::Auto | |
})) | |
} | |
} | |
} | |
}) | |
) | |
# Attached property that can be used to automatically create RowDefinitions for | |
# a Grid using only a number. | |
# <Grid ps:GridEx.Rows="5">...</Grid> | |
Static [DependencyProperty] $RowsProperty = [DependencyProperty]::RegisterAttached( | |
'Rows', [Int32], [GridEx], [PropertyMetadata]::new(-1, { | |
Param ([DependencyObject] $object, [DependencyPropertyChangedEventArgs] $e) | |
[Grid] $grid = $object -as [Grid] | |
[Int32] $rows = $e.NewValue | |
If ($grid -cne $null -and $rows -cge 0) { | |
Write-Debug 'GridEx::RowsProperty: Clearing rows.' | |
$grid.RowDefinitions.Clear() | |
If ($rows -cgt 0) { | |
1 .. $rows ` | |
| ForEach-Object { | |
Write-Debug "GridEx::RowsProperty: Adding row $PSItem." | |
$grid.RowDefinitions.Add((New-Object RowDefinition -Property @{ | |
Height = [GridLength]::Auto | |
})) | |
} | |
} | |
} | |
}) | |
) | |
<# Static [Void] #> Function SetColumns([DependencyObject] $object, [Int32] $columns) { | |
$object.SetValue([GridEx]::ColumnsProperty, $columns) | |
} | |
<# Static [Void] #> Function SetRows([DependencyObject] $object, [Int32] $rows) { | |
$object.SetValue([GridEx]::RowsProperty, $rows) | |
} | |
} | |
[Flags()] | |
Enum MouseState { | |
NoButtonDown | |
LeftButtonDown | |
RightButtonDown | |
BothButtonsDown | |
ClickAllowed | |
} | |
Enum FieldMark { | |
NoMark | |
MineMark | |
QuestionMark | |
MaximumMark | |
} |
@nikonthethird New-Object : Exception calling ".ctor" with "0" argument(s): "Cannot create more than one
System.Windows.Application instance in the same AppDomain."
At line:968 char:30* [Application] $application = New-Object Application * ``` ~~~~~~~~~~~~~~~~~~~~~~ ``` * CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvocationException * FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjec tCommand
The variable '$application' cannot be retrieved because it has not been set.
At line:969 char:1* $application.ShutdownMode = [ShutDownMode]::OnMainWindowClose * ``` + CategoryInfo : InvalidOperation: (application:String) [], RuntimeException + FullyQualifiedErrorId : VariableIsUndefined ```
The variable '$application' cannot be retrieved because it has not been set.
At line:970 char:1* $application.Run($window) | Out-Null * ``` + CategoryInfo : InvalidOperation: (application:String) [], RuntimeException + FullyQualifiedErrorId : VariableIsUndefined ```
It is not possible to create two instances of the Application
class in the same AppDomain
. So when you close the game, you have to start a new shell too, because the Application
instance cannot be reused. I have found no real fix for this problem.
Hello, your two PowerShell based games are awesome. I think about writing a blog article about it. Would like to learn more about your motivation for it and your background. Let me know if we can have a quick chat. Regards, Heiko You can find me on twitter https://www.twitter.com/HeikoBrenn or LinkedIn https://www.LinkedIn.com/in/HeikoBrenn
@nikonthethird New-Object : Exception calling ".ctor" with "0" argument(s): "Cannot create more than one
System.Windows.Application instance in the same AppDomain."
At line:968 char:30
tCommand
The variable '$application' cannot be retrieved because it has not been set.
At line:969 char:1
The variable '$application' cannot be retrieved because it has not been set.
At line:970 char:1