Monkey Fighter Monkey Fighter
Games Help Help Search Search Shop Shop
Tetris for Word


Name: Wordtris
Author: Robert Walsh
Platform: Microsoft Word 97
Language: Visual Basic for Applications (VBA)
License: Free

Download

Wordtris.doc
WordtrisXP.doc

The Code

Wordtris.txt



Google Hacks
Google Hacks


eBay Hacks
eBay Hacks


Amazon Hacks
Amazon Hacks


XP Hacks
Windows XP Hacks


Hacking Tivo
Hacking Tivo




Wordtris is Tetris for Microsoft Word 97. This game plays on the document instead of on a form. Every component of this game is native to Word. That means no Active X controls, API calls, or embedded objects. This is an interactive game playing document!


Wordtris is a hack. That means, it is software that performs a task not originally intended of the platform. As such, performance is not guaranteed. This is where I whine about all the technical issues that plague Word. But why? Word wasn't designed to do this! Oh yeah, Wordtris was developed and tested on a P4 1.8mhz. The current version is not synced with the timer. So, depending on your processor, the game may play too fast or too slow. Deal with it...


Wordtris tutorial: How to write real-time Word game documents

The rest of this page is a tutorial for writing real-time Word game documents. Specifically, I am explaining line-by-line the code I wrote to make Wordtris. Since I wrote this for Word 97, you'll need Word 97 to run this document. In typical M$ fashion, newer version of Word are not completely backwardly compatible. As such, don't expect this software to work correctly on anything but Word 97. However, I have included a download that works with Word XP.


Dim Board(12, 16) As Boolean
Dim RockPX(4, 28) As Integer
Dim RockPY(4, 28) As Integer
Dim X, Y, X1, Y1, T, P, M As Integer
Dim RockX, RockY, RX, RY, RS As Integer
Dim RockN, RockC, RockR, RR As Integer
Dim Playing As Boolea
									


The first thing to do is declare all the global variables because we don't want do any excessive parameter passing. Board(,) stores the state of the game board. If a coordinate is true, a rock occupies that spot. RockPX(,) and RockPY(,) stores how to draw all seven rocks. That is, the first index (4) selects a subrock of a rock, the next index (28) store the rock and their possible rotations. X, Y, X1, Y1, T, P, M are variables that are used a lot. So, to reduce the amount of memory VBA will leak and to improve performance, I declared them here. The falling and next rocks are the only rocks we need to track. Thus, the next set of variables store x and y position, falling speed, which rock is next, which is current, the current rotation of the rock - you get the point. Playing simply indicates if the game is playing, as opposed to waiting for it to be started.


Sub Init()
  Dim XStng, YStng As String
  Dim T As Integer

  XStng = "01120112001100000012001201120011001100110123000101110001" & _
           01120112001100000122012201120011001100110123011100010111"
  YStng = "11222121212101231222121121221210011221211111012000120121" & _
           11222121212101231112222111211210011221212222201201221012"
  T = 1
  For Y = 0 To 27
    For X = 0 To 3
      RockPX(X, Y) = Int(Val(Mid(XStng, T, 1)))
      RockPY(X, Y) = Int(Val(Mid(YStng, T, 1)))
      T = T + 1
    Next
  Next

  RS = 3
  RockN = Int(Rnd * 6)
  ClearBoard
End Su
									


Init initializes the rock position matrices, sets the falling speed to 3, randomly generates the next rock, and clears any rocks that might be on the game board. It should all make sense except the first part. You see, VBA doesn't have data statements and I didn't want to use an external data file. So, I stored the rock position data in a string. There are exactly four subrocks for each rock. So, the subrocks positons can be described as offsets with a range between 0 and 3.


Sub ClearBoard()
  For X = 1 To 12
    For Y = 1 To 16
      If X = 1 Or X = 12 Or Y = 16 Then
        Board(X, Y) = True
      Else
        Board(X, Y) = False
      End If
    Next
  Next
End Su
									


Clear the game board by setting all the coordinates to false. Of course, the left, right and bottom edge are set to true. This helps with collision detection by making it seem like there is a rock in at those coordinates.


Sub Begin()
  Randomize
  Init
  Setup
  NewRock
  Playing = True
  Play
End Su
									


Begin is the routine that is called when a person starts a new game. It starts by randomizing the randomizer. Then it calls Init which resets the variables for a new game. Setup is called to draw the visible layout of the document. That included the table, shading and all text that is static in the game. To play the game, we need a falling rock. So, NewRock is called. Playing is set to true and the play routine is called to commence game play.


