Click here to Skip to main content
Click here to Skip to main content

PowerShell Falling Blocks - Ascii art on the move

By , 17 Aug 2011
Rate this:
Please Sign up or sign in to vote.

tetris-game.png

Introduction

Does the world really need yet another Falling Blocks game? Probably not. Did Windows need a powerful shell when CMD.exe was the only kid on the block? It most certainly did.

The goal of this article is to show just how powerful the PowerShell scripting language has become. It will lay out the basics of how an application could be written in pure PowerShell script and furthermore the RawUI functionality of PowerShell is treated in some detail.

This article has no intention of going into details about how to implement the original falling blocks game itself, there are plenty of resources out there doing just that. However, feel free to browse the code to see how I have chosen to implement the old classic.

Background

This project started - like most projects do - by someone saying something they should not have said. This time it was me!

I was trying to explain to a non technical friend of mine that the guys in Redmond had now finally included a powerful shell in Windows. Why I was doing this I cannot recall - but this was clearly mistake number one.

My friend looked unimpressed at the white blinking cursor on the blue background. "Can you play games in it?", he finally said. Mistake number two was not ending the conversation right there. Instead I answered something like "Yes, I should think so", to which he of course immediately responded: "What games?".

Sadly, I discovered that there were no convincing games I could show him. Now why would there be?

Mistake number three was not admitting that Power Shell was never meant for gaming. Instead I set out on a meaningless crusade, and thus Falling Blocks for Power Shell was born.

Running the Game

Fire up PowerShell. Change to the directory containing the downloaded source and run the fallingblocks.ps1 script

>.\fallingblocks.ps1

In the odd case that you are not really the graphics kind of guy, you can play around with the game board in pure integer mode. Just uncomment the last line in the board.ps1 script and run that script from the command line:

>.\board.ps1

This should produce something like the output shown in the figure below.

board.png

Saving the State of Affairs

Unlike traditional application development we do not own the host window in PowerShell scripting. The user of our application will expect us to restore all sizes, coloring and buffer contents when he is done playing.

Luckily for us the PSHostRawUserInterface class provides all the means needed to record the state of the UI before we start messing with it:

function Record-Host-State()
{
    $global:hostWindowSize      = $Host.UI.RawUI.WindowSize
    $global:hostWindowPosition  = $Host.UI.RawUI.WindowPosition 
    $global:hostBufferSize      = $Host.UI.RawUI.BufferSize    
    $global:hostTitle           = $Host.UI.RawUI.WindowTitle    
    $global:hostBackground      = $Host.UI.RawUI.BackgroundColor    
    $global:hostForeground      = $Host.UI.RawUI.ForegroundColor
    $global:hostCursorSize      = $Host.UI.RawUI.CursorSize
    $global:hostCursorPosition  = $Host.UI.RawUI.CursorPosition
    
    #Store the full buffer
    $rectClass = "System.Management.Automation.Host.Rectangle" 
    $bufferRect = new-object $rectClass 0, 0, $global:hostBufferSize.width, 
    $global:hostBufferSize.height
    $global:hostBuffer = $Host.UI.RawUI.GetBufferContents($bufferRect)
}

Given the recorded information we can then restore the previous state later as shown in the next function:

function Recover-Host-State()
{
    $Host.UI.RawUI.CursorSize       = $global:hostCursorSize
    $Host.UI.RawUI.BufferSize       = $global:hostBufferSize
    $Host.UI.RawUI.WindowSize       = $global:hostWindowSize
    $Host.UI.RawUI.WindowTitle      = $global:hostTitle
    $Host.UI.RawUI.BackgroundColor  = $global:hostBackground
    $Host.UI.RawUI.ForegroundColor  = $global:hostForeground
    
    $pos = $Host.UI.RawUI.WindowPosition
    $pos.x = 0
    $pos.y = 0
    #First restore the contents of the buffer and then reposition the cursor
    $Host.UI.RawUI.SetBufferContents($pos, $global:hostBuffer)
    $Host.UI.RawUI.CursorPosition = $global:hostCursorPosition
}

The Main function in the application script can then take the form:

function Main {

    #Store the current state of the PS Window 
    Record-Host-State
    
    try {

        ...
         
    } finally {
        #When done we make sure to return the shell
        #to the same state we started in
        Restore-Host-State
    }
}

#Run the main function 
. Main

The host state is recorded as the first thing and the remaining execution is placed within a try block which is associated with a finally block restoring the state of the host.

A Simple Event Loop

When dealing with user input in PowerShell ReadKey will suffice in most cases. However, in a gaming context the blocking nature of ReadKey will pose a problem as it will cause the application to pause and wait for input. Fortunately there is a property called KeyAvailable which can be checked prior to calling ReadKey. We introduce a function for reading characters of the keyboard which will return 0 if no character is available or if it is a meta character being pressed.

function Read-Character()
{
    if ($host.ui.RawUI.KeyAvailable) {
        return $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp").Character
    }
   
    return 0
}

With this new Read-Character function in our toolbox it is easy to sketch up a simple event loop which can handle the state of things. The loop below will continue until the user hits the q character on the keyboard.

[boolean]$quit = $FALSE
#
# Go into the control loop
#
do {
    if ($repaint) {
        ...
    }

    if (Is-Time-To-Move) {
        ...
    }
    
    $character = Read-Character
    
    # make sure we have a character 
    # so it is not just a meta key
    if ($character -ne 0) {
    
        switch -regex ($character) {

            # Quit 
            "q" {
                $quit = $TRUE
            }
        }
    }
} while (-not $quit)

