diff --git a/.lycheeignore b/.lycheeignore index 6b69b6464..09c0d748c 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -6,3 +6,4 @@ twitter.com regexr.com papyr.com reddit.com +vimeo.com/53221562 diff --git a/bin/bootstrap_practice_exercise.exs b/bin/bootstrap_practice_exercise.exs index 269249a43..7c1bacd38 100644 --- a/bin/bootstrap_practice_exercise.exs +++ b/bin/bootstrap_practice_exercise.exs @@ -1,8 +1,8 @@ # Generate all files required for a practice exercise. # File content is filled as much as possible, but some, including tests, need manual input. # -# Run the following command from the root of the repo: -# $ elixir bin/boostrap_practice_exercise.exs complex-numbers +# Do not run directly, instead use this script: +# $ bin/generate_practice_exercise.sh complex-numbers # Pass the name of the exercise (e. g., "complex-numbers") as an argument Mix.install([ @@ -143,9 +143,6 @@ module = ## Step 1: create folder structure -Mix.Generator.create_directory("exercises/practice/#{exercise}") -Mix.Generator.create_directory("exercises/practice/#{exercise}/.docs") -Mix.Generator.create_directory("exercises/practice/#{exercise}/.meta") Mix.Generator.create_directory("exercises/practice/#{exercise}/lib") Mix.Generator.create_directory("exercises/practice/#{exercise}/test") @@ -243,38 +240,6 @@ url = :inets.start() :ssl.start() -# .docs/instructions.md -{:ok, {_status, _header, description}} = - :httpc.request(:get, {url ++ ~c"/description.md", []}, [], []) - -Mix.Generator.create_file("exercises/practice/#{exercise}/.docs/instructions.md", description) - -# .meta/config.json -{:ok, {_status, _header, metadata}} = - :httpc.request(:get, {url ++ ~c"/metadata.toml", []}, [], []) - -metadata = - metadata - |> to_string - |> Toml.decode!() - -config = %{ - authors: [], - contributors: [], - files: %{ - solution: ["lib/#{exercise_snake_case}.ex"], - test: ["test/#{exercise_snake_case}_test.exs"], - example: [".meta/example.ex"] - } -} - -config = - Map.merge(metadata, config) - |> Jason.encode!(pretty: true) - -Mix.Generator.create_file("exercises/practice/#{exercise}/.meta/config.json", config) -IO.puts("Don't forget to add your name and the names of contributors") - # tests and lib files {:ok, {_status, _header, data}} = :httpc.request(:get, {url ++ ~c"/canonical-data.json", []}, [], []) diff --git a/bin/generate_practice_exercise.sh b/bin/generate_practice_exercise.sh new file mode 100755 index 000000000..524ffdfe4 --- /dev/null +++ b/bin/generate_practice_exercise.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Exit if anything fails. +set -eo pipefail + +# If argument not provided, print usage and exit +if [ -z "$1" ]; then + echo "Usage: bin/generate_practice_exercise.sh " + exit 1 +fi + +SLUG="$1" + +# build configlet +echo "Fetching latest version of configlet..." +./bin/fetch-configlet + +# Preparing config.json +echo "Adding instructions and configuration files..." +UUID=$(bin/configlet uuid) +jq --arg slug "$SLUG" --arg uuid "$UUID" \ + '.exercises.practice += [{slug: $slug, name: "TODO", uuid: $uuid, practices: [], prerequisites: [], difficulty: 5}]' \ + config.json > config.json.tmp +mv config.json.tmp config.json + +# Create instructions and config files +./bin/configlet sync --update --yes --docs --filepaths --metadata --exercise "$SLUG" + +# Create Elixir files +echo "Creating Elixir files..." +elixir bin/bootstrap_practice_exercise.exs $SLUG + +echo "All stub files were created. After implementing the solution, tests and configuration, please run:" +echo " elixir bin/check_practice_exercise_order.exs --write" +echo " ./bin/configlet sync --update --tests --exercise ${SLUG}" +echo " ./bin/configlet fmt --update --yes --exercise ${SLUG}" diff --git a/config.json b/config.json index 63cf96959..1bf16b9f6 100644 --- a/config.json +++ b/config.json @@ -2313,6 +2313,22 @@ ], "difficulty": 5 }, + { + "slug": "game-of-life", + "name": "Game of Life", + "uuid": "32d53ab8-abea-4483-a8cb-ee3b908a9dd7", + "practices": [ + "enum" + ], + "prerequisites": [ + "enum", + "lists", + "pattern-matching", + "pipe-operator", + "multiple-clause-functions" + ], + "difficulty": 6 + }, { "slug": "knapsack", "name": "Knapsack", diff --git a/exercises/practice/game-of-life/.docs/instructions.md b/exercises/practice/game-of-life/.docs/instructions.md new file mode 100644 index 000000000..495314064 --- /dev/null +++ b/exercises/practice/game-of-life/.docs/instructions.md @@ -0,0 +1,11 @@ +# Instructions + +After each generation, the cells interact with their eight neighbors, which are cells adjacent horizontally, vertically, or diagonally. + +The following rules are applied to each cell: + +- Any live cell with two or three live neighbors lives on. +- Any dead cell with exactly three live neighbors becomes a live cell. +- All other cells die or stay dead. + +Given a matrix of 1s and 0s (corresponding to live and dead cells), apply the rules to each cell, and return the next generation. diff --git a/exercises/practice/game-of-life/.docs/introduction.md b/exercises/practice/game-of-life/.docs/introduction.md new file mode 100644 index 000000000..2347b936e --- /dev/null +++ b/exercises/practice/game-of-life/.docs/introduction.md @@ -0,0 +1,9 @@ +# Introduction + +[Conway's Game of Life][game-of-life] is a fascinating cellular automaton created by the British mathematician John Horton Conway in 1970. + +The game consists of a two-dimensional grid of cells that can either be "alive" or "dead." + +After each generation, the cells interact with their eight neighbors via a set of rules, which define the new generation. + +[game-of-life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life diff --git a/exercises/practice/game-of-life/.formatter.exs b/exercises/practice/game-of-life/.formatter.exs new file mode 100644 index 000000000..d2cda26ed --- /dev/null +++ b/exercises/practice/game-of-life/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/exercises/practice/game-of-life/.gitignore b/exercises/practice/game-of-life/.gitignore new file mode 100644 index 000000000..5535af6bc --- /dev/null +++ b/exercises/practice/game-of-life/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +game_of_life-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/exercises/practice/game-of-life/.meta/config.json b/exercises/practice/game-of-life/.meta/config.json new file mode 100644 index 000000000..c5d4e4d8b --- /dev/null +++ b/exercises/practice/game-of-life/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "jiegillet" + ], + "files": { + "solution": [ + "lib/game_of_life.ex" + ], + "test": [ + "test/game_of_life_test.exs" + ], + "example": [ + ".meta/example.ex" + ] + }, + "blurb": "Implement Conway's Game of Life.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life" +} diff --git a/exercises/practice/game-of-life/.meta/example.ex b/exercises/practice/game-of-life/.meta/example.ex new file mode 100644 index 000000000..33fe04a80 --- /dev/null +++ b/exercises/practice/game-of-life/.meta/example.ex @@ -0,0 +1,44 @@ +defmodule GameOfLife do + @doc """ + Apply the rules of Conway's Game of Life to a grid of cells + """ + + @spec tick(matrix :: list(list(0 | 1))) :: list(list(0 | 1)) + def tick([]), do: [] + + def tick(matrix) do + neighbors = count_neighbors(matrix) + + Enum.zip_with(matrix, neighbors, fn row_matrix, row_neighbor -> + Enum.zip_with(row_matrix, row_neighbor, &rule/2) + end) + end + + defp count_neighbors(matrix) do + shift_left = shift_left(matrix) + shift_right = shift_right(matrix) + + shifts = [ + shift_left, + shift_right, + shift_up(matrix), + shift_down(matrix), + shift_left |> shift_up(), + shift_left |> shift_down(), + shift_right |> shift_up(), + shift_right |> shift_down() + ] + + Enum.zip_with(shifts, fn rows -> Enum.zip_with(rows, &Enum.sum/1) end) + end + + defp shift_right(matrix), do: Enum.map(matrix, fn row -> [0 | row] end) + defp shift_left(matrix), do: Enum.map(matrix, fn row -> Enum.drop(row, 1) ++ [0] end) + defp shift_up([first | rest]), do: rest ++ [Enum.map(first, fn _ -> 0 end)] + defp shift_down([first | _] = matrix), do: [Enum.map(first, fn _ -> 0 end) | matrix] + + defp rule(1, 2), do: 1 + defp rule(1, 3), do: 1 + defp rule(0, 3), do: 1 + defp rule(_cell, _live_neighbors), do: 0 +end diff --git a/exercises/practice/game-of-life/.meta/tests.toml b/exercises/practice/game-of-life/.meta/tests.toml new file mode 100644 index 000000000..398cd4546 --- /dev/null +++ b/exercises/practice/game-of-life/.meta/tests.toml @@ -0,0 +1,34 @@ +# 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. + +[ae86ea7d-bd07-4357-90b3-ac7d256bd5c5] +description = "empty matrix" + +[4ea5ccb7-7b73-4281-954a-bed1b0f139a5] +description = "live cells with zero live neighbors die" + +[df245adc-14ff-4f9c-b2ae-f465ef5321b2] +description = "live cells with only one live neighbor die" + +[2a713b56-283c-48c8-adae-1d21306c80ae] +description = "live cells with two live neighbors stay alive" + +[86d5c5a5-ab7b-41a1-8907-c9b3fc5e9dae] +description = "live cells with three live neighbors stay alive" + +[015f60ac-39d8-4c6c-8328-57f334fc9f89] +description = "dead cells with three live neighbors become alive" + +[2ee69c00-9d41-4b8b-89da-5832e735ccf1] +description = "live cells with four or more neighbors die" + +[a79b42be-ed6c-4e27-9206-43da08697ef6] +description = "bigger matrix" diff --git a/exercises/practice/game-of-life/lib/game_of_life.ex b/exercises/practice/game-of-life/lib/game_of_life.ex new file mode 100644 index 000000000..2d47ce255 --- /dev/null +++ b/exercises/practice/game-of-life/lib/game_of_life.ex @@ -0,0 +1,9 @@ +defmodule GameOfLife do + @doc """ + Apply the rules of Conway's Game of Life to a grid of cells + """ + + @spec tick(matrix :: list(list(0 | 1))) :: list(list(0 | 1)) + def tick(matrix) do + end +end diff --git a/exercises/practice/game-of-life/mix.exs b/exercises/practice/game-of-life/mix.exs new file mode 100644 index 000000000..91b15ac62 --- /dev/null +++ b/exercises/practice/game-of-life/mix.exs @@ -0,0 +1,28 @@ +defmodule GameOfLife.MixProject do + use Mix.Project + + def project do + [ + app: :game_of_life, + version: "0.1.0", + # elixir: "~> 1.10", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/exercises/practice/game-of-life/test/game_of_life_test.exs b/exercises/practice/game-of-life/test/game_of_life_test.exs new file mode 100644 index 000000000..70e1b0cc7 --- /dev/null +++ b/exercises/practice/game-of-life/test/game_of_life_test.exs @@ -0,0 +1,83 @@ +defmodule GameOfLifeTest do + use ExUnit.Case + + # @tag :pending + test "empty matrix" do + assert GameOfLife.tick([]) == [] + end + + @tag :pending + test "live cells with zero live neighbors die" do + matrix = [[0, 0, 0], [0, 1, 0], [0, 0, 0]] + output = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] + + assert GameOfLife.tick(matrix) == output + end + + @tag :pending + test "live cells with only one live neighbor die" do + matrix = [[0, 0, 0], [0, 1, 0], [0, 1, 0]] + output = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] + + assert GameOfLife.tick(matrix) == output + end + + @tag :pending + test "live cells with two live neighbors stay alive" do + matrix = [[1, 0, 1], [1, 0, 1], [1, 0, 1]] + output = [[0, 0, 0], [1, 0, 1], [0, 0, 0]] + + assert GameOfLife.tick(matrix) == output + end + + @tag :pending + test "live cells with three live neighbors stay alive" do + matrix = [[0, 1, 0], [1, 0, 0], [1, 1, 0]] + output = [[0, 0, 0], [1, 0, 0], [1, 1, 0]] + + assert GameOfLife.tick(matrix) == output + end + + @tag :pending + test "dead cells with three live neighbors become alive" do + matrix = [[1, 1, 0], [0, 0, 0], [1, 0, 0]] + output = [[0, 0, 0], [1, 1, 0], [0, 0, 0]] + + assert GameOfLife.tick(matrix) == output + end + + @tag :pending + test "live cells with four or more neighbors die" do + matrix = [[1, 1, 1], [1, 1, 1], [1, 1, 1]] + output = [[1, 0, 1], [0, 0, 0], [1, 0, 1]] + + assert GameOfLife.tick(matrix) == output + end + + @tag :pending + test "bigger matrix" do + matrix = [ + [1, 1, 0, 1, 1, 0, 0, 0], + [1, 0, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 0], + [1, 0, 0, 0, 1, 1, 0, 0], + [1, 1, 0, 0, 0, 1, 1, 1], + [0, 0, 1, 0, 1, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 1, 1] + ] + + output = [ + [1, 1, 0, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0], + [1, 0, 1, 1, 1, 1, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 1], + [1, 1, 0, 0, 1, 0, 0, 1], + [1, 1, 0, 1, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1] + ] + + assert GameOfLife.tick(matrix) == output + end +end diff --git a/exercises/practice/game-of-life/test/test_helper.exs b/exercises/practice/game-of-life/test/test_helper.exs new file mode 100644 index 000000000..35fc5bff8 --- /dev/null +++ b/exercises/practice/game-of-life/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true)