Note About This Book: Advanced Lingo For Games was written by Gary Rosenzweig in 2000 for users of Macromedia Director 7. It is presented here for free on an as-is basis, with no updating. Most of the information and code here can be used in the most recent version of Director. The book has been reproduced from the final editing files archived in 2000, and not the final proof galleys. So some minor differences between this version and the printed version my exist. The entire contents of this book are Copyright 2000, Gary Rosenzweig. No part may be reproduced or copied without written permission. The text here is provided for individual use only.
Want to thank me for making this book available for free? Just buy Special Edition Using Macromedia Director MX and we'll call it even!

Advanced Lingo For Games
by Gary Rosenzweig


Chapter 16 Section 4

-- get character code if ascii >= chartonum("a") and ascii <= chartonum("z") then -- is it lowercase? c = numtochar(ascii-32) -- convert to uppercase end if return c end

Creating a matrix from the word list is a fairly complex process. First, a list of lists is created in the property "pMartix"{1}. There will be 15 lists of 15 characters. Each list is a row in the matrix. All the characters in each row start off as "*" characters.

Next, the "pWordList" property is initialized to an empty list. As words fit into the matrix, they are added to the word list as well.

Then, the program loops, trying to fit random words into the matrix. This is done by first picking a random word from the list{2}. Then, it picks a random direction for the word to lay in the matrix{3}. Next, a random starting spot is determined{4}, with special care that the word doesn't extrude out from the sides of the matrix.

After the handler knows which word, where it will be placed, and in what direction it will lay, it tries to fit the word into the matrix{5}. If all the characters in those positions in the matrix are "*"s, the word fits in. However, if another letter has been placed in a spot by a previous word, then the overlapping letters must be exactly the same. Otherwise, the insertion is aborted and the whole process starts over again.

As the matrix fills up with words, it becomes harder and harder to fit words in. If you have enough words, it might reach the point where some will never be able to fit. In that case the loop repeats endlessly, trying to fit the word in. To prevent this, there is a variable called "loopCount" that counts the number of times the loop repeats. After it hits a high number, the loop exits{6}. If all the words fit quickly into the matrix, this will never happen. However, if there are too many words, the loop is assured to end, even if it means excluding some of the words.

The last two things that the "on buildMatrix" handler does is replace any remaining "*"s with random letters, and sort the word list. The latter function is done so that the word list appears in alphabetical order on the screen.


-- create a matrix (list of lists) with the letters
on buildMatrix me
  -- build matrix with all * characters{1}
  pMatrix = []
  repeat with i = 1 to pMatrixSize
    temp = []
    repeat with j = 1 to pMatrixSize
      add temp, "*"
    end repeat
    add pMatrix, temp
  end repeat
  
  -- get list of words
  list = getWords(me)
  pWordList = []
  
  loopCount = 0
  
  -- loop until all words are used
  repeat while list.count > 0
    -- get random word{2}
    w = list[random(list.count)]
    
    -- pick random direction{3}
    horizPlace = random(3)-2 -- horizontal direction
    vertPlace = random(3)-2 -- vertical direction
    
    -- check for no direction at all
    if horizPlace = 0 and vertPlace = 0 then next repeat
    
    -- pick a random starting spot{4}
    x = random(pMatrixSize-abs(horizPlace)*(w.length+1))
    y = random(pMatrixSize-abs(vertPlace)*(w.length+1))
    
    -- add to position if the word is to be placed backwards
    if horizPlace = - 1 then x = x + w.length-1
    if vertPlace = - 1 then y = y + w.length-1
    
    -- see if the word fits in that spot{5}
    ok = TRUE
    repeat with i = 1 to w.length
      -- get this letter
      letter = pMatrix[y+vertPlace*(i-1)][x+horizPlace*(i-1)]
      -- see if this letter will overlap another
      if (letter <> "*") and (letter <> w.char[i]) then ok = FALSE
    end repeat
    
    -- if it will fit, then add
    if ok then
      repeat with i = 1 to w.length
        -- set letter in matrix
        pMatrix[y+vertPlace*(i-1)][x+horizPlace*(i-1)] = w.char[i]
      end repeat
      -- remove word from list
      deleteOne list, w
      -- add word to word list
      add pWordList, w
    end if
    
    -- limit loops to avoid words that can never fit in
    loopCount= loopCount + 1
    if loopCount > 10000 then exit repeat{6}
  end repeat
  
  -- replace all *s with random letters
  repeat with y = 1 to pMatrixSize
    repeat with x = 1 to pMatrixSize
      if pMatrix[y][x] = "*" then
        pMatrix[y][x] = numtochar(64+random(26))
      end if
    end repeat
  end repeat
  
  -- alphabetize word list
  sort pWordList