To Paint or not to Paint

Eventhough we do have a repaint flag in the control loop shown previously it is not like we are drawing any pixels, this is taken care of by the PowerShell host window. The UI model of PowerShell is limited to a two-dimensional character cell grid which we can alter as we see fit. Any changes made to the grid is reflected by the host window.

All UI in the implemented game is "drawn" as strings using the function shown below.

function Draw-String([int] $x, [int] $y, [string] $fgColor, [string] $bgColor, 
    [string] $str)
{
    $pos = $Host.UI.RawUI.WindowPosition
    $pos.x = $x
    $pos.y = $y
    $row = $Host.UI.RawUI.NewBufferCellArray($str, $fgColor, $bgColor) 
    $Host.UI.RawUI.SetBufferContents($pos,$row) 
}

Exactly the same thing can be achieved using Write-Host if that is your preferred output method:

function Draw-String([int] $x, [int] $y, [string] $fgColor, [string] $bgColor,
    [string] $str)
{
    $cursor = $Host.UI.RawUI.CursorPosition
    $cursor.x = $x
    $cursor.y = $y
    $Host.UI.RawUI.CursorPosition = $cursor #reposition cursor
    Write-Host -NoNewline -BackgroundColor $bgColor -ForegroundColor $fgColor $str
}

To make updates to the screen as effective as possible we only update cells where changes have occurred. When for example a piece is locked on the game board we compare the new state of the board with the previous state and update the screen buffer accordingly:

function Update-Board([array] $oldBoard, [array] $newBoard) 
{
    #Check for differences - line by line
    for ($y = 0; $y -lt $global:BOARD_HEIGHT_IN_SQUARES; $y++) {
        for ($x = 0; $x -lt $global:BOARD_WIDTH_IN_SQUARES; $x++) {
            #If the values are different we need to take action

            if ($oldBoard[$x][$y] -ne $newBoard[$x][$y]) { 
                if ($newBoard[$x][$y] -eq 0) {
                    Erase-Square $x $y
                } else { 
                    Draw-Piece-Square $x $y $newBoard[$x][$y]
                }
            }
        }
    }
}

Drawing the squares as strings requires some of that old fashioned ascii magic. We settled with an approximation made up of four characters on three lines:

function Draw-Square-With-Offset([int]$offsetX, [int]$offsetY, [int]$hIndex, 
    [int]$vIndex, [string]$fgColor, [string]$bgColor)
{   
    $x = $offsetX + $hIndex*$global:SQUARE_WIDTH
    $y = $offsetY + $vIndex*$global:SQUARE_HEIGHT

    #Draw the three lines
    Draw-String $x $y $fgColor $bgColor "┌──┐"
    Draw-String $x ($y+1) $fgColor $bgColor "│  │"
    Draw-String $x ($y+2) $fgColor $bgColor "└──┘"
}

Initializing a Jagged Array

When implementing the board functionality it was not easy finding any good resources on how to create and initialize a jagged array in PowerShell. After playing around a bit I came up with the construct shown below. The ForEach operator is used to create a column of zeros, this column is then placed in yet another array by value using an additional $.

function Initialize-Board([int] $width=10, [int] $height=18) {
    $global:boardWidth = $width
    $global:boardHeight = $height
	
    #Note % is an alias for ForEach-Object
    $column = 1..$global:boardHeight | % { 0 }
	
    #The extra $ is there to make sure it is not a 
    #bunch of references to the same column
    $global:board  = 1..$global:boardWidth | % { ,$($column) }

    $global:lineCount = 0
}

Pacman Anyone?

That was all for me. Download the source, give the game a try and let me know what you think. And now that we are at it why not try out gaming for PowerShell yourself? I will be back next week to read your article about that crazy cheese Mr. Pacman now living in the land of PowerShell - and then there will be even more I can show my gaming friend.

Troubleshooting

If you are having trouble executing the script make sure you have not specified restricted execution policy:

>Set-ExecutionPolicy RemoteSigned

History

August 2011: initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Lasse W

Denmark Denmark
No Biography provided

Comments and Discussions

 
QuestionException setting "WindowSize": "Window cannot b helpe taller than 56 PinmemberDhreminc6-Apr-12 19:06 
AnswerRe: Exception setting "WindowSize": "Window cannot b helpe taller than 56 PinmemberLasse W11-Apr-12 10:48 
GeneralNeat ... PinmemberPeter Hayward18-Aug-11 21:03 
GeneralRe: Neat ... PinmemberLasse W18-Aug-11 21:50 
GeneralNeat! PinmvpNishant Sivakumar17-Aug-11 8:52 
GeneralRe: Neat! PinmemberLasse W17-Aug-11 9:17 
QuestionNeatotron PinmvpSacha Barber17-Aug-11 5:37 
AnswerRe: Neatotron PinmvpNishant Sivakumar17-Aug-11 8:53 
GeneralRe: Neatotron PinmvpSacha Barber17-Aug-11 9:05 
GeneralRe: Neatotron PinmemberLasse W17-Aug-11 9:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140421.2 | Last Updated 17 Aug 2011
Article Copyright 2011 by Lasse W
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid