Week 39 · Phase 5 — The Builds
A 15×15 board, 100 tiles, and a real word list. Where the game gets serious.
Tic-tac-toe was 9 cells, 3 marks, 8 winning lines. Scrabble is 225 cells, 100 tiles, 27 letter values, premium squares, a 270,000-word dictionary, and a turn that requires you to spell a real word that connects to the existing tiles. The complexity is an order of magnitude bigger and the data structures matter. This is the project where you start to feel like a programmer, not someone copying tutorials.
This week we build the static skeleton: the board, the bag, the rack, the score table, and a turn that places tiles and computes a score. Next week we tackle the dictionary — looking up words quickly enough that the program doesn't grind to a halt.
That last point — "newly placed tiles only get the premium" — is the rule that catches every first-time implementation. Premium squares are consumed once and only once.
Every cell on the board has two pieces of state: the tile placed there (if any), and the premium type baked into the square. The premium is part of the board, fixed at the start; the tile is mutable.
enum class Premium { None, DoubleLetter, TripleLetter, DoubleWord, TripleWord };
struct Tile {
char letter; // 'A'..'Z', or ' ' for a blank used as letter X
char assigned; // for blanks: which letter the player chose
};
struct Square {
Premium premium;
std::optional<Tile> tile;
};
class Board {
public:
Board(); // initialise premiums
bool empty_at(int r, int c) const;
char letter_at(int r, int c) const; // '\0' if empty
Premium premium_at(int r, int c) const;
void commit(int r, int c, Tile t);
private:
Square grid_[15][15];
};
std::optional<Tile> is the C++17 idiomatic way to say "this might or might not have a value" — exactly what we want for a square that may or may not have a tile. The constructor walks the grid once and sets each square's premium from a hardcoded layout (the standard Scrabble premium pattern is symmetric, so a single 8×8 quadrant repeated four times is enough).
The bag is 100 tiles in known proportions. The simplest representation is a std::vector<Tile> that you shuffle once at the start, then pop from the back as players draw.
class Bag {
public:
Bag() {
// distribution from the standard English Scrabble set
struct Entry { char letter; int count; };
Entry dist[] = {
{'A',9},{'B',2},{'C',2},{'D',4},{'E',12},{'F',2},
{'G',3},{'H',2},{'I',9},{'J',1},{'K',1},{'L',4},
{'M',2},{'N',6},{'O',8},{'P',2},{'Q',1},{'R',6},
{'S',4},{'T',6},{'U',4},{'V',2},{'W',2},{'X',1},
{'Y',2},{'Z',1},{' ',2} // blanks
};
for (auto& e : dist)
for (int i = 0; i < e.count; i++)
tiles_.push_back({e.letter, 0});
std::random_device rd;
std::shuffle(tiles_.begin(), tiles_.end(), std::mt19937(rd()));
}
bool empty() const { return tiles_.empty(); }
std::optional<Tile> draw() {
if (empty()) return std::nullopt;
Tile t = tiles_.back(); tiles_.pop_back(); return t;
}
private:
std::vector<Tile> tiles_;
};
std::shuffle with a Mersenne Twister gives you a properly random ordering. Drawing is just popping from the back; the bag knows when it's empty.
The rack is just std::vector<Tile> with size ≤ 7. After every turn the player refills from the bag until either the rack is full or the bag is empty.
Letter values are a fixed table — A is 1, Z is 10, blanks are 0:
int letter_value(char c) {
static const int v[26] = {
1,3,3,2,1,4,2,4,1,8,5,1,3, // A..M
1,1,3,10,1,1,1,1,4,4,8,4,10 // N..Z
};
if (c == ' ') return 0; // blank: zero points regardless of assigned letter
return v[c - 'A'];
}
This is the part that matters. A move places one or more tiles in a single line (across or down). The score for the move is:
Below is a simplified score for one word direction; the full implementation calls this once for the main word and once per cross-word:
struct Placement { int r, c; char letter; bool is_new; };
int score_word(const Board& b, const std::vector<Placement>& word) {
int letters = 0;
int word_mult = 1;
for (auto& p : word) {
int v = letter_value(p.letter);
if (p.is_new) {
switch (b.premium_at(p.r, p.c)) {
case Premium::DoubleLetter: v *= 2; break;
case Premium::TripleLetter: v *= 3; break;
case Premium::DoubleWord: word_mult *= 2; break;
case Premium::TripleWord: word_mult *= 3; break;
default: break;
}
}
letters += v;
}
return letters * word_mult;
}
The is_new flag is the whole game. A premium square already covered by an old tile contributes nothing extra. A double-letter under a new tile doubles that tile only. Two triple-words spanned by one move multiply the word total by 9.
Before any of this scoring runs, you have to confirm the move is even legal:
The first two are spatial checks against the board. The third is the dictionary problem, and it's where Scrabble gets interesting computationally. A single 7-letter move can form up to eight different words simultaneously — main word plus seven cross-words — and you need to look each one up. Doing that with a slow lookup means a noticeable pause every turn. That's next week.
The skeleton you'll write this week uses everything from Phases 2–4: classes with invariants (Board, Bag), std::vector as workhorse, std::optional for "may or may not," random shuffling, score tables, and the multi-axis logic of placing pieces in 2D. There's no AI yet; the program is a referee that two humans take turns operating. Even at this stage it's substantially more code than tic-tac-toe — roughly 400 lines for a clean minimal version — and you'll feel why classes exist. Without them you'd drown in global arrays and helper functions named get_premium2.
Tic-tac-toe needed three concepts. Scrabble needs ten. Chess will need thirty. The art is keeping each one small.
Board::Board() by hardcoding the 15×15 premium pattern. Print the board with letters where tiles sit and small symbols (·, d, t, D, T) for empty premiums.Right now, dictionary validation is missing — the player can spell QXVZJ and we'll happily score it. Next week we add a dictionary lookup that runs in microseconds, even with 270,000 words. The trick is the hash map from Week 31 — and, as a bonus, sorting the letters of a word turns "what words can I spell with these tiles?" into a lookup, not a search.
Week 40 is Scrabble Lookup — the dictionary problem.
Answer: A. The premium is consumed by the first tile to land on it. It's the rule beginners forget when scoring.
Answer: A. Bingos make competitive Scrabble what it is. Without them, the game becomes much less swingy.
Answer: B. Sentinel values (like 'tile = -1 means empty') are a classic source of bugs. optional makes the absence explicit.