Friday 31 March 2017

Scrabblet: The Source Code - Java Tutorials ( Page 3 of 6 )

addLetter( )
The addLetter( ) method is used to place a letter on the tray. The letter is placed in the first slot that is empty. If the method can’t find an empty slot, it returns false.

    synchronized boolean addLetter(Letter l) {
      for (int i = 0; i < 7; i++) {
        if (tray[i] == null) {
          tray[i] = l;
          moveLetter(l, i, 15);
          return true;
        }
      }
      return false;
    }

existingLetterAt( )
The private method existingLetterAt( ) is used to check a board position to see if it has a letter in it that is not currently in play. This is used by findwords( ) next to make sure that at least one letter in a turn is touching an already existing letter.

    private boolean existingLetterAt(int x, int y) {
      Letter l = null;
      return (x >= 0 && x <= 14 && y >= 0 && y <= 14
        && (l = board[y][x]) != null && l.recall() == null);
    }

findwords( )
findwords( ) is a very large method used to examine the state of the board for a legal turn. If the rules for letter placement are broken, then null is returned. If no tiles were in play, then ““ is returned. If all of the tiles played in this turn are legal, then the list of words they formed is returned as a string containing the space-separated words. The instance variables turn_score and total_score are updated to reflect the value of the words that were just played.

First findwords( ) counts the tiles at play, ntiles, storing them in a separate array called atplay. Next, it looks at the first two tiles (if more than one was played) to determine if they are vertically or horizontally oriented. Then it inspects all of the other tiles at play, to make sure they are along the same line. If any of the tiles is out of that row or column, the method returns null.

    synchronized String findwords() {
      String res = "";
      turn_score = 0;

      int ntiles = 0;
      Letter atplay[] = new Letter[7];
      for (int i = 0; i < 7; i++) {
        if (tray[i] != null && tray[i].recall() != null) {
          atplay[ntiles++] = tray[i];
        }
      }
      if (ntiles == 0)
        return res;

      boolean horizontal = true; // if there's one tile,
                                 // call it horizontal
      boolean vertical = false;
      if (ntiles > 1) {
        int x = atplay[0].x;
        int y = atplay[0].y;
        horizontal = atplay[1].y == y;
        vertical = atplay[1].x == x;
        if (!horizontal && !vertical) // diagonal...
          return null;
        for (int i = 2; i < ntiles; i++) {
          if (horizontal && atplay[i].y != y
            || vertical && atplay[i].x != x)
            return null;
        }
      }

Next, it looks at each tile to be sure that at least one of them is touching an existing tile on one of its four sides. A special case is made for the beginning of the game: if the center tile is covered and more than one tile is played, it is legal.

      // make sure that at least one played tile is
      // touching at least one existing tile.
      boolean attached = false;
      for (int i = 0; i < ntiles; i++) {
        Point p = atplay[i].recall();
        int x = p.x;
        int y = p.y;
        if ((x == 7 && y == 7 && ntiles > 1) ||
            existingLetterAt(x-1, y) || existingLetterAt(x+1, y) ||
            existingLetterAt(x, y-1) || existingLetterAt(x, y+1)) {
          attached = true;
          break;
        }
      }
      if (!attached) {
        return null;
      }

This next loop iterates over every letter in the main word, (i == –1), then comes back again for each letter (i == 0..ntiles), which might also create a word orthogonal to the main direction, which is managed via horizontal.

      // we use -1 to mean check the major direction first
      // then 0..ntiles checks for words orthogonal to it.
      for (int i = -1; i < ntiles; i++) {
        Point p = atplay[i==-1?0:i].recall(); // where is it?
        int x = p.x;
        int y = p.y;

        int xinc, yinc;
        if (horizontal) {
          xinc = 1;
          yinc = 0;
        } else {
          xinc = 0;
          yinc = 1;
        }
        int mult = 1;

        String word = "";
        int word_score = 0;

The method then picks each tile and moves left or up from it to find the first tile in each word. Once at the beginning of the word, it moves right or down from it, inspecting every letter. It counts the letters in letters_seen. For each letter, it determines the point contribution based on the bonus multiplier beneath it. If the square is played for the first time, the multiplier value is applied; otherwise the tile is counted at face value. This score is accumulated in word_score.

      // here we back up to the top/left-most letter
      while (x >= xinc && y >= yinc &&
             board[y-yinc][x-xinc] != null) {
        x -= xinc;
        y -= yinc;
      }

      int n = 0;
      int letters_seen = 0; // letters we've just played.
      Letter l;
      while (x < 15 && y < 15 && (l = board[y][x]) != null) {
        word += l.getSymbol();
        int lscore = l.getPoints();
        if (l.recall() != null) { // one we just played...
          Color t = tiles[y < 8 ? y : 14 - y][x < 8 ? x : 14 - x];
          if (t == w3)
            mult *= 3;
          else if (t == w2)
            mult *= 2;
          else if (t == l3)
            lscore *= 3;
          else if (t == l2)
            lscore *= 2;
          if (i == -1) {
            letters_seen++;
          }
        }
        word_score += lscore;
        n++;
        x += xinc;
        y += yinc;
      }
      word_score *= mult;

One last error check is done on the main word only. Since the loop ends whenever it hits a blank square or the edge of the board, it should cover all of the freshly played tiles, as well as some previously played ones. If it sees fewer tiles, then there must have been a gap in them, which is an illegal position, so it returns null. If that test is passed, it checks to see if all seven tiles were played, awarding a 50-point bonus if they were. After inspecting the main word, findwords( ) inverts the sense of horizontal and looks for orthogonal words on the subsequent passes.

      if (i == -1) { // first pass...

        // if we didn't see all the letters, then there was a gap,
        // which is an illegal tile position.
        if (letters_seen != ntiles) {
          return null;
        }

        if (ntiles == 7) {
          turn_score += 50;
        }

        // after the first pass, switch to looking the other way.
        horizontal = !horizontal;
      }

As findwords( ) walks across the word, it needs to make sure that it only scores letters that form at least two-letter words. In this case, it adds the word_score to the turn_score and appends this word to the result string. Once all of the letters have been inspected, it totals the score and returns.

        if (n < 2) // don't count single letters twice.
            continue;

        turn_score += word_score;
        res += word + " ";
      }
      total_score += turn_score;
      return res;
    }

