По работе мне периодически приходится править и дописывать скрипты для авто-тестов. И так исторически сложилось, что написаны они на Powershell. Но статья будет не об этом.
Обычно Powershell описывается как средство автоматизации для системных администраторов. И естественно, что к нему проявляют мало интереса. Поэтому я хочу рассказать, что его можно использовать не только для скучных рабочих задач.

Ради эксперимента и в качестве разнообразия у меня возникла идея написать небольшую игру с механикой скролшутера. Сначала захотелось ограничиться одной консолью, но потом разум возобладал. Так что для графического движка было решено использовать элементы Windows.Forms:
В результате появился “стартовый экран”. Но при этом выполнение скрипта по сути заблокировалось, т.к. после запуска диалогового окна — он ожидает от этого окна ответа и дальше не выполняется. Конечно, можно было бы сделать многопоточный скрипт, но было найдено более простое решение проблемы: добавление таймера.
Каждые 100 миллисекунд таймер вызывает функцию Check независимо от того, что выполняется в самом скрипте. Временной интервал выбран на глаз. По моим ощущениям обновление игры происходит достаточно плавно, но при желании можно сделать обновление и чаще.
Как выяснилось в последствии, все переменные, указанные в «тике» таймера, сохраняют значение на момент активации таймера и Check каждый раз вызывается с одним и тем же набором данных. Поэтому чтобы функция имела доступ к актуальным данным, вся нужная информация была упакована в объект:
Чтобы придать функции Start-Scroll законченный вид, осталось добавить хоткеи управления и контроллер звука:
Итого в игре есть флажок $Data.run, который обозначает — запущена ли игра, есть флажок $Data.hide, который выполняет роль паузы, есть набор переменных, где хранятся координаты игрока (pos), количество очков (score), таймер до выстрела (shot) и таймер до добавления противника (spawn), а также два массива fires и enemies, в которых хранятся соответственно данные по снарядам и противникам.
Управление получилось достаточно простое: A и D для перемещения своего персонажа, Esc — для выхода, а пробел заменяет кнопку “Старт” запуская игру или ставя её на паузу. Чтобы на время паузы все игровые элементы скрывались, используется функция Set-Hide:
Основная логика игры описана в функции Check:
Конечно, такая игра не претендует на “Лучшую игру года”. Но она может показать, что Powershell можно использовать не только, чтобы настраивать права доступа и контролировать работу локальной сети.
А ещё, в качестве бонуса, в режиме паузы отображается список подключенных USB-девайсов )
P.S. А те, кому лень собирать код по статье, могут скачать архив со скриптом и bat-ником для запуска.
Обычно Powershell описывается как средство автоматизации для системных администраторов. И естественно, что к нему проявляют мало интереса. Поэтому я хочу рассказать, что его можно использовать не только для скучных рабочих задач.