end

After the matrix is complete, it needs to be drawn on the screen. The matrix is drawn by building a string from the contents of the "pMatrix" list. A space is placed between every letter, so that the width of the matrix is doubled, which matches the height of the matrix a little closer, making it close to square.

The handler also sets the fixedLineSpace property of the text member to 14, and the color of all the letters to black.

The text member property fixedLineSpace is a somewhat unknown and unused Lingo property. However, it's the only way to set the line spacing of a text member in Lingo. This is especially useful for making sure that line spacing is consistent cross-platform.


-- take matrix list and populate text member
on showMatrix me
  text = ""
  repeat with y = 1 to pMatrixSize
    repeat with x = 1 to pMatrixSize
      put pMatrix[y][x]&SPACE after text
    end repeat
    put RETURN after text
  end repeat
  
  -- set member text, line spacing and color
  member("Matrix").text = text
  member("Matrix").fixedLineSpace = 14
  member("Matrix").color = rgb("000000")
end

Another element that needs to be shown on the screen is the word list. The following handler put the contents of the "pWordList" property into a text member. As the player finds words, this list is redrawn to show only those words that remain.


-- put word list into text member
on showWordList me
  text = ""
  repeat with i = 1 to pWordList.count
    put pWordList[i]&RETURN after text
  end repeat
  member("Word Display").text = text
end

From here on, all the handlers deal with the player's interaction with the matrix. The player's only move in this game is to select a consecutive group of letters in the matrix. To start this off, the player positions the cursor over the first letter and presses down on the mouse button.

When this happens, the on mouseDown handler determines which character the player has selected, and sets both the "pFirstChar" and "pLastChar" to a list that contains the horizontal and vertical position of that character.


-- begin selection
on mouseDown me
  c = getChar(me,the clickLoc)
  -- make sure it is a valid selection
  if c = 0 then exit
  -- activate selection process
  pFirstChar = c
  pLastChar = c
end

To determine which character the mouse is located over, the following handler first figures out how far apart the characters are in the matrix. The horizontal distance is calculated by getting the position of the third character in the text member. By using charpostoloc to do this, we can calculate the distance between the first and third characters, which is the same as the distance between any two characters. Because the characters in the matrix have a space between them, this distance is the same as the distance between letters in the matrix. The vertical position is much more simply defined by the fixedLineSpace property of the text member.

After we know the horizontal and vertical spacing of the letters, we can take the mouse position and return a list with the horizontal and vertical position of the letter that the user is pointing to.

Experienced Lingo programmers will ask why I'm not using mouseChar or pointToChar here. Both of these functions return the character under the mouse, but neither return the horizontal and vertical position of the character in the matrix without many more lines of code.


-- get character position from cursor position
on getChar me, loc
  -- get width and height of letter in grid
  w = charpostoloc(sprite(me.spriteNum).member,3).locH
  h = sprite(me.spriteNum).member.fixedLineSpace
  -- remove offset of sprite
  loc = loc - sprite(me.spriteNum).loc
  -- calculate location
  x = loc.locH/w
  y = loc.locV/h
  return [x,y]
end

The on exitFrame handler has the task of determining the current selection and highlighting it. The "pFirstChar" property stays the same, but the "pLastChar" property is constantly rechecked every frame to determine the current selection. After we have a "pFirstChar" and a "pLastChar", we need to be sure the selection is valid.

