Compare commits
10
Commits
119bad49f1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64882d2cf2 | ||
|
|
8691afa048 | ||
|
|
b63a752d8a | ||
|
|
73e92c88b9 | ||
|
|
ce081ba799 | ||
|
|
39a25350ee | ||
|
|
a916a31c69 | ||
|
|
38760693c2 | ||
|
|
3af3fb59dc | ||
|
|
f041cec3bc |
@@ -1,8 +1,6 @@
|
|||||||
# Demordle
|
# Crowdle
|
||||||
|
|
||||||
- TODO: come up with a better name.
|
**Crowdle** is *democratic wordle*. This is a nebulous premise with multiple interpretations; so instead of picking one, `crowdle` implements them all!
|
||||||
|
|
||||||
**Demordle** is *democratic wordle*. This is a nebulous premise with multiple interpretations; so instead of picking one, `demordle` implements them all!
|
|
||||||
|
|
||||||
In all cases, "democratic" means _multiplayer cooperative_ where game actions are decided by vote. The players agree to play ahead of time and share a victory or loss. We also specifically mean _direct_ democracy where every player has equal representation in the voting process.[^representative]
|
In all cases, "democratic" means _multiplayer cooperative_ where game actions are decided by vote. The players agree to play ahead of time and share a victory or loss. We also specifically mean _direct_ democracy where every player has equal representation in the voting process.[^representative]
|
||||||
|
|
||||||
|
|||||||
+2315
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Wordle = @import("wordle.zig");
|
||||||
|
|
||||||
|
/// A statically-initialized dictionary of all Wordle words.
|
||||||
|
var Dictionary: ?[]const Wordle.Word = null;
|
||||||
|
|
||||||
|
/// Gets or statically initializes the dictionary.
|
||||||
|
pub fn getDictionary() []const Wordle.Word {
|
||||||
|
if (Dictionary) |dict| {
|
||||||
|
return dict;
|
||||||
|
} else {
|
||||||
|
const dict = loadDict();
|
||||||
|
Dictionary = dict;
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal function to load the Wordle dictionary.
|
||||||
|
fn loadDict() []const Wordle.Word {
|
||||||
|
// directly embed dictionary contents
|
||||||
|
const src = @embedFile("dict.txt");
|
||||||
|
|
||||||
|
// count lines (words)
|
||||||
|
var num: u64 = 0;
|
||||||
|
for (src, 0..) |char, idx| {
|
||||||
|
_ = idx;
|
||||||
|
if (char == '\n') {
|
||||||
|
num += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allocate all words
|
||||||
|
const dictionary = std.heap.smp_allocator.alloc(Wordle.Word, num) catch unreachable;
|
||||||
|
|
||||||
|
// iterate over all lines
|
||||||
|
var lines = std.mem.splitSequence(u8, src, "\n");
|
||||||
|
var idx: usize = 0;
|
||||||
|
while (lines.next()) |line| {
|
||||||
|
// skip empty line at end
|
||||||
|
if (line.len == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert line is correct length
|
||||||
|
if (line.len != Wordle.WordLen) {
|
||||||
|
std.debug.panic("length of '{s}' mismatches {d} characters", .{ line, Wordle.WordLen });
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy line bytes over
|
||||||
|
@memcpy(&dictionary[idx], line);
|
||||||
|
|
||||||
|
// increment dictionary entry
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return complete dictionary
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests if a word is in the dictionary.
|
||||||
|
// TODO: binary search? need to assert that words are sorted
|
||||||
|
pub fn isInDictionary(word: Wordle.Word) bool {
|
||||||
|
// exhaustively search through dictionary
|
||||||
|
for (getDictionary()) |*entry| {
|
||||||
|
if (std.mem.eql(u8, entry, &word)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no word was found, return false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts that a word literal is in the dictionary.
|
||||||
|
fn expectDictionary(word: Wordle.WordLiteral) !void {
|
||||||
|
try std.testing.expect(isInDictionary(Wordle.strToWord(word)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts that a word literal is *not* in the dictionary.
|
||||||
|
fn expectNotDictionary(word: Wordle.WordLiteral) !void {
|
||||||
|
try std.testing.expect(!isInDictionary(Wordle.strToWord(word)));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "in dictionary" {
|
||||||
|
try expectDictionary("aback");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "not in dictionary" {
|
||||||
|
try expectNotDictionary("hbzci");
|
||||||
|
}
|
||||||
+83
-2
@@ -1,5 +1,86 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const Wordle = @import("wordle.zig");
|
||||||
|
const Dict = @import("dict.zig");
|
||||||
|
|
||||||
pub fn main() void {
|
pub fn main() !void {
|
||||||
std.debug.print("Hello, {s}!\n", .{"World"});
|
// acquire stdout writer
|
||||||
|
var stdoutBuf: [1024]u8 = undefined;
|
||||||
|
var stdoutFile = std.fs.File.stdout().writer(&stdoutBuf);
|
||||||
|
if (stdoutFile.err) |err| return err;
|
||||||
|
const stdout = &stdoutFile.interface;
|
||||||
|
|
||||||
|
// acquire stdin reader
|
||||||
|
var stdinBuf: [1024]u8 = undefined;
|
||||||
|
var stdinFile = std.fs.File.stdin().reader(&stdinBuf);
|
||||||
|
if (stdinFile.err) |err| return err;
|
||||||
|
const stdin = &stdinFile.interface;
|
||||||
|
|
||||||
|
// select a random dictionary word as the solution
|
||||||
|
const dict = Dict.getDictionary();
|
||||||
|
const solutionIdx = std.crypto.random.intRangeAtMost(usize, 0, dict.len);
|
||||||
|
|
||||||
|
// create a game with the chosen solution
|
||||||
|
var game: Wordle.Game = .{ .solution = dict[solutionIdx] };
|
||||||
|
|
||||||
|
// play game until over
|
||||||
|
while (!game.isOver()) {
|
||||||
|
// display guess prompt
|
||||||
|
try stdout.writeAll("Guess: ");
|
||||||
|
try stdout.flush();
|
||||||
|
|
||||||
|
// read next guess
|
||||||
|
const input = try stdin.takeDelimiter('\n') orelse continue;
|
||||||
|
|
||||||
|
// assert that input is correct size
|
||||||
|
if (input.len != Wordle.WordLen) {
|
||||||
|
try stdout.writeAll("Guess is wrong length.\n");
|
||||||
|
try stdout.flush();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert to word
|
||||||
|
var guess: Wordle.Word = undefined;
|
||||||
|
@memcpy(&guess, input);
|
||||||
|
|
||||||
|
// test if guess is valid
|
||||||
|
if (!Dict.isInDictionary(guess)) {
|
||||||
|
try stdout.writeAll("Guess is not in dictionary.\n");
|
||||||
|
try stdout.flush();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// display clues
|
||||||
|
const clues = Wordle.checkGuess(guess, game.solution);
|
||||||
|
try writeClues(stdout, clues);
|
||||||
|
|
||||||
|
// add guess
|
||||||
|
try game.addGuess(guess);
|
||||||
|
}
|
||||||
|
|
||||||
|
// display message win/lose
|
||||||
|
if (game.isWon()) {
|
||||||
|
try stdout.writeAll("You win!\n");
|
||||||
|
} else {
|
||||||
|
try stdout.writeAll("You lose :(\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// display spoilered guesses
|
||||||
|
for (game.guesses()) |guess| {
|
||||||
|
try writeClues(stdout, Wordle.checkGuess(guess, game.solution));
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush stdout
|
||||||
|
try stdout.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeClues(out: *std.io.Writer, clues: Wordle.Clues) !void {
|
||||||
|
for (clues) |clue| {
|
||||||
|
try out.writeAll(switch (clue) {
|
||||||
|
Wordle.Clue.Gray => "⬛",
|
||||||
|
Wordle.Clue.Yellow => "🟨",
|
||||||
|
Wordle.Clue.Green => "🟩",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try out.writeAll("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Wordle = @import("wordle.zig");
|
||||||
|
const Dict = @import("dict.zig");
|
||||||
|
|
||||||
|
/// A "pattern": a word where nulls signify wildcards.
|
||||||
|
pub const Pattern = Wordle.Word;
|
||||||
|
|
||||||
|
/// A list of candidates: indexes of words in the dictionary.
|
||||||
|
pub const Candidates = std.ArrayList(u16);
|
||||||
|
|
||||||
|
/// A single vote.
|
||||||
|
pub const Vote = struct {
|
||||||
|
/// The pattern to match this vote to.
|
||||||
|
pattern: Pattern,
|
||||||
|
|
||||||
|
/// The number of votes placed on this pattern.
|
||||||
|
score: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A collection of votes.
|
||||||
|
pub const Election = []const Vote;
|
||||||
|
|
||||||
|
/// Calculates the total number of votes in an election.
|
||||||
|
pub fn electionVoteTotal(election: Election) usize {
|
||||||
|
var total: usize = 0;
|
||||||
|
for (election) |vote| total += vote.score;
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests if a pattern is satisfied by a given word.
|
||||||
|
pub fn patternSatisfied(word: Wordle.Word, pat: Pattern) bool {
|
||||||
|
// find mismatches
|
||||||
|
for (word, pat) |wordLetter, patLetter| {
|
||||||
|
if (patLetter != 0 and patLetter != wordLetter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no mismatches were found, the pattern is satisfied
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Naively calculates how many votes a word scores in an election.
|
||||||
|
pub fn wordScore(word: Wordle.Word, election: Election) usize {
|
||||||
|
var total: usize = 0;
|
||||||
|
for (election) |vote| {
|
||||||
|
if (patternSatisfied(word, vote.pattern)) {
|
||||||
|
total += vote.score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elects candidates using brute-force scoring on the whole dictionary.
|
||||||
|
///
|
||||||
|
/// The result must be deinitialized after use.
|
||||||
|
pub fn naiveElect(alloc: std.mem.Allocator, election: Election) !Candidates {
|
||||||
|
// track the maximum known score
|
||||||
|
var maxScore: usize = 0;
|
||||||
|
|
||||||
|
// keep a list of all highest-scoring candidates
|
||||||
|
var candidates: Candidates = .empty;
|
||||||
|
|
||||||
|
// evaluate all words in the dictionary
|
||||||
|
for (Dict.getDictionary(), 0..) |word, idx| {
|
||||||
|
// score this word
|
||||||
|
const score = wordScore(word, election);
|
||||||
|
|
||||||
|
// if score is lower than current known score, skip
|
||||||
|
if (score < maxScore) continue;
|
||||||
|
|
||||||
|
// if score exceeds known score, clear candidates and accommodate
|
||||||
|
if (score > maxScore) {
|
||||||
|
maxScore = score;
|
||||||
|
try candidates.resize(alloc, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add this candidate's index
|
||||||
|
try candidates.append(alloc, @intCast(idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the complete list of candidates
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats a list of the words referred to by candidate indices.
|
||||||
|
fn formatCandidates(alloc: std.mem.Allocator, candidates: Candidates) !std.ArrayList(u8) {
|
||||||
|
// cache dictionary for repeated lookup
|
||||||
|
const dict = Dict.getDictionary();
|
||||||
|
|
||||||
|
// dynamically allocate formatted words
|
||||||
|
var formatted: std.ArrayList(u8) = .empty;
|
||||||
|
for (candidates.items, 0..) |wordIdx, idx| {
|
||||||
|
// prefix word with space unless it's the first one
|
||||||
|
if (idx != 0) {
|
||||||
|
try formatted.append(alloc, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// append the word in the dictionary
|
||||||
|
try formatted.appendSlice(alloc, &dict[wordIdx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "simple naive score" {
|
||||||
|
const election = [_]Vote{.{
|
||||||
|
.pattern = Wordle.strToWord("abas\x00"),
|
||||||
|
.score = 1,
|
||||||
|
}};
|
||||||
|
|
||||||
|
try std.testing.expectEqual(wordScore(Wordle.strToWord("abash"), &election), 1);
|
||||||
|
try std.testing.expectEqual(wordScore(Wordle.strToWord("abask"), &election), 1);
|
||||||
|
try std.testing.expectEqual(wordScore(Wordle.strToWord("abear"), &election), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "simple naive election" {
|
||||||
|
const election = [_]Vote{.{
|
||||||
|
.pattern = Wordle.strToWord("abas\x00"),
|
||||||
|
.score = 1,
|
||||||
|
}};
|
||||||
|
|
||||||
|
const alloc = std.heap.smp_allocator;
|
||||||
|
var candidates = try naiveElect(alloc, &election);
|
||||||
|
defer candidates.deinit(alloc);
|
||||||
|
|
||||||
|
var candidatesFmt = try formatCandidates(alloc, candidates);
|
||||||
|
defer candidatesFmt.deinit(alloc);
|
||||||
|
|
||||||
|
try std.testing.expectEqualStrings(candidatesFmt.items, "abash abask");
|
||||||
|
}
|
||||||
+88
-23
@@ -1,3 +1,5 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
/// The length of Wordle words.
|
/// The length of Wordle words.
|
||||||
pub const WordLen = 5;
|
pub const WordLen = 5;
|
||||||
|
|
||||||
@@ -7,6 +9,51 @@ pub const Word = [WordLen]u8;
|
|||||||
/// The type of string literals representing words.
|
/// The type of string literals representing words.
|
||||||
pub const WordLiteral = *const [WordLen:0]u8;
|
pub const WordLiteral = *const [WordLen:0]u8;
|
||||||
|
|
||||||
|
/// The number of rounds in a single Wordle game.
|
||||||
|
pub const GameLen = 6;
|
||||||
|
|
||||||
|
/// An error reported when a move cannot be played since the game is over.
|
||||||
|
pub const GameOver = error{GameOver};
|
||||||
|
|
||||||
|
/// A structure for a single Wordle game.
|
||||||
|
pub const Game = struct {
|
||||||
|
/// The hidden word to guess.
|
||||||
|
solution: Word,
|
||||||
|
|
||||||
|
/// The round number (starts with 0).
|
||||||
|
round: u32 = 0,
|
||||||
|
|
||||||
|
/// The current (and future) word guesses.
|
||||||
|
///
|
||||||
|
/// The values of guessBuf[self.round..] are undefined.
|
||||||
|
guessBuf: [GameLen]Word = @splat(@splat(0)),
|
||||||
|
|
||||||
|
/// Fetches the current guesses.
|
||||||
|
pub fn guesses(self: *const Game) []const Word {
|
||||||
|
return self.guessBuf[0..self.round];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests if the game is won.
|
||||||
|
pub fn isWon(self: Game) bool {
|
||||||
|
for (self.guesses()) |guess|
|
||||||
|
if (std.mem.eql(u8, &guess, &self.solution)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests if the game is over.
|
||||||
|
pub fn isOver(self: Game) bool {
|
||||||
|
return self.round >= GameLen or self.isWon();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guesses a word unless the game is over.
|
||||||
|
pub fn addGuess(self: *Game, guess: Word) GameOver!void {
|
||||||
|
if (self.isOver()) return error.GameOver;
|
||||||
|
self.guessBuf[self.round] = guess;
|
||||||
|
self.round += 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// A clue for a spot given a guess.
|
/// A clue for a spot given a guess.
|
||||||
pub const Clue = enum {
|
pub const Clue = enum {
|
||||||
Gray,
|
Gray,
|
||||||
@@ -19,8 +66,11 @@ pub const Clues = [WordLen]Clue;
|
|||||||
|
|
||||||
/// Guesses a word given a solution and returns the clues.
|
/// Guesses a word given a solution and returns the clues.
|
||||||
pub fn checkGuess(guess: Word, solution: Word) Clues {
|
pub fn checkGuess(guess: Word, solution: Word) Clues {
|
||||||
// init clues
|
// init clues, defaulting to gray clues
|
||||||
var clues: Clues = undefined;
|
var clues: Clues = @splat(Clue.Gray);
|
||||||
|
|
||||||
|
// track which letters have been used for yellow clues
|
||||||
|
var duplicates: [WordLen]bool = @splat(false);
|
||||||
|
|
||||||
// compare each letter pairwise
|
// compare each letter pairwise
|
||||||
for (guess, 0..) |guessLetter, idx| {
|
for (guess, 0..) |guessLetter, idx| {
|
||||||
@@ -30,13 +80,11 @@ pub fn checkGuess(guess: Word, solution: Word) Clues {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// default to a gray clue
|
|
||||||
clues[idx] = Clue.Gray;
|
|
||||||
|
|
||||||
// if a matching letter in the solution can be found, write yellow
|
// if a matching letter in the solution can be found, write yellow
|
||||||
for (solution) |solutionLetter| {
|
for (solution, &duplicates) |solutionLetter, *duplicate| {
|
||||||
if (guessLetter == solutionLetter) {
|
if (guessLetter == solutionLetter and !duplicate.*) {
|
||||||
clues[idx] = Clue.Yellow;
|
clues[idx] = Clue.Yellow;
|
||||||
|
duplicate.* = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,30 +102,26 @@ pub fn strToWord(str: WordLiteral) Word {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Asserts that two word literals produce the expected clues.
|
/// Asserts that two word literals produce the expected clues.
|
||||||
fn expectGuessClues(guess: WordLiteral, solution: WordLiteral, expectClues: WordLiteral) error{Mismatch}!void {
|
fn expectGuessClues(guess: WordLiteral, solution: WordLiteral, expectClues: WordLiteral) !void {
|
||||||
// convert literals to the non-null word type
|
// convert literals to the non-null word type
|
||||||
const guessWord = strToWord(guess);
|
const guessWord = strToWord(guess);
|
||||||
const solutionWord = strToWord(solution);
|
const solutionWord = strToWord(solution);
|
||||||
const expectCluesWord = strToWord(expectClues);
|
|
||||||
|
|
||||||
// check the guess and produce clues
|
// check the guess and produce clues
|
||||||
const clues = checkGuess(guessWord, solutionWord);
|
const clues = checkGuess(guessWord, solutionWord);
|
||||||
|
|
||||||
// confirm that the expected clues match the clues
|
// convert the clues to a word
|
||||||
for (clues, expectCluesWord) |clue, expectedChar| {
|
var cluesWord: Word = undefined;
|
||||||
// convert character to associated clue variant
|
for (clues, &cluesWord) |src, *dst| {
|
||||||
const expected = switch (expectedChar) {
|
dst.* = switch (src) {
|
||||||
' ' => Clue.Gray,
|
Clue.Gray => ' ',
|
||||||
'Y' => Clue.Yellow,
|
Clue.Yellow => 'Y',
|
||||||
'G' => Clue.Green,
|
Clue.Green => 'G',
|
||||||
else => unreachable,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// if clues don't match, return an error
|
|
||||||
if (expected != clue) {
|
|
||||||
return error.Mismatch;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test the equality of the strings
|
||||||
|
try std.testing.expectEqualStrings(&cluesWord, expectClues);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "full match" {
|
test "full match" {
|
||||||
@@ -92,6 +136,27 @@ test "no match" {
|
|||||||
try expectGuessClues("salet", "fuzzy", " ");
|
try expectGuessClues("salet", "fuzzy", " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "anagram" {
|
||||||
|
try expectGuessClues("beats", "abets", "YYYGG");
|
||||||
|
}
|
||||||
|
|
||||||
test "duplicate letters" {
|
test "duplicate letters" {
|
||||||
try expectGuessClues("fuzzy", "zesty", " Y ");
|
try expectGuessClues("fuzzy", "zesty", " Y G");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "game" {
|
||||||
|
// default state
|
||||||
|
var game: Game = .{ .solution = strToWord("poise") };
|
||||||
|
try std.testing.expect(!game.isWon());
|
||||||
|
try std.testing.expect(!game.isOver());
|
||||||
|
|
||||||
|
// first guess
|
||||||
|
try game.addGuess(strToWord("brown"));
|
||||||
|
try std.testing.expect(!game.isWon());
|
||||||
|
try std.testing.expect(!game.isOver());
|
||||||
|
|
||||||
|
// second guess
|
||||||
|
try game.addGuess(strToWord("poise"));
|
||||||
|
try std.testing.expect(game.isWon());
|
||||||
|
try std.testing.expect(game.isOver());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user