(Note: These solutions were written for Event 7.) 

Beginner Event 7: The discus throw

In the discus throw event, you will be asked to go farther than ever before. To meet this challenge, you must track your progress by writing the information to a log file. You will also be required to read from that log file to determine progress.

Guest commentator: David Zazzo

David is an infrastructure and messaging consultant with Microsoft Consulting Services. He also maintains an eponymous blog on MSDN.

Image of guest commentator David Zazzo 

VBScript solution

The challenge is to take an existing VBScript and extend it to create and write outuput to a log file. Log files are invaluable for troubleshooting. This is particularly important if you have a regularly scheduled task or long-running job that you do not want to continuously monitor.

The script provided performs some basic tasksopen a new instance of Microsoft Office Word, open a document, find misspelled words, replace them with the correct spelling, and get out. This should not be too difficult to document.

The initial version had zero logging capabilities, so I started out by adding four subroutines: OpenLogFile, CloseLogFile, WriteOutput, and WriteCriticalError. These four routines are explained here:

·         OpenLogFile: Creates a new instance of FileSystemObject, and then uses the OpenTextFile method to open or create a log file at the path specified in the script.

·         CloseLogFile: If the log file is open, close it to ensure that any buffers are written out to the file and that the file is closed properly and its resources released.

·         WriteOutput: This routine was created to make my life a little easier. I wanted to write out to the console as well as the log file, and generally, the information would be the same, so this one subroutine does both. It writes out to the console, and if the log file is open and debug logging enabled, it writes it there, too.

·         WriteCriticalError: Similar to above, but this is an error handling routine. Write an error string to WriteOutput, close the log file (using CloseLogFile), and exit the script returning the error number (using Err.Number).

Additionally, I added some other logic to the script.  I added a command-line parameter to enable logging (/debug), instructed Word to save the document and politely exit (the previous version left Word open and running), and provided a summary of the script’s configuration to the user.

The completed BeginnerEvent7Solution.vbs script is seen here.

BeginnerEvent7Solution.vbs

Option Explicit
'==========================================================================
'
' NAME: Zazzo-ReplaceWordInWord.vbs
'
' AUTHOR: ed wilson , MrEd
' DATE  : 5/8/2009
'
' COMMENT: http://www.microsoft.com/technet/scriptcenter/resources/qanda/aug06/hey0808.mspx
' HSG-05-14-2009
'
' 5/15/2009 -- dzazzo -- added helper functions and added
'                                                         command-line argument to enable or disable logging
'==========================================================================

Dim strLogText
Dim fileSystemObject
Dim objLogFile
Dim colArguments
Dim blnDebugMode
Dim textToFind
Dim textToReplace
Dim strFileToOpen
Dim intErrNumber
Dim strErrDescription
Dim objWord
Dim objDoc
Dim objSelection

Const fsoOpenForAppending = 8
Const wdReplaceAll = 2

'blnDebugMode = True
strFileToOpen = "C:\Users\dzazzo\Documents\Summer 09 Scripting Games\Zazzo\test.doc"
textToFind = "mispelled"
textToReplace = "misspelled"

'***** Subroutine Declaration *****
Sub OpenLogFile
          Set fileSystemObject = CreateObject( "Scripting.FileSystemObject" )
          If IsObject( fileSystemObject ) Then
                   Set objLogFile = fileSystemObject.OpenTextFile( "c:\Users\dzazzo\Documents\Summer 09 Scripting Games\Zazzo\Zazzo-ReplaceWordinWord.log",fsoOpenForAppending,True )
                   objLogFile.WriteLine VbCrLf & "************************************************************" & VbCrLf & "Zazzo-ReplaceWordinWord started at " & Now & VbCrLf & "************************************************************"
                   WriteOutput "Debug logging enabled."
          End If
End Sub

Sub CloseLogFile
          If IsObject( objLogFile ) Then
                   objLogFile.Close
                   Set objLogFile = Nothing
          End If
End Sub

Sub WriteOutput( strLogText )
          If blnDebugMode Then
                   If IsObject( objLogFile ) Then
                             objLogFile.WriteLine Now & ":" & vbTab & strLogText
                   End If
          End If
          WScript.Echo strLogText
End Sub

Sub WriteCriticalError( intErrNumber, strErrDescription )
          WriteOutput "[!] ERROR: " & intErrNumber & ": " & strErrDescription
          CloseLogFile
          WScript.Quit intErrNumber
End Sub
         
'***** Sub Declaration *****

'***** Set up *****
On Error Resume Next
WriteOutput "Initializing..."
Err.Clear

