wordle: minimally working game

A few "minor" features are missing, including showing the correct word if you
get it wrong.
This commit is contained in:
Zack Buhman 2023-05-10 10:15:48 -07:00
parent cfad16c514
commit e15f0a9eaf
11 changed files with 13195 additions and 66 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ res/mai.data
tools/ttf-convert
common/keyboard.cpp
common/keyboard.hpp
wordle/word_list.hpp

View File

@ -62,6 +62,11 @@ smpc/input_keyboard.elf: smpc/input_keyboard.o sh/lib1funcs.o res/dejavusansmono
wordle/main_saturn.o: common/keyboard.hpp
wordle/word_list.hpp: wordle/word_list.csv wordle/word_list.py
python wordle/word_list.py > $@
wordle/wordle.o: wordle/word_list.hpp
wordle/wordle.elf: wordle/main_saturn.o wordle/wordle.o wordle/draw.o sh/lib1funcs.o res/dejavusansmono.font.bin.o common/keyboard.o common/draw_font.o common/palette.o
@ -74,4 +79,4 @@ clean-sh:
-regextype posix-egrep \
-regex '.*\.(iso|o|bin|elf|cue)$$' \
-exec rm {} \;
rm -f common/keyboard.cpp common/keyboard.hpp
rm -f common/keyboard.cpp common/keyboard.hpp wordle/word_list.hpp

View File