Sub Play()
  While (Playing)
    For P = 1 To RS
      EraseRock
      RockR = RR
      RockX = RX
      DrawRock
      SendKeys "%h"
      DoEvents
    Next

    If DropRock Then
      CheckLine
      CheckDead
      NewRock
    End If
  Wend

  GameOver
End Su
									


The Play sub loops until Playing=false which happens when the player loses or presses the key that calls the StopGame sub. The next code section iterates RS time, which is the number associated with the play speed. The lower RS the faster the rock drops. We are about to update the falling rock with a new position. So, EraseRock is called. If EraseRock wasn't called, the rock would leave a trail. RockR and RockX are updated with new position a rotation coordinates which are associtated with keyboard input. Yet, there is no code for reading the keyboard. Instead, Words' built-in keyboard customizing is used to assign game keys to their associated sub. Thus, since these subs may execute at any time, we track the rocks rotation and position with separate variables and update them here. Once the falling rock is updated, DrawRock is called to draw it on the game board. SendKeys "%h" sends alt + h to the keyboard message buffers. That key is assigned to a sub which moves the cursor off the game board. Calling the sub directly creates problems. This is the trick to getting it done right. DoEvents is a VB command that tells VB to give up the processor so other processesn namely the OS can get a time slice. If you don't call this routine regularly in a VB application that run for an extended time period, you will wish you did. The rock is falling and now is a good time to move it down one position. So, DropRock is called and will return true if it landed on something. If so, two situations could arise. First, a line may have been completed. So, CheckLine checks for that. The player may have filled the column. In which case the game is over. So, CheckDead checks for that. We will assume the game isn't over and fetch a NewRock. If the player lost or manually terminated the game, the while loop ends and GameOver is called to display that the game is over.


Sub StopGame()
  Playing = False
End Su
									


Without this routine, there would be no way to halt this program without letting it run its course. So, we create this sub and assign a key to it. Press that key and the game halts.


Sub SpeedDown()
  RS = RS + 1
  If RS > 5 Then
    RS = 5
  End If

  ActiveDocument.Tables(1).Cell(16, 19).Select
  Selection.Text = 6 - RS
End Su
									


SpeedUp increase RS (falling speed variable) by one. It shouldn't be allowed to be greater than 5. So, if it is, just set it equal to 5. Since the game speed just changed, the text displaying the game speed should be updated. A table was used to make the entire game layout into a text grid (like the C= 64). So, to update the speed text, just select the texts' cell and write the new value.


Sub SpeedUp()
  RS = RS - 1
  If RS < 1 Then
    RS = 1
  End If

  ActiveDocument.Tables(1).Cell(16, 19).Select
  Selection.Text = 6 - RS
End Su
									


SpeedDown decreases RS (falling speed variable) by one. It shouldn't be allowed to be less than 1. So, if it is, just set it equal to 1. Since the game speed just changed, the text displaying the game speed should be updated. A table was used to make the entire game layout into a text grid (like the C= 64). So, to update the speed text, just select the texts' cell and write the new value.


Sub CheckLine()
  For Y = 0 To 3
    M = 0
    For X = 2 To 11
      If RockY + Y < 16 And RockY + Y > 0 Then
        If Board(X, RockY + Y) Then
          M = M + 1
        End If
      End If
    Next
    If M = 10 Then
      RemoveLine
    End If
  Next
End Su
									


CheckLine check for the completion of a line. Since, this sub is called only when a rock lands, only four lines need to be checked. Thus, Y is 0 to 3 or from the rock position to its greatest possible offset. M counts the occupied spots for each line. So, it should start counting at zero. X iterates between 2 and 11 because X coordinates of 1 and 12 are the left and right edge. Even though four lines are checked, only one rock, the straight line rock, rotated on end could complete all four lines. Therefore, we should check to see that the line we are counting is within the bounds of the game. Valid Y coordinates are between 1 and 15 inclusively. If the line is in the playable game area, add 1 to M whenever a piece of rock occupies that coordinate. If M counted 10 occupied coordinates, that line is completed and should be removed.