commit( ) and commitLetter( )
The commit( ) and commitLetter( ) methods commit the letters that were tentatively placed on the board. These letters are removed from the tray and painted in a darker color on the board. As each letter is committed, commit( ) notifies the server of the position of each letter by calling move( ) so that the opponent’s board can be updated.

    synchronized void commit(ServerConnection s) {
      for (int i = 0 ; i < 7 ; i++) {
        Point p;
        if (tray[i] != null && (p = tray[i].recall()) != null) {
          if (s != null) // there's a server connection
            s.move(tray[i].getSymbol(), p.x, p.y);
          commitLetter(tray[i]); // marks this as not in play.
          tray[i] = null;
        }
      }
    }

    void commitLetter(Letter l) {
      if (l != null && l.recall() != null) {
        l.paint(offGraphics, Letter.DIM);
        l.remember(null); // marks this as not in play.
      }
    }

update( ) and paint( )
Many private variables are declared here to provide easy access to the dimensions of the board. This code also declares two offscreen buffers, one to be used as the image of the board and all of the permanently set tiles and another to use as a double buffer for the display. The update( ) method simply calls paint( ) to avoid flicker. The paint( ) method makes a quick call to checksize( ) to make sure all of the buffers have been created, then checks to see if we are dragging a letter around by means of pick != null. If so, then paint( ) makes a copy of the offscreen graphics context and clips it to the bounds of the letter it is painting, x0, y0, w0, h0. Next, it clips the onscreen graphics context to the same rectangle. This will minimize the number of pixels it will have to move for each move of the mouse.

To paint, we copy the background image, offscreen, then call paint on each letter in the tray with the setting of NORMAL. We paint the letter we are dragging around in the BRIGHT mode. Finally, we copy the double buffer image, offscreen2, to the screen.

    private Letter pick; // the letter being dragged around.
    private int dx, dy; // offset to topleft corner of pick.
    private int lw, lh; // letter width and height.
    private int tm, lm; // top and left margin.
    private int lt; // line thickness (between tiles).
    private int aw, ah; // letter area size.

    private Dimension offscreensize;
    private Image offscreen;
    private Graphics offGraphics;
    private Image offscreen2;
    private Graphics offGraphics2;

    public void update(Graphics g) {
      paint(g);
    }

    public synchronized void paint(Graphics g) {
      Dimension d = checksize();
      Graphics gc = offGraphics2;
      if (pick != null) {
        gc = gc.create();
        gc.clipRect(x0, y0, w0, h0);
        g.clipRect(x0, y0, w0, h0);
      }
      gc.drawImage(offscreen, 0, 0, null);

      for (int i = 0 ; i < 7 ; i++) {
        Letter l = tray[i];
        if (l != null && l != pick)
          l.paint(gc, Letter.NORMAL);
      }
      if (pick != null)
        pick.paint(gc, Letter.BRIGHT);

      g.drawImage(offscreen2, 0, 0, null);
    }

LetterHit( )
LetterHit( ) returns the letter that is under the point x,y and returns null if no letter is there.

    Letter LetterHit(int x, int y) {
      for (int i = 0; i < 7; i++) {
        if (tray[i] != null && tray[i].hit(x, y)) {
          return tray[i];
        }
      }
      return null;
    }

unplay( )
This simple method removes a letter from play that was placed on the board but was not yet committed.

    private void unplay(Letter let) {
      Point p = let.recall();
      if (p != null) {
        board[p.y][p.x] = null;
        let.remember(null);
      }
    }

No comments:

Post a Comment