From e459fc00454f64425c9d76f39d023ca13d47384b Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Fri, 12 Jun 2026 11:12:47 +1000 Subject: [PATCH] Add `save-the-cow` exercise --- config.json | 8 ++ .../save-the-cow/.docs/instructions.md | 7 ++ .../save-the-cow/.docs/introduction.md | 4 + .../practice/save-the-cow/.meta/config.json | 17 +++ .../practice/save-the-cow/.meta/example.zig | 71 +++++++++++ .../practice/save-the-cow/.meta/tests.toml | 40 ++++++ .../practice/save-the-cow/save_the_cow.zig | 44 +++++++ .../save-the-cow/test_save_the_cow.zig | 117 ++++++++++++++++++ generators/exercises/save_the_cow.py | 62 ++++++++++ 9 files changed, 370 insertions(+) create mode 100644 exercises/practice/save-the-cow/.docs/instructions.md create mode 100644 exercises/practice/save-the-cow/.docs/introduction.md create mode 100644 exercises/practice/save-the-cow/.meta/config.json create mode 100644 exercises/practice/save-the-cow/.meta/example.zig create mode 100644 exercises/practice/save-the-cow/.meta/tests.toml create mode 100644 exercises/practice/save-the-cow/save_the_cow.zig create mode 100644 exercises/practice/save-the-cow/test_save_the_cow.zig create mode 100644 generators/exercises/save_the_cow.py diff --git a/config.json b/config.json index 99938799..b65abb71 100644 --- a/config.json +++ b/config.json @@ -947,6 +947,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "save-the-cow", + "name": "Save the Cow", + "uuid": "06766bb8-e77b-43fb-80d0-f228d65f309b", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "state-of-tic-tac-toe", "name": "State of Tic-Tac-Toe", diff --git a/exercises/practice/save-the-cow/.docs/instructions.md b/exercises/practice/save-the-cow/.docs/instructions.md new file mode 100644 index 00000000..1ec98a43 --- /dev/null +++ b/exercises/practice/save-the-cow/.docs/instructions.md @@ -0,0 +1,7 @@ +# Instructions + +Implement the logic for a word-guessing game. + +A player tries to solve a secret word by guessing individual letters. +They win if they reveal all the letters in the secret word. +They lose if they make ten incorrect guesses before revealing the word. diff --git a/exercises/practice/save-the-cow/.docs/introduction.md b/exercises/practice/save-the-cow/.docs/introduction.md new file mode 100644 index 00000000..c87a7fa9 --- /dev/null +++ b/exercises/practice/save-the-cow/.docs/introduction.md @@ -0,0 +1,4 @@ +# Introduction + +Bessie the cow has wandered onto an alien spaceship. +Guess the secret door code to bring her home before the ship blasts off. diff --git a/exercises/practice/save-the-cow/.meta/config.json b/exercises/practice/save-the-cow/.meta/config.json new file mode 100644 index 00000000..6c62d61a --- /dev/null +++ b/exercises/practice/save-the-cow/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "save_the_cow.zig" + ], + "test": [ + "test_save_the_cow.zig" + ], + "example": [ + ".meta/example.zig" + ] + }, + "blurb": "Implement a word-guessing game." +} diff --git a/exercises/practice/save-the-cow/.meta/example.zig b/exercises/practice/save-the-cow/.meta/example.zig new file mode 100644 index 00000000..5b88f55e --- /dev/null +++ b/exercises/practice/save-the-cow/.meta/example.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const mem = std.mem; + +pub const State = enum { + ongoing, + win, + lose, +}; + +pub const Error = error{ GameAlreadyWon, GameAlreadyLost }; + +pub const Game = struct { + state: State, + remaining_failures: u32, + word: []const u8, + masked: []u8, + + /// Initializes a game with a copy of the given word, 9 remaining failures + /// and every letter hidden. + pub fn init(allocator: mem.Allocator, word: []const u8) mem.Allocator.Error!Game { + const copy = try allocator.dupe(u8, word); + errdefer allocator.free(copy); + const masked = try allocator.alloc(u8, word.len); + @memset(masked, '_'); + + return .{ + .state = .ongoing, + .remaining_failures = 9, + .word = copy, + .masked = masked, + }; + } + + /// Frees the game. + pub fn deinit(self: *Game, allocator: mem.Allocator) void { + allocator.free(self.word); + allocator.free(self.masked); + } + + /// Processes one guessed letter. + pub fn guess(self: *Game, letter: u8) Error!void { + switch (self.state) { + .ongoing => {}, + .win => return error.GameAlreadyWon, + .lose => return error.GameAlreadyLost, + } + + var correct = false; + for (self.word, 0..) |c, i| { + if (c == letter and self.masked[i] != letter) { + self.masked[i] = letter; + correct = true; + } + } + + if (correct) { + if (mem.eql(u8, self.masked, self.word)) { + self.state = .win; + } + } else if (self.remaining_failures > 0) { + self.remaining_failures -= 1; + } else { + self.state = .lose; + } + } + + /// Returns the word with every unguessed letter replaced by an underscore. + pub fn maskedWord(self: *const Game) []const u8 { + return self.masked; + } +}; diff --git a/exercises/practice/save-the-cow/.meta/tests.toml b/exercises/practice/save-the-cow/.meta/tests.toml new file mode 100644 index 00000000..eca72c42 --- /dev/null +++ b/exercises/practice/save-the-cow/.meta/tests.toml @@ -0,0 +1,40 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[71d340f9-fc29-4826-872e-ad7d0b83dd98] +description = "Initially 9 failures are allowed and no letters are guessed" + +[76759c24-8f1a-4fc8-9ffd-d6a1ff0cf03b] +description = "After 10 failures the game is over" + +[d6f2e202-7857-46fb-b709-43a7f3f3b2de] +description = "Losing with several correct guesses" + +[71bc0cda-2032-4637-80c8-fc8771124c08] +description = "Feeding a correct letter removes underscores" + +[5b568a1c-867d-418f-97a8-7b6f8a7ca0a2] +description = "Feeding a correct letter twice counts as a failure" + +[3d40f15b-0271-4c5d-b1a4-3e1f66ff221f] +description = "Guessing a repeated letter reveals all instances" + +[11a86435-e401-4250-a26e-3b0d8c4049ad] +description = "Getting all the letters right makes for a win" + +[b3d81876-84ee-45bb-b531-baa1b05b4709] +description = "Winning on the last guess is still a win" + +[cf204398-5e9f-402f-8bff-42cb7cbbbda9] +description = "Guessing after a lose is error" + +[c2ec5b3d-4923-4a0e-a485-6aa6e78c7ece] +description = "Guessing after a win is error" diff --git a/exercises/practice/save-the-cow/save_the_cow.zig b/exercises/practice/save-the-cow/save_the_cow.zig new file mode 100644 index 00000000..e7a63edd --- /dev/null +++ b/exercises/practice/save-the-cow/save_the_cow.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const mem = std.mem; + +pub const State = enum { + ongoing, + win, + lose, +}; + +pub const Error = error{ GameAlreadyWon, GameAlreadyLost }; + +pub const Game = struct { + state: State, + remaining_failures: u32, + // Additional fields need to be added. + + /// Initializes a game with a copy of the given word, 9 remaining failures + /// and every letter hidden. + pub fn init(allocator: mem.Allocator, word: []const u8) mem.Allocator.Error!Game { + _ = allocator; + _ = word; + @compileError("please implement the init function"); + } + + /// Frees the game. + pub fn deinit(self: *Game, allocator: mem.Allocator) void { + _ = self; + _ = allocator; + @compileError("please implement the deinit function"); + } + + /// Processes one guessed letter. + pub fn guess(self: *Game, letter: u8) Error!void { + _ = self; + _ = letter; + @compileError("please implement the guess function"); + } + + /// Returns the word with every unguessed letter replaced by an underscore. + pub fn maskedWord(self: *const Game) []const u8 { + _ = self; + @compileError("please implement the maskedWord function"); + } +}; diff --git a/exercises/practice/save-the-cow/test_save_the_cow.zig b/exercises/practice/save-the-cow/test_save_the_cow.zig new file mode 100644 index 00000000..a611e348 --- /dev/null +++ b/exercises/practice/save-the-cow/test_save_the_cow.zig @@ -0,0 +1,117 @@ +const std = @import("std"); +const testing = std.testing; + +const save_the_cow = @import("save_the_cow.zig"); +const State = save_the_cow.State; +const Game = save_the_cow.Game; + +/// Plays `guesses` against `word` and checks the resulting game. +fn testGame( + allocator: std.mem.Allocator, + word: []const u8, + guesses: []const u8, + state: State, + masked_word: []const u8, + remaining_failures: u32, +) !void { + var game = try Game.init(allocator, word); + defer game.deinit(allocator); + for (guesses) |letter| try game.guess(letter); + try testing.expectEqual(state, game.state); + try testing.expectEqualStrings(masked_word, game.maskedWord()); + try testing.expectEqual(remaining_failures, game.remaining_failures); +} + +/// Plays `guesses` against `word` and checks that the final guess returns +/// `expected_error`. +fn testGameError( + allocator: std.mem.Allocator, + word: []const u8, + guesses: []const u8, + expected_error: anyerror, +) !void { + var game = try Game.init(allocator, word); + defer game.deinit(allocator); + for (guesses[0 .. guesses.len - 1]) |letter| try game.guess(letter); + try testing.expectError(expected_error, game.guess(guesses[guesses.len - 1])); +} + +test "Initially 9 failures are allowed and no letters are guessed" { + try testing.checkAllAllocationFailures( + testing.allocator, + testGame, + .{ "loot", "", .ongoing, "____", 9 }, + ); +} + +test "After 10 failures the game is over" { + try testing.checkAllAllocationFailures( + testing.allocator, + testGame, + .{ "loot", "abcdefghij", .lose, "____", 0 }, + ); +} + +test "Losing with several correct guesses" { + try testing.checkAllAllocationFailures( + testing.allocator, + testGame, + .{ "loot", "toabcdefghij", .lose, "_oot", 0 }, + ); +} + +test "Feeding a correct letter removes underscores" { + try testing.checkAllAllocationFailures( + testing.allocator, + testGame, + .{ "loot", "t", .ongoing, "___t", 9 }, + ); +} + +test "Feeding a correct letter twice counts as a failure" { + try testing.checkAllAllocationFailures( + testing.allocator, + testGame, + .{ "loot", "tt", .ongoing, "___t", 8 }, + ); +} + +test "Guessing a repeated letter reveals all instances" { + try testing.checkAllAllocationFailures( + testing.allocator, + testGame, + .{ "loot", "tto", .ongoing, "_oot", 8 }, + ); +} + +test "Getting all the letters right makes for a win" { + try testing.checkAllAllocationFailures( + testing.allocator, + testGame, + .{ "loot", "ttol", .win, "loot", 8 }, + ); +} + +test "Winning on the last guess is still a win" { + try testing.checkAllAllocationFailures( + testing.allocator, + testGame, + .{ "loot", "abcdefghitol", .win, "loot", 0 }, + ); +} + +test "Guessing after a lose is error" { + try testing.checkAllAllocationFailures( + testing.allocator, + testGameError, + .{ "loot", "abcdefghijk", error.GameAlreadyLost }, + ); +} + +test "Guessing after a win is error" { + try testing.checkAllAllocationFailures( + testing.allocator, + testGameError, + .{ "loot", "toll", error.GameAlreadyWon }, + ); +} diff --git a/generators/exercises/save_the_cow.py b/generators/exercises/save_the_cow.py new file mode 100644 index 00000000..0e0620bd --- /dev/null +++ b/generators/exercises/save_the_cow.py @@ -0,0 +1,62 @@ +from lib import zstr + +HEADER = """const State = save_the_cow.State; +const Game = save_the_cow.Game; + +/// Plays `guesses` against `word` and checks the resulting game. +fn testGame( + allocator: std.mem.Allocator, + word: []const u8, + guesses: []const u8, + state: State, + masked_word: []const u8, + remaining_failures: u32, +) !void { + var game = try Game.init(allocator, word); + defer game.deinit(allocator); + for (guesses) |letter| try game.guess(letter); + try testing.expectEqual(state, game.state); + try testing.expectEqualStrings(masked_word, game.maskedWord()); + try testing.expectEqual(remaining_failures, game.remaining_failures); +} + +/// Plays `guesses` against `word` and checks that the final guess returns +/// `expected_error`. +fn testGameError( + allocator: std.mem.Allocator, + word: []const u8, + guesses: []const u8, + expected_error: anyerror, +) !void { + var game = try Game.init(allocator, word); + defer game.deinit(allocator); + for (guesses[0 .. guesses.len - 1]) |letter| try game.guess(letter); + try testing.expectError(expected_error, game.guess(guesses[guesses.len - 1])); +} +""" + + +def gen_case(case): + inp = case["input"] + word = zstr(inp["word"]) + guesses = zstr("".join(inp["guesses"])) + e = case["expected"] + + if "error" in e: + err = "GameAlreadyLost" if "lost" in e["error"] else "GameAlreadyWon" + fn = "testGameError" + args = f"{word}, {guesses}, error.{err}" + else: + fn = "testGame" + args = ( + f"{word}, {guesses}, .{e['state'].lower()}, " + f"{zstr(e['maskedWord'])}, {e['remainingFailures']}" + ) + + return ( + " try testing.checkAllAllocationFailures(\n" + " testing.allocator,\n" + f" {fn},\n" + f" .{{ {args} }},\n" + " );\n" + )