The only valid selections are the ones that are completely horizontal, vertical, or diagonal. By diagonal, we mean a 45-degree angle in this case. If the selection is valid, then "on drawLine" is called to show the selection.


-- show selection if needed
on exitFrame me
  if not voidP(pFirstChar) then
    -- get current character under cursor
    c = getChar(me,the mouseLoc)
    -- assume a valid selection
    ok = FALSE
    -- horizontal selection
    if c[1] = pFirstChar[1] then ok = TRUE
    -- vertical selection
    if c[2] = pFirstChar[2] then ok = TRUE
    -- diagonal selection
    if abs(c[1]-pFirstChar[1]) = abs(c[2]-pFirstChar[2]) then ok = TRUE
    -- if not a valid selection, change nothing
    if not ok then exit
    -- set a new last character in selection
    pLastChar = c
    -- highlight selection
    drawLine(me)
  end if
end

Even though highlighting the selection is little more than a detail in this game, the following handler is the most complex. There are actually many ways that you could highlight the selection. In a word search game that I have written for my site, I use vector shapes to draw an oval around the selection. This is even more involved than the following handler, which uses a line shape sprite.

The "on drawLine" handler first gets the horizontal and vertical spacing of the letters in the matrix in the same way that the "on getChar" handler did. It then uses these values to determine the starting and ending screen points.

The line sprite can be a horizontal, a vertical or a diagonal line. As it turns out, a diagonal line appears to be thicker when used as a background to letters. To compensate, the line is thinner when a diagonal is needed{7}. It is 65 percent of the size of a horizontal or vertical line, to be exact.

Adjustments also need to be made to the start and end points of the line so that it covers the letters when it is positioned at different angles{8}. These adjustments can be determined with logic, but I used trial and error to get them just right.

Finally, the line sprite is set into place. The direction of the line is set so that the proper type of line is drawn in the proper situation. Sometimes an upper left to lower right line is needed, and sometimes an upper right to lower left line is needed{9}.

Every sprite in Director is defined by the rectangle of its bounding box. In the case of rectangle shapes and bitmaps, this is easy to see. However, in the case of line shapes, the bounding box rectangle does not tell the whole picture. Two lines can share exactly the same rectangle, but one can be drawn from the upper left to the lower right and the other from the upper right to the lower left. In Lingo, this difference is indicated by the lineDirection property.


-- draw the selection line
on drawLine me
  -- get the width of the grid
  w = charpostoloc(sprite(me.spriteNum).member,3).locH
  -- get the height of the grid
  h = sprite(me.spriteNum).member.fixedLineSpace
  
  -- get the basic location of the two ends of the line
  p1 = point(pFirstChar[1]*w,pFirstChar[2]*h)
  p2 = point(pLastChar[1]*w,pLastChar[2]*h)
  
  -- add location of sprite
  p1 = p1 + sprite(me.spriteNum).loc
  p2 = p2 + sprite(me.spriteNum).loc
  
  -- smaller line size if at an angle{7}
  if (p1.locH = p2.locH) or (p1.locV = p2.locV) then
    lineSize = w
  else
    lineSize = .65*w
  end if
  
  -- adjust the line according to the angle{8}
  if (p2.locH >= p1.locH) and (p2.locV >= p1.locV) then
    p2 = p2 + point(w,h)
  else if (p2.locH < p1.locH) and (p2.locV >= p1.locV) then
    p1 = p1 + point(w,0)
    p2 = p2 + point(0,h)
  else if (p2.locH < p1.locH) and (p2.locV < p1.locV) then
    p1 = p1 + point(w,h)
  else if (p2.locH >= p1.locH) and (p2.locV < p1.locV) then
    p1 = p1 + point(0,h)
    p2 = p2 + point(w,0)
  end if
  
  -- adjust line slightly
  sprite(me.spriteNum-1).rect = rect(p1,p2) - rect(3,2,3,2)
  
  -- set line direction{9}
  if ((p1.locH < p2.locH) and (p1.locV < p2.locV)) or ((p1.locH > p2.locH) and (p1.locV > p2.locV)) then
    sprite(me.spriteNum-1).member.lineDirection = 0
  else
    sprite(me.spriteNum-1).member.lineDirection = 1
  end if
  
  -- set line size
  sprite(me.spriteNum-1).lineSize = lineSize