Sub RemoveLine()
  For Y1 = RockY + Y To 2 Step -1
    For X1 = 2 To 11
      Beep
      Board(X1, Y1) = Board(X1, Y1 - 1)
      ActiveDocument.Tables(1).Cell(Y1, X1).Select
      If Board(X1, Y1) Then
        Selection.Text = "X"
      Else
        Selection.Text = ""
      End If
    Next
  Next
End Su
									


The line RockY + Y is the one we need to remove. This is done by shifting the game board above this line down one line. Note that Y is a Global with multiple meanings in this module. I am careful to enter this sub only when Ys' meaning is valid. Incidentally, this is bad coding practice. So, don't do this at home, kids. Anyhow, the Y1 iterates up the game board from the line in question. X1 iterates across the line. An beep is play to make things exciting. Then, the X1, Y1 coordinate is updated with the X1, Y1-1 coordinate. To draw this visually on the document, the associated cell is selected and an X or nothing is type accordingly.


Sub CheckDead()
  If RockY < 1 Then
    Playing = False
  End If
End Su
									


If the rock couldn't move completely onto the game board (Rock < 1), the player loses. So, set Playing to False to end the game.


Sub GameOver()
  ActiveDocument.Tables(1).Cell(8, 2).Select
  Selection.Text = "G"
  Beep
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "A"
  Beep
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "M"
  Beep
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "E"
  Beep
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = ""
  Beep
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = ""
  Beep
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "O"
  Beep
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "V"
  Beep
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "E"
  Beep
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "R"
  Beep
  Selection.MoveRight Unit:=wdCell, Count:=1

  Selection.EndOf wdStory
End Su
									


This routine draws GAME OVER on the game board. It is pretty self explainatory. If it is not, study the Word object model and things should clear up. If nothing else, record a bunch of macros and study the code that Word creates.


Sub NewRock()
  RockX = 5
  RX = RockX
  RockY = -2
  RockR = 0
  RR = RockR
  RockC = RockN
  RockN = Int(Rnd * 6)
  DrawNext
End Su
									


NewRock creates a new falling rock. It is centered (RockX=5) and above (RockY=-2) the game board. The rocks' rotation is normalized (RockR=0). The next rock becomes the current rock and then, a new Next rock is randomly computed and drawn on the document.


Sub EraseRock()
  For T = 0 To 3
    If RockPY(T, (7 * RockR) + RockC) + RockY > 0 Then
      ActiveDocument.Tables(1).Cell(RockPY(T, (7 * RockR) + RockC) + RockY, _
                            RockPX(T, (7 * RockR) + RockC) + RockX).Select
      Selection.Text = ""
    End If
  Next
End Su
									


To erase a rock, just select each subrocks' cell and type a null string. Of course, we need to check whether the cell is in the visible game area or not. Otherwise, VBA would flag an error.


Sub DrawRock()
  For T = 0 To 3
    If RockPY(T, (7 * RockR) + RockC) + RockY > 0 Then
      ActiveDocument.Tables(1).Cell(RockPY(T, (7 * RockR) + RockC) + RockY, _
                            RockPX(T, (7 * RockR) + RockC) + RockX).Select
      Selection.Text = "X"
    End If
  Next
End Su
									


To Draw a rock, just select each subrocks' cell and type an X. Of course, we need to check whether the cell is in the visible game area or not. Otherwise, VBA would flag an error.


Sub DrawNext()
  For X = 0 To 3
    For Y = 0 To 3
      ActiveDocument.Tables(1).Cell(6 + Y, 15 + X).Select
      Selection.Text = ""
    Next
  Next

  For T = 0 To 3
    ActiveDocument.Tables(1).Cell(6 + RockPY(T, RockN), _
                                  15 + RockPX(T, RockN)).Select
    Selection.Text = "X"
  Next
End Su
									


To draw the 'next' rock, the area where it is displayed is cleared by setting each cell equal to the null string. Then, the new 'next' rock is drawn by selecting the appropriate cell and typing an X.


Sub HoldRock()
  Selection.EndOf wdStory
  Selection.Text = " "
  Selection.Text = ""
End Su
									


HoldRock actually moves the cursor out of the game playing area. A little bit of extra code is needed to make it less visible.


Sub PutRock()
  Beep
  For T = 0 To 3
    If RockPY(T, (7 * RockR) + RockC) + RockY > 0 Then
      Board(RockPX(T, (7 * RockR) + RockC) + RockX, _
            RockPY(T, (7 * RockR) + RockC) + RockY) = True
    End If
  Next
End Su
									


