Home

A complete example: Crossword helper

A complete example: Crossword helper

Let's now implement a complete, useful program that can help us solve crossword puzzles. You type in a pattern—a word, where some letters have been replaced by question marks— and it displays all the English words that match this pattern.

Here is what it should look like:

Screenshot of Crossword

And here is the complete code for crossword1.scala:
import scala.swing._
import scala.swing.event._

class Solver {
  private val words = scala.io.Source.fromFile("words.txt").getLines().toSet

  private def matches(pattern: String, word: String): Boolean = {
    if (word.length != pattern.length)
      return false
    for (i <- 0 until word.length) {
      val p = pattern(i)
      if (p != '?' && p != word(i))
	return false
    }
    true
  }

  def findWords(pattern: String): List[String] = {
    var w = List[String]()
    for (e <- words) {
      if (matches(pattern, e))
	w = e :: w
    }
    w
  }
}

class UI(val solver: Solver) extends MainFrame {
  private def restrictHeight(s: Component) {
    s.maximumSize = new Dimension(Short.MaxValue, s.preferredSize.height)
  }

  title = "Crossword Puzzle Helper"
  val searchField = new TextField { columns = 32 }
  val searchButton = new Button("Search")
  val searchLine = new BoxPanel(Orientation.Horizontal) {
    contents += searchField
    contents += Swing.HStrut(20)
    contents += searchButton
  }
  val resultField = new TextArea { 
    rows = 10
    lineWrap = true
    wordWrap = true
    editable = false
  }

  // make sure that resizing only changes the resultField:
  restrictHeight(searchLine)
  
  contents = new BoxPanel(Orientation.Vertical) {
    contents += searchLine
    contents += Swing.VStrut(10)
    contents += new ScrollPane(resultField)
    border = Swing.EmptyBorder(10, 10, 10, 10)
  }

  listenTo(searchField)
  listenTo(searchButton)
  reactions += {
    case EditDone(`searchField`) => searchNow()
    case ButtonClicked(`searchButton`) => searchNow()
  }
  
  def searchNow() {
    val pattern = searchField.text.toLowerCase
    val words = solver.findWords(pattern)
    if (words.length == 0) {
      resultField.text = "\n\nSorry, no words found."
    } else {
      resultField.text = words.sorted mkString "\n"
      resultField.caret.position = 0
    }
  }
}

object Crossword {
  def main(args: Array[String]) {
    val solver = new Solver
    val ui = new UI(solver)
    ui.visible = true
  }
}

Note that the output area (the resultField) is read-only: its contents cannot be changed by the user. This is achieved by setting its editable attribute.

After filling the output area with a long list of words, the cursor (called caret in the Swing documentation) is at the end of the text. I prefer the display to show the beginning of the list, so I move the caret to the beginning using the line:

      resultField.caret.position = 0

Note that I have completely separated the logical part of the program (the Solver) from the user interface part (the UI). This is a good strategy for writing maintainable and reusable code.

There is a slightly improved version of this program in crossword2.scala. It has buttons to change the font size and a selector to switch between different word files.