Set colArguments = WScript.Arguments
If colArguments.Count > 0 Then
          If colArguments.Unnamed.Exists( "/debug" ) Then
                   blnDebugMode = True
          End If
End If

If blnDebugMode Then
          WScript.Echo "Enabling debug logging..."
          OpenLogFile
End If

WriteOutput "Running ReplaceWordinWord with the following settings:" & VbCrLf & _
          vbTab & "Target File:" & VbCrLf & vbTab & strFileToOpen & VbCrLf & _
          vbTab & "Word to Find:" & vbTab & textToFind & VbCrLf & _
          vbTab & "Replace With:" & vbTab & textToReplace & VbCrLf
         
'***** End Set up *****

Err.Clear
WriteOutput "Starting Microsoft Word..."
Set objWord = CreateObject( "Word.Application" )

If Not IsObject( objWord ) Then
          WriteCriticalError Err.Number, Err.Description
End If

WriteOutput "Word.Application successfully created."

WriteOutput "Making Word visible..."
objWord.Visible = True


WriteOutput "Opening: " & strFileToOpen
Set objDoc = objWord.Documents.Open(strFileToOpen)
If Err.Number <> 0 Then
          WriteCriticalError Err.Number, Err.Description
End If

Set objSelection = objWord.Selection
objSelection.Find.Text = textToFind
objSelection.Find.Forward = True
objSelection.Find.MatchWholeWord = True
objSelection.Find.Replacement.Text = textToReplace

WriteOutput "Finding and replacing text..."
objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll
If Err.Number <> 0 Then
          WriteCriticalError Err.Number, Err.Description
End If
         
'***** Clean up *****
WriteOutput "Saving document..."
objWord.ActiveDocument.Save
If Err.Number <> 0 Then
          WriteCriticalError Err.Number, Err.Description
End If

WriteOutput "Closing Microsoft Word..."
objWord.ActiveDocument.Close
objWord.ActiveWindow.Close
objWord.Application.Quit
Set objWord = Nothing

WriteOutput "Finished!"
CloseLogFile

 

When the script is launched, the is displayed:

Image of what is displayed when script is run 

Guest commentator: Ben Pearce

 Image of guest commentator Ben Pearce

Ben is a premier field engineer at Microsoft in the United Kingdom. He maintains the Benp's guide to stuff blog on TechNet.  

Windows PowerShell solution

Here are additional details about my solution to this event. There are really two interesting parts to this script: the objects and error checking.

Objects

In Windows PowerShell, objects are your best friend. After you have something represented as an object, you can stick it in an array. And after you have something in an array, you can use the pipeline to get at all of Windows PowerShell’s funky stuff. Ben, what do you mean by “funky stuff”? After we use objects and arrays, we can easily sort, filter, format, and export data. For that reason, I try to use objects as often as possible.

Let me explain the CreateLogObject function. The purpose of this function is to return an object that comprises two properties: the TimeStamp property and the Message property. This object can then be added to the array stored in the $results variable.

Inside the CreateLogObject function, we must create a new .NET object. To do this, we use the New-Object cmdlet and tell it to create a new system.object. We then use the Select-Object cmdlet to add properties to the object. This a bit weird, but Select-Object can add new properties to an object as well as remove properties! This is seen here:

$Log = new-object system.object | select-object Timestamp, message

After we get the object, it is relatively easy to populate the properties with values. We just assign values to the properties as shown here:

$Log.Timestamp = (get-date).datetime

$Log.Message = $Message

We can use the CreateLogObject function to create an entry in the log by passing it the message. We then append it to the end of the array. This is shown here:

$LogRecord = CreateLogObject "Error opening document in Word $ErrMsg  Exiting Script!!"

$results += $LogRecord

After all that is done, look how easy it is to output to console, CSF, and HTML respectively:

$results

$results | Export-csv -Path .\WordDebug.csv –NoTypeInformation

$results | ConvertTo-HTML > WordDebug.html 


Error Checking

The other interesting part of the script is error checking. First, we set the value of ErrorActionPreference to “SilentlyContinue.” This means that any errors are added to the automatic $Error array but are suppressed. We then clear $Error by calling the clear method as seen here:

$Error.clear()

When $Error has been cleared, we execute some code. Following that, we check if $Error is still empty. If it is empty, the code completed without an error, and if it is greater than 0, we have an error. This is seen here:

if ($error.count -gt 0)

If we have an error, we can get the error message by querying the message property of the exception. This is seen here:

$ErrMsg = $error[0].exception.message

The completed BeginnerEvent7Solution.ps1 script is seen here.

