PowerShellでテトリス

本当は七行で作りたかったんだけど、全然無理だった……。

←→
左右へブロック移動
ブロックを速く落とす(使わない)
ブロックを下まで落とす
Enter
回転(右のみ)


$RANDOM = New-Object -TypeName 'Random'
# 直径, 左上, ブロックの位置 × 4
$BLOCKS = @(
@(2,(0,4),(0,4),(0,5),(1,4),(1,5)),@(3,(0,3),(0,4),(1,3),(1,4),(1,5)),
@(3,(0,3),(0,3),(0,4),(0,5),(1,3)),@(3,(0,3),(0,3),(0,4),(0,5),(1,5)),
@(4,(-1,2),(0,2),(0,3),(0,4),(0,5))
)

function New-Block(){ $BLOCKS[$RANDOM.Next(5)] }

function Clear-Block(){ Write-Block(' ') }

function Get-Key(){
while($RUI.KeyAvailable){
$t = $RUI.ReadKey('NoEcho,IncludeKeyDown,IncludeKeyUp')
if($t.KeyDown){ $t }
}
}

function Make-Board(){
1..20 | % { $script:STR += ('##' + (' ' * 20) + '##')}
$script:STR += '##' * 12; $script:STR += ''
$RUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates -ArgumentList 6, 21
}

function Check-Block($new){
for($i = 2; $i -le 5; $i++){ if(([string]($STR[$new[$i][0]][$new[$i][1] * 2 + 2])) -ne ' '){ return $false } }
return $true
}

function Write-Block($c){
2..5 | % {
$y = $BLOCK[$_][0]; $x = $BLOCK[$_][1] * 2 + 1
$STR[$y] = [string]($STR[$y][0..$x]) + $c + [string]($STR[$y][($x+3)..23])
}
}

function Copy-Block() {
$o = $script:BLOCK; @($o[0], $o[1][0..1], $o[2][0..1], $o[3][0..1], $o[4][0..1], $o[5][0..1])
}

function Turn-Block($new){
switch($new[0]){
2 {}
3 { 2..5 | % {
$h = $new[$_][0] - $new[1][0]; $w = $new[$_][1] - $new[1][1]
$new[$_] = ($new[$_][0] - $h + $w), ($new[$_][1] + 2 - $h - $w)
} }
4 { 2..5 | % { $new[$_] = ($new[1][0] + $new[$_][1] - $new[1][1]), ($new[1][1] + $new[$_][0] - $new[1][0]) } }
}
}

# Blockの移動
function Move-Block(){
# キー操作
$script:KEY | % {
$new = Copy-Block
switch($_.VirtualKeyCode){
37 { 1..5 | % { $new[$_][1] = $new[$_][1] - 1 } } # ←
39 { 1..5 | % { $new[$_][1] = $new[$_][1] + 1 } } # →
40 { 1..5 | % { $new[$_][0] = $new[$_][0] + 1 } } # ↓
38 { # ↑
while(Check-Block $new){
1..5 | % { $new[$_][0] = $new[$_][0] + 1 }
$script:BLOCK = $new
}
1..5 | % { $new[$_][0] = $new[$_][0] - 1 }
}
13 { Turn-Block $new } # Enter
}
if(Check-Block $new){ $script:BLOCK = $new }
}

# 落下
$new = Copy-Block
1..5 | % { $new[$_][0] = $new[$_][0] + 1 }
if(Check-Block $new)
{ $script:BLOCK = $new }
else {
Write-Block '**'
$script:BLOCK = New-Block
if(-not (Check-Block $script:BLOCK)) { $script:GAMEOVER = $true }
}
}

# そろったLine消す
function Delete-Line(){
for($i = 19; $i -ge 0;){
if(($script:STR[$i].ToCharArray() -eq ' ').Length -eq 0){
$i..1 | % { $script:STR[$_] = $script:STR[$_ - 1] }
$script:STR[0] = ('##' + (' ' * 20) + '##')
$script:SCORE++
} else { $i-- }
}
}

# Blockの描画
$ZERO = New-Object System.Management.Automation.Host.Coordinates -ArgumentList 0, 0
function Display-Board(){
Write-Block '@@'
$script:STR[21] = "SCORE: $SCORE"
$RUI.SetBufferContents($ZERO, $RUI.NewBufferCellArray($STR, 'White', 'Black'))
}

# MAIN
Clear-Host

$OFS = ''
$RUI = $host.UI.RawUI
$FRAME = 200

$STR = @() # 表示する画面文字列
$KEY = $null # キー入力(配列)
$SCORE = 0 # 得点

$GAMEOVER = $false

Make-Board # 初期化
$BLOCK = New-Block # 操作するブロック

while($true){
$time = Measure-Command { $script:KEY = @(Get-Key) }

$wait = $FRAME - $time.Milliseconds - $script:SCORE * 10
if($wait -le $FRAME) { Start-Sleep -millisecond $wait }

Clear-Block
Move-Block
Delete-Line
if($GAMEOVER) { Break }
Display-Board
}
Clear-Host
while($RUI.KeyAvailable){ [void]$RUI.ReadKey('NoEcho,IncludeKeyDown,IncludeKeyUp') }
Write-Host "GAME OVER `nSCORE:$SCORE"
[void]$RUI.ReadKey('NoEcho,IncludeKeyDown,IncludeKeyUp')

メインのループとか、キー入力を拾うところはProudly Serving My Corporate Masters: Aliens.msh - A Monad Video Gameを参考にしました。
$host.UI.RawUI.ReadKey()するとキー入力待ちになっちゃうので、どうやってんのかなーと思ったら、ReadKey()ってそれまでに入力されて、まだ処理されていないキー入力を後から取り出せるみたいで、まずStart-Sleepでキー入力を待ち、$host.UI.RawUI.KeyAvailableがTrueだったらReadKeyすれば入力待ちにならない。
ラインを20位消すとWaitが0になるのでとても難しい……。