Ради эксперимента и в качестве разнообразия у меня возникла идея написать небольшую игру с механикой скролшутера. Сначала захотелось ограничиться одной консолью, но потом разум возобладал. Так что для графического движка было решено использовать элементы Windows.Forms:
Add-Type -Assemblyname System.Windows.Forms function Create-Form ([string]$name, $x, $y, $w, $h){ $win = New-Object System.Windows.Forms.Form $win.StartPosition = "Manual" $win.Location = New-Object System.Drawing.Size($x, $y) $win.Width = $w $win.Height = $h $win.Text = $name $win.Topmost = $True $win } function Create-Label ([string]$name, $x, $y){ $label = New-Object System.Windows.Forms.Label $label.Location = New-Object System.Drawing.Point($x, $y) $label.Text = $name $label.AutoSize = $true $label } function Create-Button ([string]$name, $x, $y, $w, $h){ $button = New-Object System.Windows.Forms.Button $button.Location = New-Object System.Drawing.Point($x, $y) $button.Size = New-Object System.Drawing.Size($w, $h) $button.Text = $name $button.Enabled = $false $button } function Start-Scroll (){ $form = Create-Form "Let's GO!" 200 150 300 400 $start = Create-Label "Press SPACE to run" 90 200 $info = Create-Label "<-- A D --> 'Esc' for exit" 80 340 $ship = Create-Label "/|\" 135 400 $form.Controls.Add($start) $form.Controls.Add($info) $form.Controls.Add($ship) $form.ShowDialog() }
В результате появился “стартовый экран”. Но при этом выполнение скрипта по сути заблокировалось, т.к. после запуска диалогового окна — он ожидает от этого окна ответа и дальше не выполняется. Конечно, можно было бы сделать многопоточный скрипт, но было найдено более простое решение проблемы: добавление таймера.
$timer = New-Object system.windows.forms.timer $timer.Interval = 100 $timer.add_tick({Check}) $timer.start()
Каждые 100 миллисекунд таймер вызывает функцию Check независимо от того, что выполняется в самом скрипте. Временной интервал выбран на глаз. По моим ощущениям обновление игры происходит достаточно плавно, но при желании можно сделать обновление и чаще.
Как выяснилось в последствии, все переменные, указанные в «тике» таймера, сохраняют значение на момент активации таймера и Check каждый раз вызывается с одним и тем же набором данных. Поэтому чтобы функция имела доступ к актуальным данным, вся нужная информация была упакована в объект:
$Data = @{run = $false; hide = $false; pos = 135; shot = 0; spawn = 0; usb = 0; score = 0; fires = @(); enemies = @()}
Чтобы придать функции Start-Scroll законченный вид, осталось добавить хоткеи управления и контроллер звука:
$form.KeyPreview = $True $form.Add_KeyDown({ if ($_.KeyCode -eq "A") {if ($Data.run -and -not $Data.hide -and $Data.pos -gt 0) {$Data.pos -= 5}} }) $form.Add_KeyDown({ if ($_.KeyCode -eq "D") {if ($Data.run -and -not $Data.hide -and $Data.pos -lt 265) {$Data.pos += 5}} }) $form.Add_KeyDown({ if ($_.KeyCode -eq "Escape") {$timer.stop(); $form.Close()} }) $form.Add_KeyDown({ if ($_.KeyCode -eq "Space") { if ($Data.run) { Set-Hide } else { $start.Text = ""; $Data.run = $true } } }) $sound = new-Object System.Media.SoundPlayer; $sound.SoundLocation = "$env:WINDIR\Media\Windows Information Bar.wav"
Итого в игре есть флажок $Data.run, который обозначает — запущена ли игра, есть флажок $Data.hide, который выполняет роль паузы, есть набор переменных, где хранятся координаты игрока (pos), количество очков (score), таймер до выстрела (shot) и таймер до добавления противника (spawn), а также два массива fires и enemies, в которых хранятся соответственно данные по снарядам и противникам.
Управление получилось достаточно простое: A и D для перемещения своего персонажа, Esc — для выхода, а пробел заменяет кнопку “Старт” запуская игру или ставя её на паузу. Чтобы на время паузы все игровые элементы скрывались, используется функция Set-Hide:
function Set-Hide (){ if ($Data.hide) { $start.Text = "" $start.Location=New-Object System.Drawing.Point(90, 200) $Data.enemies | foreach {$_.obj.Visible = $true} $Data.fires | foreach {$_.obj.Visible = $true} $info.Visible = $true $ship.Visible = $true } else { $start.Location=New-Object System.Drawing.Point(10, 10) $Data.enemies | foreach {$_.obj.Visible = $false} $Data.fires | foreach {$_.obj.Visible = $false} $info.Visible = $false $ship.Visible = $false } $Data.hide = -not $Data.hide }
Основная логика игры описана в функции Check:
function Check ()
function Check () { # Если игра не запущена - ничего не делаем if (!$Data.run) {return} # Если пауза - выводим сторонний текст if ($Data.hide) { if ($Data.usb -eq 0){ $start.Text = "" gwmi Win32_USBControllerDevice | %{[wmi]($_.Dependent)} | where {$_.DeviceID -notlike '*ROOT_HUB*'} | Sort Description | foreach { $start.Text += $_.Description +"`n" } $Data.usb = 500 } else { $Data.usb -= 1 } return } # Обновляем положение игрока $ship.Location=New-Object System.Drawing.Point($Data.pos, 300) # Создаем снаряд, если пришло время if ($Data.shot -eq 0) { $Data.fires += @{ obj = Create-Label "*" ($Data.pos + 5) 290; x = $Data.pos + 5; y = 290 } $form.Controls.Add($Data.fires[$Data.fires.Length - 1].obj) $Data.shot = 4 } else { $Data.shot -= 1 } # Создаем противника, если пришло время if ($Data.spawn -eq 0) { $hp = Get-Random -minimum 4 -maximum 6 $pos = Get-Random -minimum 0 -maximum 200 $Data.enemies += @{ obj = Create-Button "$hp" $pos -22 30 20; x = $pos; y = -22; health = $hp } $form.Controls.Add($Data.enemies[$Data.enemies.Length - 1].obj) $Data.spawn = 150 * $Data.enemies.Length } else { $Data.spawn -= 1 } # Проверяем снаряды foreach ($fire in $Data.fires){ # Обновляем положение $fire.obj.Location = New-Object System.Drawing.Point($fire.x, $fire.y) $fire.y -= 5 # Проверяем для каждого снаряда/противника - нет ли столкновения foreach ($enemy in $Data.enemies){ if ($fire.x + 5 -gt $enemy.x -and $fire.x -lt $enemy.x + 25 -and $fire.y -gt $enemy.y -and $fire.y -lt $enemy.y + 20){ $enemy.health -= 1 $enemy.obj.Text = $enemy.health $fire.y = -20 $sound.Play() } } } # Если первый в списке снаряд вышел за экран - убираем его if ($Data.fires[0].y -lt -10) { $form.Controls.Remove($Data.fires[0].obj) $Data.fires = $Data.fires[1..($Data.fires.Length - 1)] } # Проверяем противников foreach ($enemy in $Data.enemies){ # Если убит - перезапускаем if ($enemy.health -gt 0){ $enemy.y += 1 } else { $Data.score += 1 $enemy.health = Get-Random -minimum 4 -maximum 6 $enemy.x = Get-Random -minimum 1 -maximum 200 $enemy.y = -22 $enemy.obj.Text = $enemy.health } # Обновляем положение $enemy.obj.Location = New-Object System.Drawing.Point($enemy.x, $enemy.y) # Если приземлился - останавливаем игру if ($enemy.y -gt 300) { $Data.run = $false $start.Text = "Total score: " + $Data.score } } }
Конечно, такая игра не претендует на “Лучшую игру года”. Но она может показать, что Powershell можно использовать не только, чтобы настраивать права доступа и контролировать работу локальной сети.
А ещё, в качестве бонуса, в режиме паузы отображается список подключенных USB-девайсов )
P.S. А те, кому лень собирать код по статье, могут скачать архив со скриптом и bat-ником для запуска.