BeginnerEvent7Solution.ps1

#Useage
#.\ReplaceWordInWord.ps1 (Runs script with no debug output)
#.\ReplaceWordInWord.ps1 -debug (Displays debug on screen)
#.\ReplaceWordInWord.ps1 -debug -csv (Creates CSV file with debug output)
#.\ReplaceWordInWord.ps1 -debug -html (Creates html file with debug ouput)

#Use Param to specify debug parameters
param([switch]$debug, [switch]$csv, [switch]$html)

function CreateLogObject($Message)
{
    #This function creates a new custom object and then returns it
    #It takes the message as a parameter
    $Log = new-object system.object | select-object Timestamp, message
    $Log.Timestamp = (get-date).datetime
    $Log.Message = $Message
    return $Log
}

#Suppress error messages as we will deal with these in an error handler
$erroractionpreference = "SilentlyContinue"
$error.clear()

#Test harness that copies the document from the original
del "C:\store\MS\Tech Docs\Powershell\Scripting Games 09\test.doc"
copy "C:\store\MS\Tech Docs\Powershell\Scripting Games 09\test - Original.doc" -destination "C:\store\MS\Tech Docs\Powershell\Scripting Games 09\test.doc"

#create an empty Array
$results = @()

#Create a Word Object
$objWord = New-Object -ComObject word.application
$objWord.Visible = $True
$objDoc = $objWord.Documents.Open("C:\store\MS\Tech Docs\Powershell\Scripting Games 09\test.doc")
if ($error.count -gt 0)
{
   
        #If error > 0 Could not open word
        $ErrMsg = $Error[0].exception.message
        #Call CreateLogObject function
        $LogRecord = CreateLogObject "Error opening document in Word $ErrMsg  Exiting Script!!"
        #Add Object to Results array
        $results += $LogRecord
         #If Debug flag has been specified then output the results
        if($debug)
        {
            if($csv)
            {
                #Pipe results into Export-CSV
                $results | Export-csv -Path .\WordDebug.csv -NoTypeInformation
                #Open file in default csv file handler
                Invoke-Item .\WordDebug.csv
            }
            elseif($html)
            {
                #Pipe results into Convert-HTML
                $results | ConvertTo-HTML > WordDebug.html
                #Open file in default HTML file handler
                Invoke-Item .\WordDebug.html
            }
            else
            {
                #Simply output results to console
                $results
            }
        } #End $Debug if statement
} #End problem opening Word If Statement
else
{
        #We succesfuly opend the Word Document      
        #Create custom message objct
        $LogRecord =  CreateLogObject "Document Succesfully Opened"
        #Add success record to array
        $results += $LogRecord
        $objSelection = $objWord.Selection

        $FindText = "mispelled"
        $ReplaceText = "spelled incorrectly"

        $ReplaceAll = 2
        $FindContinue = 1
        $MatchCase = $False
        $MatchWholeWord = $True
        $MatchWildcards = $False
        $MatchSoundsLike = $False
        $MatchAllWordForms = $False
        $Forward = $True
        $Wrap = $FindContinue
        $Format = $False
       
        #Clear the error log to use it again
        $error.clear()
        $objSelection.Find.Execute($FindText,$MatchCase,
          $MatchWholeWord,$MatchWildcards,$MatchSoundsLike,
          $MatchAllWordForms,$Forward,$Wrap,$Format,
          $ReplaceText,$ReplaceAll)

        if ($error.count -gt 0)
        {
                #An Error occured whilst trying to Replace text       
                $ErrMsg = $error[0].exception.message
                $LogRecord = CreateLogObject "Error Executing Replace $ErrMsg"
                $results += $LogRecord
            }
        else
        {
                #Succesfully replaced in Word document
                $LogRecord = CreateLogObject "Replace Worked"
                $results += $LogRecord
        }

        #If Debug flag has been specified then output the results
        if($debug)
        {
            if($csv)
            {
                #Pipe results into Export-CSV
                $results | Export-csv -Path .\WordDebug.csv -NoTypeInformation
                #Open file in default csv file handler
                Invoke-Item .\WordDebug.csv
            }
            elseif($html)
            {
                #Pipe results into Convert-HTML
                $results | ConvertTo-HTML > WordDebug.html 
                #Open file in default HTML file handler
                Invoke-Item .\WordDebug.html
            }
            else
            {
                $results
            }
        } #End $Debug if
        
} #End Word Could not open file Else

When the script is run with the debug switch and the HTMLl switch, Internet Explorer displays the output seen here:

Image of output displayed  