end

When the player lifts up the mouse button, "pFirstChar" and "pLastChar" are used to determine what the current selection actually is. There is no need to recalculate the "pLastChar" again, because it was just updated by the last on exitFrame handler.

The "on compileSelection" handler is used to build a string from the selection points. Then, the "on select" handler is called to determine if the word matches one in the list. If it does, then the "on grayLetters" handler is called to permanently color those letters in. Regardless, the selection line is removed from the screen, and "pFirstChar" and "pLastChar" are reset.


-- end selection
on mouseUp me
  if not voidP(pFirstChar) then
    -- get word from selection
    text = compileSelection(me,pFirstChar,pLastChar)
    -- see if it is a word in the list
    if select(me,text) then
      -- change color of letters in word
      grayLetters(me)
    end if
    -- remove selection line
    sprite(me.spriteNum-1).locV = -1000
    pFirstChar = VOID
    pLastChar = VOID
  end if
end

The following on mouseUpOutside handler is used to redirect any "mouseUp" messages to the on mouseUp handler regardless of whether the cursor is still over the matrix when released. This might occur if the player stretches the selection beyond the boundaries of the matrix sprite.


-- send ALL mouseUps to same place
on mouseUpOutside me
  mouseUp(me)
end

The next handler, "on compileSelection" takes two selection points and compiles a string of characters from them. It does this by determining the direction of the selection as horizontal and vertical differences. Then, the "on compileSelection" handler moves from the first to the last character and adds each letter to the list.


-- take a first and last character and compile word
on compileSelection me, c1, c2 
  -- determine difference between start and end
  dx = c2[1] - c1[1]
  dy = c2[2] - c1[2]
  
  -- determine line direction
  if dx <> 0 then dx = abs(dx)/dx
  if dy <> 0 then dy = abs(dy)/dy
  
  text = ""
  -- loop through characters
  c = c1
  repeat while TRUE
-- see if this is past the edge of the puzzle
    if c[1] < 0 or c[1] > pMatrixSize-1[cc]
       or c[2] < 0 or c[2] > pMatrixSize-1 then exit repeat
    -- add character to text
    put pMatrix[c[2]+1][c[1]+1] after text
    -- see if this is the last character
    if c = c2 then exit repeat
    -- next character
    c = c + [dx,dy]
  end repeat
  return text
end

The "on select" handler takes a word and determines if it is in the word list. If so, the handler removes the word from the word list, and redisplays the word list. Then, it checks to see if all words have been found. Either way, it returns TRUE only if a word from the list was found.


-- try out a player's selection
on select me, text
  -- see if it is in the word list
  if getOne(pWordList,text) then
    -- remove from list
    deleteOne pWordList, text
    showWordList(me)
    -- see if the game is over
    if pWordList.count < 1 then
      go to frame pEndGameFrame
    else
      return TRUE
    end if
  end if
end

When a word is found, the letters in the text member are changed to a different color to signify that they have been used in a word. The "on grayLetters" handler actually resembles the "on compileSelection" handler quite a bit. It uses the first and last character position, determines a direction, and then loops through the characters. Instead of compiling them into a string, it turns them all gray.


-- change letters in selection to different color
on grayLetters me
  -- determine difference between start and end
  dx = pLastChar[1] - pFirstChar[1]
  dy = pLastChar[2] - pFirstChar[2]
  
  -- determine line direction
  if dx <> 0 then dx = abs(dx)/dx
  if dy <> 0 then dy = abs(dy)/dy
  -- loop through characters
  c = pFirstChar
  repeat while TRUE
    -- change color
    member("Matrix").line[c[2]+1].char[2*(c[1]+1)-1].color = rgb("999999")
    -- see if the last character has been reached
    if c = pLastChar then exit repeat
    -- next character
    c = c + [dx,dy]
  end repeat
end

In addition to the sprite behavior, there is a simple frame behavior used to keep the frame looping while the game is being played.


on exitFrame
  go to the frame
end