From 6a2eb1d391a26dd9b97b9c971eb65c9f2bba6bd5 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Sat, 13 Jun 2026 20:10:49 +0200 Subject: [PATCH 1/4] Add `game-of-life` exercice --- config.json | 8 + .../game-of-life/.docs/instructions.md | 11 + .../game-of-life/.docs/introduction.md | 9 + .../practice/game-of-life/.meta/config.json | 19 + .../practice/game-of-life/.meta/example.sh | 77 +++ .../practice/game-of-life/.meta/tests.toml | 34 + .../practice/game-of-life/bats-extra.bash | 637 ++++++++++++++++++ .../practice/game-of-life/game_of_life.bats | 145 ++++ .../practice/game-of-life/game_of_life.sh | 24 + 9 files changed, 964 insertions(+) create mode 100644 exercises/practice/game-of-life/.docs/instructions.md create mode 100644 exercises/practice/game-of-life/.docs/introduction.md create mode 100644 exercises/practice/game-of-life/.meta/config.json create mode 100644 exercises/practice/game-of-life/.meta/example.sh create mode 100644 exercises/practice/game-of-life/.meta/tests.toml create mode 100644 exercises/practice/game-of-life/bats-extra.bash create mode 100644 exercises/practice/game-of-life/game_of_life.bats create mode 100644 exercises/practice/game-of-life/game_of_life.sh diff --git a/config.json b/config.json index ebb3e41e..47267e04 100644 --- a/config.json +++ b/config.json @@ -667,6 +667,14 @@ "math" ] }, + { + "slug": "game-of-life", + "name": "Conway's Game of Life", + "uuid": "8a0f7cae-d5f5-4cda-9fc1-5b7ff3774be7", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "diamond", "name": "Diamond", 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 00000000..49531406 --- /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 00000000..2347b936 --- /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/.meta/config.json b/exercises/practice/game-of-life/.meta/config.json new file mode 100644 index 00000000..c6fab640 --- /dev/null +++ b/exercises/practice/game-of-life/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "resu-xuniL" + ], + "files": { + "solution": [ + "game_of_life.sh" + ], + "test": [ + "game_of_life.bats" + ], + "example": [ + ".meta/example.sh" + ] + }, + "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.sh b/exercises/practice/game-of-life/.meta/example.sh new file mode 100644 index 00000000..dd19d62f --- /dev/null +++ b/exercises/practice/game-of-life/.meta/example.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +declare -A matrix +declare -i height width + +create_matrix() { + local -i col row=0 + + while read -r line; do + for ((col = 0; col < ${#line}; col++)); do + matrix[${col},${row}]=${line:col:1} + done + + height=${#line} + (( row++ )) + done < <(printf "%s\n" "${@//, /}") + + width=$row +} + +tick() { + local -A next_matrix + local -i row col countLiveCell + + for ((row = 0; row < height; row++)); do + for ((col = 0; col < width; col++)); do + countLiveCell=$(check_neighborhood $col $row) + + if (( ${matrix[${col},${row}]} == 1 && (countLiveCell == 2 || countLiveCell == 3) )); then + next_matrix[${col},${row}]=1 + elif (( ${matrix[${col},${row}]} == 0 && countLiveCell == 3 )); then + next_matrix[${col},${row}]=1 + else + next_matrix[${col},${row}]=0 + fi + done + done + + print_next_matrix "next_matrix" +} + +check_neighborhood() { + local cell_col=$1 cell_row=$2 + local -i neighb_row neighb_col + local count=0 + + for check_row in -1 0 1; do + neighb_row=$(( check_row + cell_row )) + (( neighb_row < 0 || neighb_row >= height )) && continue + for check_col in -1 0 1; do + neighb_col=$(( check_col + cell_col )) + (( neighb_col < 0 || neighb_col >= width )) && continue + (( check_row == 0 && check_col == 0 )) && continue + + (( count+=${matrix[${neighb_col},${neighb_row}]} )) + done + done + + echo $count +} + +print_next_matrix() { + local -n array=$1 + local -i row col + local output + + for ((row = 0; row < height; row++)); do + output="" + for ((col = 0; col < width; col++)); do + output+="${array[${col},${row}]}, " + done + echo "${output%, }" + done +} + +create_matrix "$@" +tick 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 00000000..398cd454 --- /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/bats-extra.bash b/exercises/practice/game-of-life/bats-extra.bash new file mode 100644 index 00000000..54d48070 --- /dev/null +++ b/exercises/practice/game-of-life/bats-extra.bash @@ -0,0 +1,637 @@ +# This is the source code for bats-support and bats-assert, concatenated +# * https://github.com/bats-core/bats-support +# * https://github.com/bats-core/bats-assert +# +# Comments have been removed to save space. See the git repos for full source code. + +############################################################ +# +# bats-support - Supporting library for Bats test helpers +# +# Written in 2016 by Zoltan Tombol +# +# To the extent possible under law, the author(s) have dedicated all +# copyright and related and neighboring rights to this software to the +# public domain worldwide. This software is distributed without any +# warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication +# along with this software. If not, see +# . +# + +fail() { + (( $# == 0 )) && batslib_err || batslib_err "$@" + return 1 +} + +batslib_is_caller() { + local -i is_mode_direct=1 + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -i|--indirect) is_mode_direct=0; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + # Arguments. + local -r func="$1" + + # Check call stack. + if (( is_mode_direct )); then + [[ $func == "${FUNCNAME[2]}" ]] && return 0 + else + local -i depth + for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do + [[ $func == "${FUNCNAME[$depth]}" ]] && return 0 + done + fi + + return 1 +} + +batslib_err() { + { if (( $# > 0 )); then + echo "$@" + else + cat - + fi + } >&2 +} + +batslib_count_lines() { + local -i n_lines=0 + local line + while IFS='' read -r line || [[ -n $line ]]; do + (( ++n_lines )) + done < <(printf '%s' "$1") + echo "$n_lines" +} + +batslib_is_single_line() { + for string in "$@"; do + (( $(batslib_count_lines "$string") > 1 )) && return 1 + done + return 0 +} + +batslib_get_max_single_line_key_width() { + local -i max_len=-1 + while (( $# != 0 )); do + local -i key_len="${#1}" + batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len" + shift 2 + done + echo "$max_len" +} + +batslib_print_kv_single() { + local -ir col_width="$1"; shift + while (( $# != 0 )); do + printf '%-*s : %s\n' "$col_width" "$1" "$2" + shift 2 + done +} + +batslib_print_kv_multi() { + while (( $# != 0 )); do + printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )" + printf '%s\n' "$2" + shift 2 + done +} + +batslib_print_kv_single_or_multi() { + local -ir width="$1"; shift + local -a pairs=( "$@" ) + + local -a values=() + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + values+=( "${pairs[$i]}" ) + done + + if batslib_is_single_line "${values[@]}"; then + batslib_print_kv_single "$width" "${pairs[@]}" + else + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )" + done + batslib_print_kv_multi "${pairs[@]}" + fi +} + +batslib_prefix() { + local -r prefix="${1:- }" + local line + while IFS='' read -r line || [[ -n $line ]]; do + printf '%s%s\n' "$prefix" "$line" + done +} + +batslib_mark() { + local -r symbol="$1"; shift + # Sort line numbers. + set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" ) + + local line + local -i idx=0 + while IFS='' read -r line || [[ -n $line ]]; do + if (( ${1:--1} == idx )); then + printf '%s\n' "${symbol}${line:${#symbol}}" + shift + else + printf '%s\n' "$line" + fi + (( ++idx )) + done +} + +batslib_decorate() { + echo + echo "-- $1 --" + cat - + echo '--' + echo +} + +############################################################ + +assert() { + if ! "$@"; then + batslib_print_kv_single 10 'expression' "$*" \ + | batslib_decorate 'assertion failed' \ + | fail + fi +} + +assert_equal() { + if [[ $1 != "$2" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$2" \ + 'actual' "$1" \ + | batslib_decorate 'values do not equal' \ + | fail + fi +} + +assert_failure() { + : "${output?}" + : "${status?}" + + (( $# > 0 )) && local -r expected="$1" + if (( status == 0 )); then + batslib_print_kv_single_or_multi 6 'output' "$output" \ + | batslib_decorate 'command succeeded, but it was expected to fail' \ + | fail + elif (( $# > 0 )) && (( status != expected )); then + { local -ir width=8 + batslib_print_kv_single "$width" \ + 'expected' "$expected" \ + 'actual' "$status" + batslib_print_kv_single_or_multi "$width" \ + 'output' "$output" + } \ + | batslib_decorate 'command failed as expected, but status differs' \ + | fail + fi +} + +assert_line() { + local -i is_match_line=0 + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + : "${lines?}" + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -n|--index) + if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + echo "\`--index' requires an integer argument: \`$2'" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + is_match_line=1 + local -ri idx="$2" + shift 2 + ;; + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + + # Arguments. + local -r expected="$1" + + if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$expected'" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + + # Matching. + if (( is_match_line )); then + # Specific line. + if (( is_mode_regexp )); then + if ! [[ ${lines[$idx]} =~ $expected ]]; then + batslib_print_kv_single 6 \ + 'index' "$idx" \ + 'regexp' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression does not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} != *"$expected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line does not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} != "$expected" ]]; then + batslib_print_kv_single 8 \ + 'index' "$idx" \ + 'expected' "$expected" \ + 'actual' "${lines[$idx]}" \ + | batslib_decorate 'line differs' \ + | fail + fi + fi + else + # Contained in output. + if (( is_mode_regexp )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} =~ $expected ]] && return 0 + done + { local -ar single=( 'regexp' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'no output line matches regular expression' \ + | fail + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == *"$expected"* ]] && return 0 + done + { local -ar single=( 'substring' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'no output line contains substring' \ + | fail + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == "$expected" ]] && return 0 + done + { local -ar single=( 'line' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'output does not contain line' \ + | fail + fi + fi +} + +assert_output() { + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + local -i is_mode_nonempty=0 + local -i use_stdin=0 + : "${output?}" + + # Handle options. + if (( $# == 0 )); then + is_mode_nonempty=1 + fi + + while (( $# > 0 )); do + case "$1" in + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + -|--stdin) use_stdin=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + return $? + fi + + # Arguments. + local expected + if (( use_stdin )); then + expected="$(cat -)" + else + expected="${1-}" + fi + + # Matching. + if (( is_mode_nonempty )); then + if [ -z "$output" ]; then + echo 'expected non-empty output, but output was empty' \ + | batslib_decorate 'no output' \ + | fail + fi + elif (( is_mode_regexp )); then + if [[ '' =~ $expected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$expected'" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + elif ! [[ $output =~ $expected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regexp' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression does not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output != *"$expected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'output does not contain substring' \ + | fail + fi + else + if [[ $output != "$expected" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$expected" \ + 'actual' "$output" \ + | batslib_decorate 'output differs' \ + | fail + fi + fi +} + +assert_success() { + : "${output?}" + : "${status?}" + + if (( status != 0 )); then + { local -ir width=6 + batslib_print_kv_single "$width" 'status' "$status" + batslib_print_kv_single_or_multi "$width" 'output' "$output" + } \ + | batslib_decorate 'command failed' \ + | fail + fi +} + +refute() { + if "$@"; then + batslib_print_kv_single 10 'expression' "$*" \ + | batslib_decorate 'assertion succeeded, but it was expected to fail' \ + | fail + fi +} + +refute_line() { + local -i is_match_line=0 + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + : "${lines?}" + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -n|--index) + if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + echo "\`--index' requires an integer argument: \`$2'" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + is_match_line=1 + local -ri idx="$2" + shift 2 + ;; + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + + # Arguments. + local -r unexpected="$1" + + if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$unexpected'" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + + # Matching. + if (( is_match_line )); then + # Specific line. + if (( is_mode_regexp )); then + if [[ ${lines[$idx]} =~ $unexpected ]]; then + batslib_print_kv_single 6 \ + 'index' "$idx" \ + 'regexp' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression should not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} == "$unexpected" ]]; then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should differ' \ + | fail + fi + fi + else + # Line contained in output. + if (( is_mode_regexp )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} =~ $unexpected ]]; then + { local -ar single=( 'regexp' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'no line should match the regular expression' \ + | fail + return $? + fi + done + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + { local -ar single=( 'substring' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'no line should contain substring' \ + | fail + return $? + fi + done + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == "$unexpected" ]]; then + { local -ar single=( 'line' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'line should not be in output' \ + | fail + return $? + fi + done + fi + fi +} + +refute_output() { + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + local -i is_mode_empty=0 + local -i use_stdin=0 + : "${output?}" + + # Handle options. + if (( $# == 0 )); then + is_mode_empty=1 + fi + + while (( $# > 0 )); do + case "$1" in + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + -|--stdin) use_stdin=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Arguments. + local unexpected + if (( use_stdin )); then + unexpected="$(cat -)" + else + unexpected="${1-}" + fi + + if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$unexpected'" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Matching. + if (( is_mode_empty )); then + if [ -n "$output" ]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" \ + | batslib_decorate 'output non-empty, but expected no output' \ + | fail + fi + elif (( is_mode_regexp )); then + if [[ $output =~ $unexpected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regexp' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression should not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output == *"$unexpected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'output should not contain substring' \ + | fail + fi + else + if [[ $output == "$unexpected" ]]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" \ + | batslib_decorate 'output equals, but it was expected to differ' \ + | fail + fi + fi +} diff --git a/exercises/practice/game-of-life/game_of_life.bats b/exercises/practice/game-of-life/game_of_life.bats new file mode 100644 index 00000000..33cdcc4a --- /dev/null +++ b/exercises/practice/game-of-life/game_of_life.bats @@ -0,0 +1,145 @@ +#!/usr/bin/env bats +load bats-extra + +join() { + local IFS=$'\n' + echo "$*" +} + +@test "empty matrix" { + #[[ $BATS_RUN_SKIPPED == "true" ]] || skip + input=( "" ) + run bash game_of_life.sh "${input[@]}" + assert_success + expected=( "" ) + assert_output "$(join "${expected[@]}")" +} + +@test "live cells with zero live neighbors die" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + input=( + "0, 0, 0" + "0, 1, 0" + "0, 0, 0" + ) + run bash game_of_life.sh "${input[@]}" + assert_success + expected=( + "0, 0, 0" + "0, 0, 0" + "0, 0, 0" + ) + assert_output "$(join "${expected[@]}")" +} + +@test "live cells with only one live neighbor die" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + input=( + "0, 0, 0" + "0, 1, 0" + "0, 1, 0" + ) + run bash game_of_life.sh "${input[@]}" + assert_success + expected=( + "0, 0, 0" + "0, 0, 0" + "0, 0, 0" + ) + assert_output "$(join "${expected[@]}")" +} + +@test "live cells with two live neighbors stay alive" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + input=( + "1, 0, 1" + "1, 0, 1" + "1, 0, 1" + ) + run bash game_of_life.sh "${input[@]}" + assert_success + expected=( + "0, 0, 0" + "1, 0, 1" + "0, 0, 0" + ) + assert_output "$(join "${expected[@]}")" +} + +@test "live cells with three live neighbors stay alive" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + input=( + "0, 1, 0" + "1, 0, 0" + "1, 1, 0" + ) + run bash game_of_life.sh "${input[@]}" + assert_success + expected=( + "0, 0, 0" + "1, 0, 0" + "1, 1, 0" + ) + assert_output "$(join "${expected[@]}")" +} + +@test "dead cells with three live neighbors become alive" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + input=( + "1, 1, 0" + "0, 0, 0" + "1, 0, 0" + ) + run bash game_of_life.sh "${input[@]}" + assert_success + expected=( + "0, 0, 0" + "1, 1, 0" + "0, 0, 0" + ) + assert_output "$(join "${expected[@]}")" +} + +@test "live cells with four or more neighbors die" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + input=( + "1, 1, 1" + "1, 1, 1" + "1, 1, 1" + ) + run bash game_of_life.sh "${input[@]}" + assert_success + expected=( + "1, 0, 1" + "0, 0, 0" + "1, 0, 1" + ) + assert_output "$(join "${expected[@]}")" +} + +@test "bigger matrix" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + input=( + "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" + ) + run bash game_of_life.sh "${input[@]}" + assert_success + expected=( + "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_output "$(join "${expected[@]}")" +} diff --git a/exercises/practice/game-of-life/game_of_life.sh b/exercises/practice/game-of-life/game_of_life.sh new file mode 100644 index 00000000..960869f3 --- /dev/null +++ b/exercises/practice/game-of-life/game_of_life.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# The following comments should help you get started: +# - Bash is flexible. You may use functions or write a "raw" script. +# +# - Complex code can be made easier to read by breaking it up +# into functions, however this is sometimes overkill in bash. +# +# - You can find links about good style and other resources +# for Bash in './README.md'. It came with this exercise. +# +# Example: +# # other functions here +# # ... +# # ... +# +# main () { +# # your main function code here +# } +# +# # call main with all of the positional arguments +# main "$@" +# +# *** PLEASE REMOVE THESE COMMENTS BEFORE SUBMITTING YOUR SOLUTION *** From 6ef303a1ba67feaa5f5b56a74378d19d4a66ce31 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Sun, 14 Jun 2026 12:50:10 +0200 Subject: [PATCH 2/4] Update `example.sh` and `game_of_life.bats` * Co-authored-by: IsaacG * Co-authored-by: glennj --- .../practice/game-of-life/.meta/example.sh | 24 ++- .../practice/game-of-life/game_of_life.bats | 155 +++++++++--------- 2 files changed, 92 insertions(+), 87 deletions(-) diff --git a/exercises/practice/game-of-life/.meta/example.sh b/exercises/practice/game-of-life/.meta/example.sh index dd19d62f..79a5eac5 100644 --- a/exercises/practice/game-of-life/.meta/example.sh +++ b/exercises/practice/game-of-life/.meta/example.sh @@ -7,7 +7,7 @@ create_matrix() { local -i col row=0 while read -r line; do - for ((col = 0; col < ${#line}; col++)); do + for (( col = 0; col < ${#line}; col++ )); do matrix[${col},${row}]=${line:col:1} done @@ -22,13 +22,11 @@ tick() { local -A next_matrix local -i row col countLiveCell - for ((row = 0; row < height; row++)); do - for ((col = 0; col < width; col++)); do + for (( row = 0; row < height; row++ )); do + for (( col = 0; col < width; col++ )); do countLiveCell=$(check_neighborhood $col $row) - if (( ${matrix[${col},${row}]} == 1 && (countLiveCell == 2 || countLiveCell == 3) )); then - next_matrix[${col},${row}]=1 - elif (( ${matrix[${col},${row}]} == 0 && countLiveCell == 3 )); then + if (( countLiveCell == 3 || (matrix[${col},${row}] == 1 && countLiveCell == 2) )); then next_matrix[${col},${row}]=1 else next_matrix[${col},${row}]=0 @@ -42,7 +40,7 @@ tick() { check_neighborhood() { local cell_col=$1 cell_row=$2 local -i neighb_row neighb_col - local count=0 + local -i count=0 for check_row in -1 0 1; do neighb_row=$(( check_row + cell_row )) @@ -56,7 +54,7 @@ check_neighborhood() { done done - echo $count + echo "$count" } print_next_matrix() { @@ -64,12 +62,12 @@ print_next_matrix() { local -i row col local output - for ((row = 0; row < height; row++)); do - output="" - for ((col = 0; col < width; col++)); do - output+="${array[${col},${row}]}, " + for (( row = 0; row < height; row++ )); do + output="" + for (( col = 0; col < width; col++ )); do + output+="${array[${col},${row}]}" done - echo "${output%, }" + echo "$output" done } diff --git a/exercises/practice/game-of-life/game_of_life.bats b/exercises/practice/game-of-life/game_of_life.bats index 33cdcc4a..a59d0f3b 100644 --- a/exercises/practice/game-of-life/game_of_life.bats +++ b/exercises/practice/game-of-life/game_of_life.bats @@ -9,137 +9,144 @@ join() { @test "empty matrix" { #[[ $BATS_RUN_SKIPPED == "true" ]] || skip input=( "" ) + expected=( "" ) run bash game_of_life.sh "${input[@]}" assert_success - expected=( "" ) assert_output "$(join "${expected[@]}")" } @test "live cells with zero live neighbors die" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip input=( - "0, 0, 0" - "0, 1, 0" - "0, 0, 0" + "000" + "010" + "000" + ) + expected_rows=( + "000" + "000" + "000" ) + expected_string="$(join "${expected_rows[@]}")" run bash game_of_life.sh "${input[@]}" assert_success - expected=( - "0, 0, 0" - "0, 0, 0" - "0, 0, 0" - ) - assert_output "$(join "${expected[@]}")" + assert_output "$expected_string" } @test "live cells with only one live neighbor die" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip input=( - "0, 0, 0" - "0, 1, 0" - "0, 1, 0" + "000" + "010" + "010" + ) + expected_rows=( + "000" + "000" + "000" ) + expected_string="$(join "${expected_rows[@]}")" run bash game_of_life.sh "${input[@]}" assert_success - expected=( - "0, 0, 0" - "0, 0, 0" - "0, 0, 0" - ) - assert_output "$(join "${expected[@]}")" + assert_output "$expected_string" } @test "live cells with two live neighbors stay alive" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip input=( - "1, 0, 1" - "1, 0, 1" - "1, 0, 1" + "101" + "101" + "101" + ) + expected_rows=( + "000" + "101" + "000" ) + expected_string="$(join "${expected_rows[@]}")" run bash game_of_life.sh "${input[@]}" assert_success - expected=( - "0, 0, 0" - "1, 0, 1" - "0, 0, 0" - ) - assert_output "$(join "${expected[@]}")" + assert_output "$expected_string" } @test "live cells with three live neighbors stay alive" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip input=( - "0, 1, 0" - "1, 0, 0" - "1, 1, 0" + "010" + "100" + "110" + ) + expected_rows=( + "000" + "100" + "110" ) + expected_string="$(join "${expected_rows[@]}")" run bash game_of_life.sh "${input[@]}" assert_success - expected=( - "0, 0, 0" - "1, 0, 0" - "1, 1, 0" - ) - assert_output "$(join "${expected[@]}")" + assert_output "$expected_string" } @test "dead cells with three live neighbors become alive" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip input=( - "1, 1, 0" - "0, 0, 0" - "1, 0, 0" + "110" + "000" + "100" + ) + expected_rows=( + "000" + "110" + "000" ) + expected_string="$(join "${expected_rows[@]}")" run bash game_of_life.sh "${input[@]}" assert_success - expected=( - "0, 0, 0" - "1, 1, 0" - "0, 0, 0" - ) - assert_output "$(join "${expected[@]}")" + assert_output "$expected_string" } @test "live cells with four or more neighbors die" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip input=( - "1, 1, 1" - "1, 1, 1" - "1, 1, 1" + "111" + "111" + "111" + ) + expected_rows=( + "101" + "000" + "101" ) + expected_string="$(join "${expected_rows[@]}")" run bash game_of_life.sh "${input[@]}" assert_success - expected=( - "1, 0, 1" - "0, 0, 0" - "1, 0, 1" - ) - assert_output "$(join "${expected[@]}")" + assert_output "$expected_string" } @test "bigger matrix" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip input=( - "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" + "11011000" + "10110000" + "11100111" + "00000110" + "10001100" + "11000111" + "00101001" + "10000011" + ) + expected_rows=( + "11011000" + "00000110" + "10111101" + "10000001" + "11001001" + "11010001" + "10000000" + "00000011" ) + expected_string="$(join "${expected_rows[@]}")" run bash game_of_life.sh "${input[@]}" assert_success - expected=( - "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_output "$(join "${expected[@]}")" + assert_output "$expected_string" } From 16723246523ec710de9a0d9e3c307d4974f5f1d8 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Tue, 16 Jun 2026 08:43:38 +0200 Subject: [PATCH 3/4] Changed difficulty rank --- config.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config.json b/config.json index 47267e04..80cfd394 100644 --- a/config.json +++ b/config.json @@ -667,14 +667,6 @@ "math" ] }, - { - "slug": "game-of-life", - "name": "Conway's Game of Life", - "uuid": "8a0f7cae-d5f5-4cda-9fc1-5b7ff3774be7", - "practices": [], - "prerequisites": [], - "difficulty": 4 - }, { "slug": "diamond", "name": "Diamond", @@ -980,6 +972,14 @@ "variables" ] }, + { + "slug": "game-of-life", + "name": "Conway's Game of Life", + "uuid": "8a0f7cae-d5f5-4cda-9fc1-5b7ff3774be7", + "practices": [], + "prerequisites": [], + "difficulty": 6 + }, { "slug": "meetup", "name": "Meetup", From c4fa6bfe14a2c6a6df30fdd8c9c45f22b9f64e75 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Tue, 16 Jun 2026 08:45:35 +0200 Subject: [PATCH 4/4] Update `example.sh` * Co-authored-by: IsaacG --- exercises/practice/game-of-life/.meta/example.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exercises/practice/game-of-life/.meta/example.sh b/exercises/practice/game-of-life/.meta/example.sh index 79a5eac5..cdf73308 100644 --- a/exercises/practice/game-of-life/.meta/example.sh +++ b/exercises/practice/game-of-life/.meta/example.sh @@ -13,7 +13,7 @@ create_matrix() { height=${#line} (( row++ )) - done < <(printf "%s\n" "${@//, /}") + done < <(printf "%s\n" "${@}") width=$row } @@ -24,9 +24,9 @@ tick() { for (( row = 0; row < height; row++ )); do for (( col = 0; col < width; col++ )); do - countLiveCell=$(check_neighborhood $col $row) + countLiveCell=$(check_neighborhood "$col" "$row") - if (( countLiveCell == 3 || (matrix[${col},${row}] == 1 && countLiveCell == 2) )); then + if (( countLiveCell == 3 || matrix[${col},${row}] == 1 && countLiveCell == 2 )); then next_matrix[${col},${row}]=1 else next_matrix[${col},${row}]=0