PutRock is called when a rock lands. In which case, the game board needs to be updated accordingly. So, lets make a beep to signal the landing. Then, iterate through the four subrocks setting the corresponding board coordinates True. Remember, RockPX and RockPY store the subrock offsets for all seven rocks and their rotations. Don't let the math scare you, it is just simple algebra. (7*RockR)+RockC means the seven the rocks are grouped sequentially with each rotation following the next. T references each subrock. This finds the subrock offsets which is added to the rocks' board location, RockX/Y.


Function DropRock() As Boolean
  EraseRock
  DropRock = False

  For T = 0 To 3
    Y = RockPY(T, (7 * RockR) + RockC) + RockY + 1
    If Y > 0 Then
      If Board(RockPX(T, (7 * RockR) + RockC) + RockX, Y) Then
        DropRock = True
      End If
    End If
  Next

  If Not DropRock Then
    RockY = RockY + 1
  Else
    PutRock
  End If

  DrawRock
End Functio
									


DropRock moves the rock down one position. Since the rocks' position may change, it must be erased to avoid leaving trails. The return value is set to false, which means we start with the assumption that the rock didn't land. Then, iterate through the four subrocks checking whether a move down one position will cause a collision (it landed). If there is a collision, DropRock is set to True indicating the rock landed. If the rock is still falling move the rock down one position else embed the rock into the gameboard. Finally, draw the updated rock.


Sub RotateRock()
  RR = RockR + 1
  If RR > 3 Then
    RR = 0
  End If

  For T = 0 To 3
    X = RockPX(T, (7 * RR) + RockC) + RockX
    Y = RockPY(T, (7 * RR) + RockC) + RockY
    If Y > 0 Then
      If Board(X, Y) Then
        Beep
        RR = RockR
      End If
    End If
  Next
End Su
									


RotateRock does exactly what it says. This game only allows rotations in one direction and adding 1 to RockR performs this rotation. Note, RR store this value, not RockR because this sub is assigned a key and may execute at any time. So, we just want to track the rotation in this sub. RockR gets updated in the Play sub. Rotations greater than 3 don't have meaning because there is only 4 posible unique rotations. So, anything greater should loop back to 0. Now, we need to check if the rotation caused a collision. So, iterate through the four subrocks. Extract the X and Y offsets for the subrock. If Y is in the visible game area, check the board coordinates for occupation. If a subrock already exists in that coordinate, beep and unrotate the rock.


Sub RockDown()
  P = RS
End Su
									


RockDown is assigned a key which allows the player to move the rock down at full speed. This didn't work very well because the game doesn't detect if the key is held down and setting P=RS only drops the rock one position quickly.


Sub RockLeft()
  M = 1

  For T = 0 To 3
    X = RockPX(T, (7 * RockR) + RockC) + RockX - M
    Y = RockPY(T, (7 * RockR) + RockC) + RockY
    If Y > 0 Then
      If Board(X, Y) Then
        Beep
        M = 0
      End If
    End If
  Next

  RX = RockX - M
End Su
									


RockLeft moves the rock left one position. It first checks for collisions. if a collision is detected, a beep is played and M is set to 0. RockX - M computes the new location. Note, we don't need an 'If' statement here because if a collision occurred, M=0!


Sub RockRight()
  M = 1

  For T = 0 To 3
    X = RockPX(T, (7 * RockR) + RockC) + RockX + M
    Y = RockPY(T, (7 * RockR) + RockC) + RockY
    If Y > 0 Then
      If Board(X, Y) Then
        Beep
        M = 0
      End If
    End If
  Next

  RX = RockX + M
End Su
									


RockLeft moves the rock right one position. It first checks for collisions. if a collision is detected, a beep is played and M is set to 0. RockX - M computes the new location. Note, we don't need an 'If' statement here because if a collision occurred, M=0!


