Last active
August 15, 2024 23:12
-
-
Save nikonthethird/2ab6bfad9a81d5fe127fd0d1c2844b7c to your computer and use it in GitHub Desktop.
PowerShell script for playing Snake.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@MattClarke-ESi if you want to see some more powershell fun... I have made few public.
https://github.com/mi4c/Posh3d_cube_ball