@ -56,7 +56,8 @@ uint32_t single_character_centered(state const& s,
const int32_t x1, // in 26.6 fixed point
const int32_t y1,
const int32_t x2,
const int32_t y2)
const int32_t y2,
uint16_t color)
{
//assert(c <= s.char_code_offset);
const T c_offset = c - s._font->char_code_offset;
@ -64,12 +65,10 @@ uint32_t single_character_centered(state const& s,
glyph_bitmap const& bitmap = s._glyphs[c_offset].bitmap;
glyph_metrics const& metrics = s._glyphs[c_offset].metrics;
constexpr uint16_t magenta = (0x31 << 10) | (0x31 << 0);
vdp1.vram.cmd[cmd_ix].CTRL = CTRL__JP__JUMP_NEXT | CTRL__COMM__POLYLINE;
vdp1.vram.cmd[cmd_ix].LINK = 0;
vdp1.vram.cmd[cmd_ix].PMOD = PMOD__ECD | PMOD__SPD;
vdp1.vram.cmd[cmd_ix].COLR = COLR__RGB | magenta; // non-palettized (rgb15) color data
vdp1.vram.cmd[cmd_ix].COLR = COLR__RGB | color; // non-palettized (rgb15) color data
vdp1.vram.cmd[cmd_ix].XA = x1;
vdp1.vram.cmd[cmd_ix].YA = y1;
vdp1.vram.cmd[cmd_ix].XB = x2;

View File

@ -47,7 +47,7 @@ void main()
vdp1.vram.cmd[1].XA = 0;
vdp1.vram.cmd[1].YA = 0;
constexpr uint16_t magenta = (0x31 << 10) | (0x31 << 0);
constexpr uint16_t magenta = (31 << 10) | (31 << 0);
vdp1.vram.cmd[2].CTRL = CTRL__JP__JUMP_NEXT | CTRL__COMM__POLYGON;
vdp1.vram.cmd[2].LINK = 0;
// "Set [ECD] to '1' for polygons, polylines, and lines"

View File

@ -13,10 +13,10 @@ const static uint8_t layout[3][10] = {
};
void keyboard(struct screen const& s, void (*draw_char)(uint8_t, int32_t, int32_t, int32_t, int32_t))
void keyboard(struct screen const& s, void (*draw_char)(uint8_t, int32_t, int32_t, int32_t, int32_t, enum clue))
{
constexpr int32_t origin_x[3] = {46, 57, 69};
constexpr int32_t origin_y = 170;
constexpr int32_t origin_y = 160;
constexpr uint32_t rows = 3;
constexpr uint32_t cols[3] = {10, 9, 7};
@ -29,19 +29,20 @@ void keyboard(struct screen const& s, void (*draw_char)(uint8_t, int32_t, int32_
int32_t x2 = x1 + box_dim;
int32_t y2 = y1 + box_dim;
draw_char(l, x1, y1, x2, y2);
int32_t l_ix = static_cast<int32_t>(l) - static_cast<int32_t>('A');
draw_char(l, x1, y1, x2, y2, s.clues[l_ix]);
}
}
}
void guesses(struct screen const& s, void (*draw_char)(uint8_t, int32_t, int32_t, int32_t, int32_t))
void guesses(struct screen const& s, void (*draw_char)(uint8_t, int32_t, int32_t, int32_t, int32_t, enum clue))
{
// first row is at (104,23).
// midpoint is +(10,10)
// grid is +(13,13)
constexpr int32_t origin_x = 103;
constexpr int32_t origin_y = 17;
constexpr int32_t origin_y = 12;
for (uint32_t row = 0; row < wordle::guesses; row++) {
@ -55,7 +56,7 @@ void guesses(struct screen const& s, void (*draw_char)(uint8_t, int32_t, int32_t
int32_t x2 = x1 + box_dim;
int32_t y2 = y1 + box_dim;
draw_char(l, x1, y1, x2, y2);
draw_char(l, x1, y1, x2, y2, r.clues[col]);
}
}
}

View File

@ -1,6 +1,6 @@
namespace wordle {
namespace draw {
void keyboard(struct screen const& s, void (*draw_char)(uint8_t, int32_t, int32_t, int32_t, int32_t));
void guesses(struct screen const& s, void (*draw_char)(uint8_t, int32_t, int32_t, int32_t, int32_t));
void keyboard(struct screen const& s, void (*draw_char)(uint8_t, int32_t, int32_t, int32_t, int32_t, enum clue));
void guesses(struct screen const& s, void (*draw_char)(uint8_t, int32_t, int32_t, int32_t, int32_t, enum clue));
}
}

View File

@ -46,6 +46,23 @@ struct intback_state {
static intback_state intback;
static int oreg_ix;
struct xorshift32_state {
uint32_t a;
};
uint32_t xorshift32(struct xorshift32_state *state)
{
uint32_t x = state->a;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
return state->a = x;
}
static xorshift32_state random_state = { 0x12345678 };
static uint32_t frame_count = 0;
void smpc_int(void) __attribute__ ((interrupt_handler));
void smpc_int(void) {
scu.reg.IST &= ~(IST__SMPC);
@ -71,6 +88,7 @@ void smpc_int(void) {
- up to 2 controllers may be connected
- multitaps are not parsed correctly
*/
while (oreg_ix < 31) {
reg8 const& oreg = smpc.reg.oreg[oreg_ix++];
switch (intback.fsm++) {
@ -103,7 +121,20 @@ void smpc_int(void) {
if (kbd_bits & 0b1000) { // Make
enum keysym k = scancode_to_keysym(keysym);
char16_t c = keysym_to_char16(k);
(void)c;
if (k != keysym::UNKNOWN) {
if (c >= 'a' && c <= 'z') {
// uppercase
wordle::type_letter(wordle_state, c);
} else if (k == keysym::ENTER) {
wordle::confirm_word(wordle_state);
} else if (k == keysym::BACKSPACE) {
wordle::backspace(wordle_state);
} else if (k == keysym::ESC) {
random_state.a += frame_count;
uint32_t rand = xorshift32(&random_state);
wordle::init_screen(wordle_state, rand);
}
}
} else if (kbd_bits & 0b0001) { // Break
@ -135,16 +166,28 @@ void smpc_int(void) {
// rendering
void draw_char(uint8_t c, int32_t x1, int32_t y1, int32_t x2, int32_t y2)
inline constexpr uint16_t clue_color(enum wordle::clue c)
{
switch (c) {
default:
case wordle::clue::exact: return ( 0 << 10) | (31 << 5) | ( 0 << 0);
case wordle::clue::present: return ( 7 << 10) | (19 << 5) | (22 << 0);
case wordle::clue::absent: return ( 4 << 10) | ( 4 << 5) | ( 4 << 0);
case wordle::clue::none: return (14 << 10) | (14 << 5) | (14 << 0);
};
}
void draw_char(uint8_t l, int32_t x1, int32_t y1, int32_t x2, int32_t y2, enum wordle::clue c)
{
draw_state.cmd_ix =
draw_font::single_character_centered(draw_state.font,
draw_state.cmd_ix,
c,
l,
x1,
y1,
x2,
y2);
y2,
clue_color(c));
}
void render()
@ -162,6 +205,8 @@ void v_blank_in_int() {
scu.reg.IST &= ~(IST__V_BLANK_IN);
scu.reg.IMS = ~(IMS__SMPC | IMS__V_BLANK_IN);
frame_count++;
sh2.reg.FRC.H = 0;
sh2.reg.FRC.L = 0;
sh2.reg.FTCSR = 0; // clear flags
@ -219,8 +264,8 @@ void main()
v_blank_in();
// wordle init
const uint8_t word[] = "67890";
wordle::init_screen(wordle_state, word);
const uint8_t word_ix = 6;
wordle::init_screen(wordle_state, word_ix);
// end wordle init
// DISP: Please make sure to change this bit from 0 to 1 during V blank.

12972
wordle/word_list.csv Normal file

File diff suppressed because it is too large Load Diff

41
wordle/word_list.py Normal file
View File

@ -0,0 +1,41 @@
import os
import sys
abspath = os.path.abspath(__file__)
dirname = os.path.dirname(abspath)
os.chdir(dirname)
print("namespace wordle {")
print("const uint8_t word_list[][word_length] = {")
answers = list()
with open("word_list.csv", "r") as f:
index = 0
for line in f:
l = line.strip()
if not l:
continue
word, occurrence, day = l.split(",")
w = word.upper()
o = eval(occurrence)
d = day.strip()
if o > 1e-08:
print(f"{{'{w[0]}', '{w[1]}', '{w[2]}', '{w[3]}', '{w[4]}'}},")
if day:
answers.append(index)
index+=1
print("};")
print()
print("const uint32_t answers[] = {")
for answer in answers:
print(f"{answer},")
print("};")
print("}")
print(f"words: {index}", file=sys.stderr)
print(f"answers: {len(answers)}", file=sys.stderr)

View File

@ -2,10 +2,11 @@
#include <stdbool.h>
#include "wordle.hpp"
#include "word_list.hpp"
namespace wordle {
void init_screen(struct screen& s, const uint8_t * word)
void init_screen(struct screen& s, uint32_t rand)
{
s.edit.row = 0;
s.edit.index = 0;
@ -13,66 +14,48 @@ void init_screen(struct screen& s, const uint8_t * word)
for (uint32_t j = 0; j < word_length; j++) {
for (uint32_t i = 0; i < guesses; i++) {
s.rows[i].letters[j] = ' ';
s.rows[i].clues[j] = clue::none;
}
s.word[j] = word[j];
}
s.rows[0].letters[0] = 'A';
s.rows[0].letters[1] = 'B';
s.rows[0].letters[2] = 'C';
s.rows[0].letters[3] = 'D';
s.rows[0].letters[4] = 'E';
for (uint32_t k = 0; k < 26; k++) {
s.clues[k] = clue::none;
}
s.rows[1].letters[0] = 'F';
s.rows[1].letters[1] = 'G';
s.rows[1].letters[2] = 'H';
s.rows[1].letters[3] = 'I';
s.rows[1].letters[4] = 'J';
s.rows[2].letters[0] = 'K';
s.rows[2].letters[1] = 'L';
s.rows[2].letters[2] = 'M';
s.rows[2].letters[3] = 'N';
s.rows[2].letters[4] = 'O';
s.rows[3].letters[0] = 'P';
s.rows[3].letters[1] = 'Q';
s.rows[3].letters[2] = 'R';
s.rows[3].letters[3] = 'S';
s.rows[3].letters[4] = 'T';
s.rows[4].letters[0] = 'U';
s.rows[4].letters[1] = 'V';
s.rows[4].letters[2] = 'W';
s.rows[4].letters[3] = 'X';
s.rows[4].letters[4] = 'Y';
s.rows[5].letters[0] = 'Z';
s.rows[5].letters[1] = '1';
s.rows[5].letters[2] = '2';
s.rows[5].letters[3] = '3';
s.rows[5].letters[4] = '4';
s.all_letters = 0;
constexpr uint32_t answer_length = (sizeof (answers)) / (sizeof (uint32_t));
s.word_ix = answers[rand % answer_length];
}
bool type_letter(struct screen& s, const uint8_t letter)
{
if (s.edit.row >= guesses)
return false;
uint8_t l;
if (s.edit.index >= word_length)
return false;
if (!(letter >= 'a' && letter <= 'z'))
if (letter >= 'a' && letter <= 'z') {
l = letter - ('a' - 'A'); // upcase
} else if (letter >= 'A' && letter <= 'Z') {
l = letter;
} else {
return false;
}
struct row& r = s.rows[s.edit.row];
r.letters[s.edit.index++] = letter;
r.letters[s.edit.index++] = l;
return true;
}
bool backspace(struct screen& s)
{
if (s.edit.row >= guesses)
return false;
if (s.edit.index <= 0)
return false;
@ -83,4 +66,83 @@ bool backspace(struct screen& s)
return true;
}
static inline int32_t word_in_list(const uint8_t * word)
{
constexpr uint32_t word_list_items = (sizeof word_list) / (word_length);
for (uint32_t i = 0; i < word_list_items; i++) {
for (uint32_t j = 0; j < word_length; j++) {
if (word_list[i][j] != word[j])
goto next_word;
}
// word_length loop exited and all letters matched
return true;
next_word:
continue;
}
// exhausted word list; nothing matches
return false;
}
constexpr inline enum clue next_clue(enum clue a, enum clue b)
{
if (a == clue::exact || b == clue::exact)
return clue::exact;
if (a == clue::present || b == clue::present)
return clue::present;
if (a == clue::absent || b == clue::absent)
return clue::absent;
return clue::none;
}
static inline void update_clues(enum clue * clues, struct row &r, const uint8_t * word)
{
for (uint32_t i = 0; i < word_length; i++) {
enum clue &c = r.clues[i];
c = clue::absent;
const uint8_t l = r.letters[i];
if (l == word[i]) {
c = clue::exact;
} else {
// also check for position errors
for (uint32_t j = 0; j < word_length; j++) {
if (word[j] == l) {
c = clue::present;
break;
}
}
}
int32_t l_ix = static_cast<int32_t>(l) - static_cast<int32_t>('A');
if (l_ix >= 0 && l_ix < 26) {
// update global clues
clues[l_ix] = next_clue(clues[l_ix], c);
}
}
}
bool confirm_word(struct screen& s)
{
if (s.edit.row >= guesses)
return false;
if (s.edit.index < word_length)
return false;
if (!(word_in_list(s.rows[s.edit.row].letters)))
return false;
update_clues(s.clues, s.rows[s.edit.row], word_list[s.word_ix]);
s.edit.row++;
s.edit.index = 0;
return true;
}
}

View File

@ -6,14 +6,15 @@ constexpr uint32_t word_length = 5;
constexpr uint32_t guesses = 6;
enum class clue {
correct,
position,
exact,
present,
absent,
none
};
struct row {
uint8_t letters[word_length];
enum clue clues[word_length];
};
struct edit {
@ -24,10 +25,12 @@ struct edit {
struct screen {
struct edit edit;
struct row rows[guesses];
uint8_t word[word_length];
uint32_t all_letters;
uint32_t word_ix;
enum clue clues[26];
};
void init_screen(struct screen& s, const uint8_t * word);
void init_screen(struct screen& s, uint32_t word_ix);
bool type_letter(struct screen& s, const uint8_t letter);
bool backspace(struct screen& s);
bool confirm_word(struct screen& s);
}