-
-
Save nikonthethird/2ab6bfad9a81d5fe127fd0d1c2844b7c to your computer and use it in GitHub Desktop.
#Requires -Version 5.1 | |
Using Assembly PresentationCore | |
Using Assembly PresentationFramework | |
Using Namespace System.Collections.Generic | |
Using Namespace System.ComponentModel | |
Using Namespace System.Linq | |
Using Namespace System.Reflection | |
Using Namespace System.Text | |
Using Namespace System.Windows | |
Using Namespace System.Windows.Input | |
Using Namespace System.Windows.Markup | |
Using Namespace System.Windows.Media | |
Using Namespace System.Windows.Threading | |
Set-StrictMode -Version Latest | |
[Int32] $boardWidth = 20 | |
[Int32] $boardHeight = 15 | |
[Int32] $fieldSizePixels = 30 | |
[Int32] $stepsMilliseconds = 75 | |
Class ViewModel : INotifyPropertyChanged { | |
Hidden [PropertyChangedEventHandler] $PropertyChanged | |
[Int32] $BoardWidthPixels | |
[Int32] $BoardHeightPixels | |
[Int32] $FieldDisplaySizePixels | |
[Int32] $HalfFieldDisplaySizePixels | |
[Int32] $Score | |
[Object] $SnakeGeometry | |
[Object] $FoodCenter | |
[Boolean] $GameOverVisible | |
[Boolean] $WonVisible | |
[Void] add_PropertyChanged([PropertyChangedEventHandler] $propertyChanged) { | |
$this.PropertyChanged = [Delegate]::Combine($this.PropertyChanged, $propertyChanged) | |
} | |
[Void] remove_PropertyChanged([PropertyChangedEventHandler] $propertyChanged) { | |
$this.PropertyChanged = [Delegate]::Remove($this.PropertyChanged, $propertyChanged) | |
} | |
Hidden [Void] NotifyPropertyChanged([String] $propertyName) { | |
If ($this.PropertyChanged -cne $null) { | |
$this.PropertyChanged.Invoke($this, (New-Object PropertyChangedEventArgs $propertyName)) | |
} | |
} | |
[Void] SetScore([Int32] $score) { | |
If ($this.Score -cne $score) { | |
$this.Score = $score | |
$this.NotifyPropertyChanged('Score'); | |
} | |
} | |
[Void] SetSnakeGeometry([Object] $snakeGeometry) { | |
If ($this.SnakeGeometry -cne $snakeGeometry) { | |
$this.SnakeGeometry = $snakeGeometry | |
$this.NotifyPropertyChanged('SnakeGeometry') | |
} | |
} | |
[Void] SetFoodCenter([Object] $foodCenter) { | |
If ($this.FoodCenter -cne $foodCenter) { | |
$this.FoodCenter = $foodCenter | |
$this.NotifyPropertyChanged('FoodCenter') | |
} | |
} | |
[Void] SetGameOverVisible([Boolean] $gameOverVisible) { | |
If ($this.GameOverVisible -cne $gameOverVisible) { | |
$this.GameOverVisible = $gameOverVisible | |
$this.NotifyPropertyChanged('GameOverVisible') | |
} | |
} | |
[Void] SetWonVisible([Boolean] $wonVisible) { | |
If ($this.WonVisible -cne $wonVisible) { | |
$this.WonVisible = $wonVisible | |
$this.NotifyPropertyChanged('WonVisible') | |
} | |
} | |
} | |
Enum SnakeDirection { | |
Left | |
Right | |
Up | |
Down | |
} | |
Enum SnakeAction { | |
Nothing | |
Collision | |
FoodEaten | |
} | |
Class SnakeSegment { | |
[Int32] $Length | |
[SnakeDirection] $Direction | |
SnakeSegment([Int32] $length, [SnakeDirection] $direction) { | |
$this.Length = $length | |
$this.Direction = $direction | |
} | |
[String] GetGeometryOperation([Int32] $fieldSizePixels) { | |
[String] $directionChar = @('h', 'v')[$this.Direction -gt [SnakeDirection]::Right] | |
[Int32] $directionFactor = $this.Direction % 2 * 2 - 1 | |
Return "$directionChar $($this.Length * $fieldSizePixels * $directionFactor)" | |
} | |
} | |
Class Snake { | |
Hidden [Int32] $BoardWidth | |
Hidden [Int32] $BoardHeight | |
Hidden [Int32] $FieldSizePixels | |
[Int32] $HeadX | |
[Int32] $HeadY | |
[Int32] $TailX | |
[Int32] $TailY | |
[List[SnakeSegment]] $Segments | |
[SnakeDirection] $Direction | |
Snake([Int32] $boardWidth, [Int32] $boardHeight, [Int32] $fieldSizePixels) { | |
$this.BoardWidth = $boardWidth | |
$this.BoardHeight = $boardHeight | |
$this.FieldSizePixels = $fieldSizePixels | |
$this.Reset() | |
} | |
[Void] Reset() { | |
$this.TailX = $this.BoardWidth / 2 - 2 | |
$this.TailY = $this.BoardHeight / 2 | |
$this.HeadX = $this.TailX + 4 | |
$this.HeadY = $this.TailY | |
$this.Segments = New-Object List[SnakeSegment] | |
$this.Segments.Add((New-Object SnakeSegment 4, 'Right')) | |
$this.Direction = 'Right' | |
} | |
[String] GetGeometryString() { | |
[StringBuilder] $geometry = New-Object StringBuilder | |
$geometry.Append("m $($this.TailX * $this.FieldSizePixels + $this.FieldSizePixels / 2) $($this.TailY * $this.FieldSizePixels + $this.FieldSizePixels / 2)") | |
ForEach ($segment In $this.Segments) { | |
$geometry.Append($segment.GetGeometryOperation($this.FieldSizePixels)) | |
} | |
Return $geometry.ToString() | |
} | |
[HashSet[Tuple[Int32, Int32]]] GetPoints() { | |
[HashSet[Tuple[Int32, Int32]]] $points = New-Object 'HashSet[Tuple[Int32, Int32]]' | |
[Int32] $x = $this.TailX | |
[Int32] $y = $this.TailY | |
$points.Add((New-Object 'Tuple[Int32, Int32]' $x, $y)) | |
ForEach ($segment In $this.Segments) { | |
1 .. $segment.Length ` | |
| ForEach-Object { | |
Switch ($segment.Direction) { | |
'Left' { $x-- } | |
'Right' { $x++ } | |
'Up' { $y-- } | |
'Down' { $y++ } | |
} | |
$points.Add((New-Object 'Tuple[Int32, Int32]' $x, $y)) | |
} | |
} | |
return $points | |
} | |
[SnakeAction] Move([Food] $food) { | |
[Int32] $currentHeadX = $this.HeadX | |
[Int32] $currentHeadY = $this.HeadY | |
# Move the head. | |
Switch ($this.Direction) { | |
'Left' { $this.HeadX-- } | |
'Right' { $this.HeadX++ } | |
'Up' { $this.HeadY-- } | |
'Down' { $this.HeadY++ } | |
} | |
# Check OOB. | |
If ($this.HeadX -lt 0 -or $this.HeadX -ge $this.BoardWidth -or $this.HeadY -lt 0 -or $this.HeadY -ge $this.BoardHeight) { | |
$this.HeadX = $currentHeadX | |
$this.HeadY = $currentHeadY | |
return [SnakeAction]::Collision | |
} | |
# Check collision. | |
[HashSet[Tuple[Int32, Int32]]] $points = $this.GetPoints() | |
If ($points.Contains((New-Object 'Tuple[Int32, Int32]' $this.HeadX, $this.HeadY))) { | |
$this.HeadX = $currentHeadX | |
$this.HeadY = $currentHeadY | |
return [SnakeAction]::Collision | |
} | |
# Check food. | |
[SnakeAction] $result = @([SnakeAction]::Nothing, [SnakeAction]::FoodEaten)[ | |
$this.HeadX -ceq $food.FoodX -and $this.HeadY -ceq $food.FoodY | |
] | |
# Handle head segment. | |
[SnakeSegment] $headSegment = $this.Segments[-1] | |
If ($headSegment.Direction -ceq $this.Direction) { | |
$headSegment.Length++ | |
} Else { | |
$this.Segments.Add((New-Object SnakeSegment 1, $this.Direction)) | |
} | |
# Handle tail segment. | |
If ($result -cne 'FoodEaten') { | |
[SnakeSegment] $tailSegment = $this.Segments[0] | |
$tailSegment.Length-- | |
Switch ($tailSegment.Direction) { | |
'Left' { $this.TailX-- } | |
'Right' { $this.TailX++ } | |
'Up' { $this.TailY-- } | |
'Down' { $this.TailY++ } | |
} | |
If ($tailSegment.Length -ceq 0) { | |
$this.Segments.RemoveAt(0) | |
} | |
} | |
Return $result | |
} | |
} | |
Class Food { | |
Hidden [Int32] $FieldSizePixels | |
Hidden [Random] $Random | |
Hidden [HashSet[Tuple[Int32, Int32]]] $AllValidPoints | |
[Int32] $FoodX | |
[Int32] $FoodY | |
Food([Int32] $boardWidth, [Int32] $boardHeight, [Int32] $fieldSizePixels) { | |
$this.FieldSizePixels = $fieldSizePixels | |
$this.Random = New-Object Random | |
$this.AllValidPoints = New-Object 'HashSet[Tuple[Int32, Int32]]' | |
For ([Int32] $x = 0; $x -lt $boardWidth; $x++) { | |
For ([Int32] $y = 0; $y -lt $boardHeight; $y++) { | |
$this.AllValidPoints.Add((New-Object 'Tuple[Int32, Int32]' $x, $y)) | |
} | |
} | |
} | |
[Tuple[Int32, Int32]] GetGeometryLocation() { | |
Return New-Object 'Tuple[Int32, Int32]' ` | |
($this.FoodX * $this.FieldSizePixels + $this.FieldSizePixels / 2), | |
($this.FoodY * $this.FieldSizePixels + $this.FieldSizePixels / 2) | |
} | |
[Boolean] Move([Snake] $snake) { | |
[HashSet[Tuple[Int32, Int32]]] $availablePoints = New-Object 'HashSet[Tuple[Int32, Int32]]' $this.AllValidPoints | |
$availablePoints.ExceptWith($snake.GetPoints()) | |
If ($availablePoints.Count -ceq 0) { | |
Return $true | |
} | |
[Tuple[Int32, Int32]] $foodPoint = [Enumerable]::ElementAt($availablePoints, $this.Random.Next($availablePoints.Count)) | |
$this.FoodX = $foodPoint.Item1 | |
$this.FoodY = $foodPoint.Item2 | |
Return $false | |
} | |
} | |
[Window] $mainWindow = [XamlReader]::Parse(@' | |
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
Title="{Binding Score, StringFormat={}Snake - {0}}" | |
SizeToContent="WidthAndHeight" | |
ResizeMode="NoResize" | |
> | |
<Window.Resources> | |
<BooleanToVisibilityConverter x:Key="VisibilityConverter" /> | |
</Window.Resources> | |
<Grid Margin="5"> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="Auto" /> | |
<RowDefinition Height="*" /> | |
</Grid.RowDefinitions> | |
<DockPanel Grid.Row="0" LastChildFill="True" Margin="0 0 0 5"> | |
<TextBlock DockPanel.Dock="Left" | |
Text="{Binding Score, StringFormat={}Score: {0}}" | |
Margin="0 0 5 0" | |
/> | |
<TextBlock DockPanel.Dock="Left" | |
Text="GAME OVER" | |
FontWeight="Bold" | |
Visibility="{Binding GameOverVisible, Converter={StaticResource VisibilityConverter}}" | |
/> | |
<TextBlock DockPanel.Dock="Left" | |
Text="YOU WON" | |
FontWeight="Bold" | |
Foreground="DarkGreen" | |
Visibility="{Binding WonVisible, Converter={StaticResource VisibilityConverter}}" | |
/> | |
<TextBlock Text="Use arrow keys to move, Enter to reset." TextAlignment="Right" /> | |
</DockPanel> | |
<Border Grid.Row="1" BorderBrush="Black" BorderThickness="1"> | |
<Canvas Width="{Binding BoardWidthPixels}" Height="{Binding BoardHeightPixels}"> | |
<Path Stroke="DarkGreen" | |
StrokeThickness="{Binding FieldDisplaySizePixels}" | |
StrokeStartLineCap="Round" | |
StrokeEndLineCap="Round" | |
StrokeLineJoin="Round" | |
Data="{Binding SnakeGeometry}" | |
/> | |
<Path Fill="DarkRed"> | |
<Path.Data> | |
<EllipseGeometry Center="{Binding FoodCenter}" | |
RadiusX="{Binding HalfFieldDisplaySizePixels}" | |
RadiusY="{Binding HalfFieldDisplaySizePixels}" | |
/> | |
</Path.Data> | |
</Path> | |
</Canvas> | |
</Border> | |
</Grid> | |
</Window> | |
'@) | |
[ViewModel] $viewModel = New-Object ViewModel -Property @{ | |
BoardWidthPixels = $boardWidth * $fieldSizePixels | |
BoardHeightPixels = $boardHeight * $fieldSizePixels | |
FieldDisplaySizePixels = $fieldSizePixels - 2 | |
HalfFieldDisplaySizePixels = ($fieldSizePixels - 2) / 2 | |
} | |
$mainWindow.DataContext = $viewModel | |
[DispatcherTimer] $timer = New-Object DispatcherTimer -Property @{ | |
Interval = New-Object TimeSpan 0, 0, 0, 0, $stepsMilliseconds | |
} | |
[Snake] $snake = New-Object Snake $boardWidth, $boardHeight, $fieldSizePixels | |
[Food] $food = New-Object Food $boardWidth, $boardHeight, $fieldSizePixels | |
$food.Move($snake) | Out-Null | |
Function Update-View() { | |
$viewModel.SetSnakeGeometry([Geometry]::Parse($snake.GetGeometryString())) | |
[Tuple[Int32, Int32]] $foodLocation = $food.GetGeometryLocation() | |
$viewModel.SetFoodCenter((New-Object Point $foodLocation.Item1, $foodLocation.Item2)) | |
} | |
$mainWindow.add_Loaded({ | |
Update-View | |
$timer.Start() | |
}) | |
$timer.Tag = [SnakeAction]::Nothing | |
$timer.add_Tick({ | |
[SnakeAction] $action = $snake.Move($food) | |
Switch ($action) { | |
'Collision' { | |
if ($timer.Tag -ceq 'Collision') { | |
$viewModel.SetGameOverVisible($true) | |
$timer.Stop() | |
} | |
Break | |
} | |
'FoodEaten' { | |
$viewModel.SetScore($viewModel.Score + 1) | |
If ($food.Move($snake)) { | |
$viewModel.SetWonVisible($true) | |
$timer.Stop() | |
} | |
Break | |
} | |
} | |
Update-View | |
$timer.Tag = $action | |
}) | |
[EventManager]::RegisterClassHandler([Window], [Keyboard]::KeyDownEvent, [KeyEventHandler] { | |
Param ([Object] $sender, [KeyEventArgs] $eventArgs) | |
Switch ($eventArgs.Key) { | |
'Left' { | |
If ($snake.Segments[-1].Direction -cne 'Right') { | |
$snake.Direction = 'Left' | |
} | |
Break | |
} | |
'Right' { | |
If ($snake.Segments[-1].Direction -cne 'Left') { | |
$snake.Direction = 'Right' | |
} | |
Break | |
} | |
'Up' { | |
If ($snake.Segments[-1].Direction -cne 'Down') { | |
$snake.Direction = 'Up' | |
} | |
Break | |
} | |
'Down' { | |
If ($snake.Segments[-1].Direction -cne 'Up') { | |
$snake.Direction = 'Down' | |
} | |
Break | |
} | |
'Return' { | |
$snake.Reset() | |
$food.Move($snake) | |
$viewModel.SetScore(0) | |
$viewModel.SetGameOverVisible($false) | |
$viewModel.SetWonVisible($false) | |
Update-View | |
$timer.Start() | |
Break | |
} | |
'Q' { | |
If (-not $timer.IsEnabled) { | |
'Cheater ;)' | Out-Host | |
$viewModel.SetGameOverVisible($false) | |
$timer.Start() | |
} | |
Break | |
} | |
} | |
}) | |
[Application] $application = New-Object Application | |
$application.Run($mainWindow) | Out-Null | |
$timer.Stop() |
@nikonthethird New-Object : Exception calling ".ctor" with "0" argument(s): "Cannot create more than one
System.Windows.Application instance in the same AppDomain."
At C:\Users******PC\Documents\Snake.ps1:417 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 C:\Users******PC\Documents\Snake.ps1:418 char:1
- $application.Run($mainWindow) | Out-Null
-
+ CategoryInfo : InvalidOperation: (application:String) [], RuntimeException + FullyQualifiedErrorId : VariableIsUndefined
@nikonthethird New-Object : Exception calling ".ctor" with "0" argument(s): "Cannot create more than one
System.Windows.Application instance in the same AppDomain."
At C:\Users******PC\Documents\Snake.ps1:417 char:30
[Application] $application = New-Object Application
~~~~~~~~~~~~~~~~~~~~~~
- CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvocationException
- FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjec
tCommandThe variable '$application' cannot be retrieved because it has not been set.
At C:\Users******PC\Documents\Snake.ps1:418 char:1
- $application.Run($mainWindow) | Out-Null
+ CategoryInfo : InvalidOperation: (application:String) [], RuntimeException + FullyQualifiedErrorId : VariableIsUndefined
I got these as well when using Powershell IDE but no errors when running direct.
@nikonthethird
Replace the lines
417 and 418
With this this
$mainWindow.ShowDialog() | Out-Null
[GC]::Collect()
And you can reopen the snake multiple times...
Anyway nice example script ๐
@nikonthethird Replace the lines 417 and 418 With this this $mainWindow.ShowDialog() | Out-Null [GC]::Collect()
And you can reopen the snake multiple times... Anyway nice example script ๐
Perfect, thank you.
@MattClarke-ESi if you want to see some more powershell fun... I have made few public.
https://github.com/mi4c/Posh3d_cube_ball
Excellent (only?) example of
INotifyPropertyChanged
in PowerShell! Helped me get binding working in PowerShell.Still trying to get
ValidationRule
working, seems like your minesweeper will help for that.