Sub Setup()
  Selection.WholeStory
  Selection.Delete Unit:=wdCharacter, Count:=1

  ActiveDocument.Tables.Add Range:=Selection.Range, NumRows:=16, NumColumns:=20
  ActiveDocument.Tables(1).Borders(wdBorderBottom).ColorIndex = wdDarkBlue
  ActiveDocument.Tables(1).Borders(wdBorderLeft).ColorIndex = wdDarkBlue
  ActiveDocument.Tables(1).Borders(wdBorderRight).ColorIndex = wdDarkBlue
  ActiveDocument.Tables(1).Borders(wdBorderTop).ColorIndex = wdDarkBlue
  ActiveDocument.Tables(1).Borders(wdBorderHorizontal).ColorIndex = wdWhite
  ActiveDocument.Tables(1).Borders(wdBorderVertical).ColorIndex = wdWhite

  ActiveDocument.Tables(1).Select
  Selection.Cells.SetWidth ColumnWidth:=InchesToPoints(0.2), _
                           RulerStyle:=wdAdjustNone
  Selection.Rows.SpaceBetweenColumns = InchesToPoints(0)
  Selection.Paragraphs.Alignment = wdAlignParagraphCenter

  ActiveDocument.Tables(1).Cell(16, 1).Select
  Selection.MoveRight Unit:=wdCharacter, Count:=11, Extend:=wdExtend
  Selection.Cells.Borders(wdBorderVertical).ColorIndex = wdDarkBlue
  Selection.Cells.Shading.Texture = wdTextureSolid
  Selection.Cells.Shading.ForegroundPatternColorIndex = wdDarkBlue

  ActiveDocument.Tables(1).Cell(1, 1).Select
  Selection.MoveDown Unit:=wdLine, Count:=15, Extend:=wdExtend
  Selection.Cells.Borders(wdBorderHorizontal).ColorIndex = wdDarkBlue
  Selection.Cells.Shading.Texture = wdTextureSolid
  Selection.Cells.Shading.ForegroundPatternColorIndex = wdDarkBlue

  ActiveDocument.Tables(1).Cell(1, 12).Select
  Selection.MoveDown Unit:=wdLine, Count:=15, Extend:=wdExtend
  Selection.Cells.Borders(wdBorderHorizontal).ColorIndex = wdDarkBlue
  Selection.Cells.Shading.Texture = wdTextureSolid
  Selection.Cells.Shading.ForegroundPatternColorIndex = wdDarkBlue

  ActiveDocument.Tables(1).Cell(1, 2).Select
  Selection.MoveRight Unit:=wdCharacter, Count:=9, Extend:=wdExtend
  Selection.MoveDown Unit:=wdLine, Count:=14, Extend:=wdExtend
  Selection.Font.Bold = True

  ActiveDocument.Tables(1).Cell(1, 13).Select
  Selection.Text = "W"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkRed
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "O"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkRed
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "R"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkRed
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "D"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkRed
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "T"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkRed
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "R"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkRed
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "I"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkRed
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "S"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkRed

  ActiveDocument.Tables(1).Cell(5, 15).Select
  Selection.Text = "N"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkBlue
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "E"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkBlue
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "X"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkBlue
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "T"
  Selection.Font.Bold = True
  Selection.Font.ColorIndex = wdDarkBlue
  ActiveDocument.Tables(1).Cell(6, 15).Select
  Selection.MoveRight Unit:=wdCharacter, Count:=3, Extend:=wdExtend
  Selection.MoveDown Unit:=wdLine, Count:=3, Extend:=wdExtend
  Selection.Cells.Borders(wdBorderTop).ColorIndex = wdDarkBlue
  Selection.Cells.Borders(wdBorderBottom).ColorIndex = wdDarkBlue
  Selection.Cells.Borders(wdBorderLeft).ColorIndex = wdDarkBlue
  Selection.Cells.Borders(wdBorderRight).ColorIndex = wdDarkBlue
  Selection.Font.Bold = True

  ActiveDocument.Tables(1).Cell(16, 14).Select
  Selection.Text = "S"
  Selection.Font.ColorIndex = wdDarkBlue
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "P"
  Selection.Font.ColorIndex = wdDarkBlue
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "E"
  Selection.Font.ColorIndex = wdDarkBlue
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "E"
  Selection.Font.ColorIndex = wdDarkBlue
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = "D"
  Selection.Font.ColorIndex = wdDarkBlue
  Selection.MoveRight Unit:=wdCell, Count:=1
  Selection.Text = 6 - RS
  Selection.Font.ColorIndex = wdBlack

  Selection.EndOf wdStory
  Selection.Text = vblfcr & "Coded by Robert Walsh" & _
                   vbCrLf & "MonkeyFighter.com" & vbCrLf
  Selection.ParagraphFormat.Alignment = wdAlignParagraphCenter
End Su
									


Setup draws the document. I don't plan to explain the Word specific code. I think if you study the code, you'll be able to figure it out.


Copyright © 2005, Robert Walsh, All Rights reserved.