Not participating in the hype around Wordle seemed wrong, and what better way to do it than to create it cleanly at compile time in C++20? Introducing Wordlexpr!
Let’s figure out how this magic works!
Overview
Wordlexpr is executed entirely at compile time – no executable is created, and the game itself is played through compiler errors. This requires three key tasks:
- Outputting random text to compiler diagnostic messages.
- Generating random numbers at compile time.
- Saving state and tracking player progress between compilations.
Error as a new printf
To get the compiler to print arbitrary strings, let’s start with the simplest way, static_assert:
cpp
static_assert(false, “Welcome to Wordlexpr!”);
The compiler will output:
vbnet
error: static assertion failed: Welcome to Wordlexpr!
However, static_assert accepts only string literals, and you cannot use constexpr character array or const char*:
cpp
constexpr const char* msg = “Welcome to Wordlexpr!”;
static_assert(false, msg); // Error!
Using templates
Let’s try to encode the string into a template type:
cpp
template struct print;
print<'H', 'e', 'l', 'l', 'o'> _{};
The compiler will output:
pgsql
error: variable 'print<'H', 'e', 'l', 'l', 'o'> _' has initializer but incomplete type
Now we can pass characters to print, but this method is inconvenient.
C++20 with support for classes in non-typical template parameters (P0732R2) will help us:
cpp
struct ct_str {
char _data[512]{};
std::size_t _size{0};
template <std::size_t N>
constexpr ct_str(const char (&str)[N]) : _size{N - 1} {
for (std::size_t i = 0; i < _size; ++i)
_data[i] = str[i];
}
};
template struct print;
print<“Welcome to Wordlexpr!”> _{};
The compiler now displays:
go
error: variable 'print _' has initializer but incomplete type
So we get the ability to output text through compiler errors and even change strings at compile time.
Generating random numbers at compile time
Random number generation is deterministic if we use a predefined seed value passed through the preprocessor:
sh
g++ -std=c++20 ./wordlexpr.cpp -DSEED=123
Instead of complex algorithms, you can use the simplest remainder from division:
cpp
constexpr const ct_str& get_target_word() {
return wordlist[SEED % wordlist_size];
}
The second step is solved!
Storing state and progress
Since we pass seed through the preprocessor, why not use a similar scheme to store the current state? The player passes the state as a string:
sh
g++ -std=c++20 ./wordlexpr.cpp -DSEED=123 -DSTATE=DJYHULDOPALISHJRBFJNSWAEIM
The state is encoded and output after each move:
rust
error: variable 'print<ct_str{"Your attempts:
1. crane → x-xx-.
2. white → xxox-.
3. black → xoxxx.
4. tower → xxxoo.
To continue, use -DSTATE=EJYHULDOPALISHJRAVDLYWAEIM”, 242}> _' has initializer but incomplete type
A structure is used to store the state:
cpp
struct state {
std::size_t _n_guesses{0};
ct_str _guesses[5];
};
And encoding/decoding is implemented like this:
cpp
constexpr ct_str encode_state(const state& s);
constexpr state decode_state(const ct_str& str);
Wordlexpr uses Caesar cipher to hide information about current attempts.
Conclusion
Wordlexpr is an experiment with C++20 features, allowing you to play Wordle at compile time. Everyone is welcome to try it out in Compiler Explorer and explore the source code. If you have any questions, feel free to ask!