Advanced Event 7: The discus throw

In this discus throw event, you will decide if you want to hold or throw cards as you attempt to reach the sum of 21 in your hand.

Guest commentator: Jeremy Engel

Jeremy is a senior systems engineer for LabCorp and a coder extraordinaire. He was nominated to be a moderator for the Official Scripting Guys forum. His motto is "No Web site, no books, just a camp fire and a lot of good stories!"

Image of guest commentator Jeremy Engel 


VBScript solution

My script is divided into two parts. A class, which holds the core game data and data manipulation methods, and an HTA, which handles the game logic and makes the script visually appealing. When formulating my ideas for the code, I tried to envision the process one would go through to set up the game and then play it.

The first step was to get a deck of cards. I decided at this point to add a little flare and allow the player to select the number of decks to be used. Though my code allows for an infinite number of decks, I decided to put in a hard cap of four decks to allow for more frequent reshuffling in case the player decides to play an extended game. The (re)shuffling of the decks happens at the beginning of the game and whenever there are no more available cards.

I decided from the beginning, that playing 21 by myself is mind-numbingly boring, so I planned to accommodate multiple players. Again, I set the cap at four players, mainly because I like the number four (it’s a very sturdy-sounding number), but also because I didn’t want to get into coding wildly dynamic HTML layouts for the HTA. When setting up the game, you can choose whether your opponents will be fellow humans or the computer, though I made it so that the first player must be a human. It would be rather boring to watch the computer play itself. For the computer-controlled players, I decided to use “dealer rules.” They’re pretty simple: if the computer player’s hand is worth more than 17 points, it stays; otherwise, it hits.

I contemplated adding in additional commonly used rules, such as: Aces can count for 1 or 11, and if you stay under 21 with five cards, you win. However, I decided against it so I could stay as close to the defined rules of the game as laid out in the scenario, except for the following change. While the scenario instructed that as soon as someone gets 21, they automatically win that round; I altered that to be that they immediately “stay” and play moves to the next player. Chances are that you’ll have won the round anyway when it’s all said and done; that is unless someone else got 21! This was done because it just didn’t seem fair to the other players do not even get a chance to nullify that player’s assumed win.

To make the game a little more interesting, I decided to rotate the order of play each turn. The reason for this is that the first player is always at a disadvantage because their actions help decide how the others will play. Conversely, the last player has the best advantage.

In the HTA itself, I displayed all of the statistics I thought a player would need. And finally, just to make it really cool, I created a deck of cards and call those .PNG images as needed.

Because of the length of the HTA, the VBSscript class,  and all of the .PNG image files, the solution is too huge to post here and is therefore available by download on the Microsoft Download Center.

Image of Jeremy Engel's 21 solution 


Guest commentator: Marco Shaw

Marco has been working in the IT industry for over 10 years and is a Microsoft MVP for Windows PowerShell. Marco co-authored the SAMS PowerShell Unleashed book. He also wrote an excellent article on System Center and Windows PowerShell for TechNet Magazine and has contributed to various other projects. He runs a virtual Windows PowerShell user group that meets online via Office Live Meeting and is one of the community directors of the new Windows PowerShell Community site. He also maintains an eponymous blog.

Image of guest commentator Marco Shaw

 

Windows PowerShell solution

Reading the event description thoroughly is important and trying to determine whether something was intentional or not is always forefront on my mind. I spent a short amount of time thinking about whether the numbering of the suits was actually something significant, but I decided that didn’t really make sense. I also noticed that the event didn’t make any mention about pulling cards out of the deck after they were chosen. This is what made sense to me.

My first thought was to use a hash table instead of an array to hold the cards. I quickly tested how I would add and remove keys/values from a hash table, and then realized that going forward with a hash table approach might not be the best method. I decided to use an array. I was able to develop something by using an array, but it was too simple, so I decided to add the capability to play against the computer.

Things were going well, until I realized at the end that I needed additional logic to handle the situation of a player deciding to end the game prematurely. That part was fixed easily enough.

I decided to go a step further and record the number of wins in a multigame match. That required a few changes, and remembering to reset the appropriate variables back to zero.

In the end, I thought about a few more things I might have done. In particular, I don’t think I have added all the logic on the computer side that I could have added. I could have made the computer a smarter player, especially when the they decide to stop playing, while the computer is still playing additional cards when it probably should not.

Just when I was ready to submit the event,I realized the error of my ways. I was playing with two decks of cards! What was I thinking? I started my script by creating two decks of cards: One for the player and one for the computer, but they were supposed to be using only one deck. I thought I was almost there!

I also noticed, after a bit more testing, that I was generating an error under certain conditions. I had to fix my number of decks and retest for the error afterwards.

The changes I had to make in order to play with only one deck were minimal. I also managed to generate the same error. This was good because it is difficult to troubleshoot an error that you cannot reproduce. However, I could not see the problem at first glance. I decided to echo out the variables to the console on each run. I still couldn’t figure it out. Next, I simply tried casting some of the variables to a string before running the split method, but that cascaded into other errors.

Finally, I realized that I was starting new games, but I wasn’t starting with a new deck! I was using the same deck and must have been running out of cards! I fixed up my logic, and all seemed well afterward. Here is the complete AdvancedEvent7Solution.ps1 script.

AdvancedEvent7Solution.ps1

# Set a few variables to zero.
$my_wins=0
$comp_wins=0
$ties=0

# Create my object for random numbers.
$rand=new-object system.random

# Keep going until the user wants to stop playing.
while($true){

  # Set a few variables to zero for each new game.
  $my_hand=0
  $comp_hand=0

  # Create the deck of cards.
  $cards=@()
  "h","d","c","s"|%{for($i=1;$i -lt 14;$i++){$cards+="$i $_"}}

  # This loop is for the current game.
  while($true){
    cls

    # Get my card, if still playing.
    if($response -ne "n"){
      $my_rand_number=$rand.next(0,$cards.length-1)
      $my_card=$cards[$my_rand_number]
      $cards=$cards|where{$_ -ne "$my_card"}
    }

    # Get the computer's card.
    $comp_rand_number=$rand.next(0,$cards.length-1)
    $comp_card=$cards[$comp_rand_number]
    $cards=$cards|where{$_ -ne "$comp_card"}

    # Get my current count.
    if($response -ne "n"){
      $my_number,$my_suit=$my_card.split()
      $my_hand+=$my_number
    }
   
    # Get the computer's count.
    $comp_number,$comp_suit=$comp_card.split()
    $comp_hand+=$comp_number
 
    # Switch back to the full suit name and value.
    switch ($my_number) {
      1 {$my_number="Ace"}
      11 {$my_number="Jack"}
      12 {$my_number="Queen"}
      13 {$my_number="King"}
    }
    switch ($comp_number) {
      1 {$comp_number="Ace"}
      11 {$comp_number="Jack"}
      12 {$comp_number="Queen"}
      13 {$comp_number="King"}
    }
    switch ($my_suit) {
      h {$my_suit="Hearts"}
      d {$my_suit="Diamonds"}
      c {$my_suit="Clubs"}
      s {$my_suit="Spades"}
    }
    switch ($comp_suit) {
      h {$comp_suit="Hearts"}
      d {$comp_suit="Diamonds"}
      c {$comp_suit="Clubs"}
      s {$comp_suit="Spades"}
    }

    # Display the current card and total.
    "My card: $my_number of $my_suit"
    "My total: $my_hand"

    "Computer's card: $comp_number of $comp_suit"
    "Computer's total: $comp_hand"

    # Determine whether there is a winner/loser, a tie, or to continue.
    if($my_hand -gt 21 -and $comp_hand -gt 21){"Game over.  Both over 21.";break}
    elseif($my_hand -gt 21){"Game over.  The Computer wins.";$comp_wins++;break}
    elseif($comp_hand -gt 21){"Game over.  I win!";$my_wins++;break}
    elseif($comp_hand -gt $my_hand -and $response -eq "n"){"Game over.  The computer wins.";$comp_wins++;break}
    elseif($my_hand -eq 21 -and $comp_hand -eq 21){"Game over.  Tie";$ties++;break}
    elseif($my_hand -eq 21){"Game over.  I win!";$my_wins++;break}
    elseif($comp_hand -eq 21){"Game over.  The computer wins.";$comp_wins++;break}
    elseif($my_hand -lt 21){if($response -ne "n"){$response=read-host "Pick another? (y/n)"}}
  }

  # The player wants to continue.
  $continue=read-host "Keep playing? (y/n)"
  if($continue -eq "n"){break}
  else{$response=$null}
}

# Display the win/loss results for a multigame session.
"My wins: $my_wins"
"Computer wins: $comp_wins"
"Ties: $ties"

When the script is run, this output is seen:

Image of script's output 


Once again, we come to the end of another great commentator series. Our commentators exceeded our wildest expectations, and clearly got into the spirit of the games. We extend our most sincere thanks to David, Ben, Jeremy, and Marco for a job well done.

Ed Wilson and Craig Liebendorfer, Scripting Guys