Draft

Pocket Frogs and Disco Zoo search minigame solver

Generalized solver for the minigames, using light information theory and Bayesian approach.
R
Code snippet
Published

February 5, 2026

A while ago, I wrote a generalized solver for Pocket Frogs’ search game as well as for Disco Zoo. At the end of a previous 2025-08-13 post, I had mentioned this.

They were fully functional, but I never got around to posting the code. I no longer play the minigames, so am not particularly motivated to write an explanatory post about how it all works, but I include a sample run solving a game in Pocket Frogs, as well as the Julia source code. The puzzle patterns may have changed since then, so the code would need a little bit of updating.

Background from the 2025-08-13 post

The spots I favor have higher expected information density (see also 3Blue1Brown Wordle information theory which is where I got the idea of using simple information theory), resulting in a simpler algorithm for playing. … However, you can also play with higher level frog species, each of which has its own pattern. In this case, the goal might not be to farm gifts, but instead to farm the frogs themselves.

Pocket Frogs minigame patterns

I also learned that the search minigame is based on the core game mechanic of another NimbleBit Game – Disco Zoo. In Disco Zoo, you also have a 5x5 grid but there can be more or less than two animals per game board.

Eventually, I wrote some Julia code to solve for the more general scenario, where given a starting position of n different patterns on a 5x5 grid, the probabilities of each animal and the expected information content of each position would be calculated, updated after each move.

Using this tool, I ground through all the Disco Zoo levels embarassingly quickly. A future post will show the Julia code…

Sample run, after loading the Julia code

julia> play_game()
Enter comma-separated list of species: losg,garc
Probability empty
5×5 Matrix{Float64}:
 0.8   0.77  0.77  1.0   1.0
 0.77  0.77  0.77  1.0   1.0
 0.77  0.77  0.6   0.77  0.77
 1.0   1.0   0.77  0.77  0.77
 1.0   1.0   0.77  0.77  0.8


Probability of 1: Losgann
5×5 Matrix{Float64}:
 0.1   0.11  0.11  0.0   0.0
 0.11  0.11  0.11  0.0   0.0
 0.11  0.11  0.2   0.11  0.11
 0.0   0.0   0.11  0.11  0.11
 0.0   0.0   0.11  0.11  0.1


Probability of 2: Garcius
5×5 Matrix{Float64}:
 0.1   0.11  0.11  0.0   0.0
 0.11  0.11  0.11  0.0   0.0
 0.11  0.11  0.2   0.11  0.11
 0.0   0.0   0.11  0.11  0.11
 0.0   0.0   0.11  0.11  0.1


Expected information
5×5 Matrix{Float64}:
 0.92  1.0  1.0   0.0  0.0
 1.0   1.0  1.0   0.0  0.0
 1.0   1.0  1.37  1.0  1.0
 0.0   0.0  1.0   1.0  1.0
 0.0   0.0  1.0   1.0  0.92


70 possibilities remaining

* <row> and <col> range from 1 to 5
* <val> is 0 for empty space or # for goal number
* <display> shows all remaining possibilities

Enter <row><col><val>, <display> to show possibilities, or <enter> to exit: 330
Probability empty
5×5 Matrix{Float64}:
 1.0   0.71  0.71  1.0   1.0
 0.71  0.71  0.71  1.0   1.0
 0.71  0.71  1.0   0.71  0.71
 1.0   1.0   0.71  0.71  0.71
 1.0   1.0   0.71  0.71  1.0


Probability of 1: Losgann
5×5 Matrix{Float64}:
 0.0   0.14  0.14  0.0   0.0
 0.14  0.14  0.14  0.0   0.0
 0.14  0.14  0.0   0.14  0.14
 0.0   0.0   0.14  0.14  0.14
 0.0   0.0   0.14  0.14  0.0


Probability of 2: Garcius
5×5 Matrix{Float64}:
 0.0   0.14  0.14  0.0   0.0
 0.14  0.14  0.14  0.0   0.0
 0.14  0.14  0.0   0.14  0.14
 0.0   0.0   0.14  0.14  0.14
 0.0   0.0   0.14  0.14  0.0


Expected information
5×5 Matrix{Float64}:
 0.0   1.15  1.15  0.0   0.0
 1.15  1.15  1.15  0.0   0.0
 1.15  1.15  0.0   1.15  1.15
 0.0   0.0   1.15  1.15  1.15
 0.0   0.0   1.15  1.15  0.0


42 possibilities remaining

Enter <row><col><val>, <display> to show possibilities, or <enter> to exit: 122
Probability empty
5×5 Matrix{Float64}:
 1.0   0.0   0.83  1.0   1.0
 0.83  0.83  0.83  1.0   1.0
 0.83  0.83  1.0   0.0   0.83
 1.0   1.0   0.83  0.83  0.83
 1.0   1.0   0.83  0.83  1.0


Probability of 1: Losgann
5×5 Matrix{Float64}:
 0.0   0.0   0.17  0.0   0.0
 0.17  0.17  0.17  0.0   0.0
 0.17  0.17  0.0   0.0   0.17
 0.0   0.0   0.17  0.17  0.17
 0.0   0.0   0.17  0.17  0.0



*** SOLVED ***: 2: Garcius

5×5 Matrix{Float64}:
 0.0  1.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  1.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0


Expected information
5×5 Matrix{Float64}:
 0.0   0.0   0.65  0.0   0.0
 0.65  0.65  0.65  0.0   0.0
 0.65  0.65  0.0   0.0   0.65
 0.0   0.0   0.65  0.65  0.65
 0.0   0.0   0.65  0.65  0.0


6 possibilities remaining

Enter <row><col><val>, <display> to show possibilities, or <enter> to exit: 130
Probability empty
5×5 Matrix{Float64}:
 1.0  0.0  1.0  1.0  1.0
 0.8  0.8  0.8  1.0  1.0
 0.8  0.8  1.0  0.0  1.0
 1.0  1.0  0.8  0.8  0.8
 1.0  1.0  0.8  0.8  1.0


Probability of 1: Losgann
5×5 Matrix{Float64}:
 0.0  0.0  0.0  0.0  0.0
 0.2  0.2  0.2  0.0  0.0
 0.2  0.2  0.0  0.0  0.0
 0.0  0.0  0.2  0.2  0.2
 0.0  0.0  0.2  0.2  0.0



*** SOLVED ***: 2: Garcius

5×5 Matrix{Float64}:
 0.0  1.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  1.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0


Expected information
5×5 Matrix{Float64}:
 0.0   0.0   0.0   0.0   0.0
 0.72  0.72  0.72  0.0   0.0
 0.72  0.72  0.0   0.0   0.0
 0.0   0.0   0.72  0.72  0.72
 0.0   0.0   0.72  0.72  0.0


5 possibilities remaining

Enter <row><col><val>, <display> to show possibilities, or <enter> to exit: 211
Probability empty
5×5 Matrix{Float64}:
 1.0  0.0  1.0  1.0  1.0
 0.0  1.0  1.0  1.0  1.0
 1.0  1.0  1.0  0.0  1.0
 1.0  1.0  0.0  1.0  1.0
 1.0  1.0  1.0  1.0  1.0



*** SOLVED ***: 1: Losgann

5×5 Matrix{Float64}:
 0.0  0.0  0.0  0.0  0.0
 1.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  1.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0



*** SOLVED ***: 2: Garcius

5×5 Matrix{Float64}:
 0.0  1.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  1.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0


Expected information
5×5 Matrix{Float64}:
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0


1 possibilities remaining



.2...   
1....   
...2.   
..1..   
.....   

1 different possibilities
Set{Matrix{Int64}} with 1 element:
  [0 2 … 0 0; 1 0 … 0 0; … ; 0 0 … 0 0; 0 0 … 0 0]

julia> 

Julia code

# Pocket Frogs and Disco Zoo search minigame solver
#
# - v1.01, 2026-02-05, Joseph Chou
#
# - for Pocket Frogs (https://en.wikipedia.org/wiki/Pocket_Frogs) and Disco Zoo (https://en.wikipedia.org/wiki/Disco_Zoo)
# - analyzes all possible solutions
# - calculates probability at given location for each target
# - calculates expected information for each location for reducing possibilities
#

# Search game patterns
# - Pocket Frogs: https://docs.google.com/spreadsheets/d/1UeN9LeU5ko1fwAPwPaFIkL8yLk6wc95Lyq1c4rEeiNA/edit?gid=1783165384#gid=1783165384
# - Disco Zoo: https://discozoo.fandom.com/wiki/ 
#
# Information theory: 
# - 3Blue1Brown Wordle information theory: https://www.youtube.com/watch?v=v68zYyaEmEA
# - I = -log2(p), where p = probability of target "present" with this test
# - expected information: summation [ p(x) • log2(1/p(x)) ] over all the possibilities = Entropy


# Global Variables -- frog_patterns and zoo_patterns dictionaries
# - key = species
# - values = 0-based pattern offsets as matrix, each row one location
frog_patterns = Dict(
    "Porto"     => [0 0; 0 1; 1 0; 1 1], # level 10
    "Cerebrum"  => [0 0; 0 2; 1 1],      # level 20
    "Garcius"   => [0 0; 2 2],           # level 36
    "Anura"     => [0 0; 0 3; 1 0; 1 3],
    "Crustalli" => [0 0; 0 1; 0 2; 1 1],
    "Nasus"     => [0 0; 0 1; 0 2; 1 1],
    "Velatus"   => [0 3; 1 0; 1 1; 1 2],
    "Bovis"     => [0 3; 1 0; 1 1; 1 2],
    "Clunicula" => [0 1; 0 2; 1 0; 1 3],
    "Marmorea"  => [0 0; 0 3; 1 1; 2 1],
    "Puncti"    => [0 0; 0 3; 3 0; 3 3],
    "Zebrae"    => [0 0; 0 1; 1 1; 1 2],
    "Adamantis" => [0 1; 1 0; 1 2; 2 1],
    "Stellata"  => [0 1; 1 0; 1 2; 2 1],
    "Africanus" => [0 2; 1 0; 2 3; 3 1],
    "Partiri"   => [0 0; 0 1; 0 2; 0 3],
    "Roboris"   => [0 0; 0 2; 1 1; 2 1],
    "Mixtus"    => [0 0; 1 0; 1 3; 2 1],
    "Calyx"     => [0 0; 1 0; 2 0; 3 0],
    "Cesti"     => [0 0; 1 0; 2 0; 3 0],
    "Viduo"     => [0 0; 0 1; 2 0; 2 1],
    "Serpentis" => [0 1; 1 0; 2 1; 3 0],
    "Figularis" => [0 1; 1 0; 2 1; 3 0],
    "Spinae"    => [0 1; 2 0; 2 1; 2 2],
    "Ocularis"  => [0 1; 2 0; 2 1; 2 2],
    "Nimbilis"  => [0 0; 0 1; 1 0; 1 1],
    "Lunaris"   => [0 0; 0 1; 1 0; 1 1],
    "Sagitta"   => [0 1; 1 0; 1 2; 3 1],
    "Amfractus" => [0 1; 1 0; 2 0; 3 1],
    "Ornatus"   => [0 2; 1 1; 2 0; 3 0],
    "Sol"       => [0 3; 1 2; 2 0; 3 1],
    "Lucus"     => [0 1; 1 0; 1 2; 1 3],
    "Ligo"      => [0 3; 1 2; 2 1; 3 0],
    "Corona"    => [0 0; 1 1; 2 1; 3 0],
    "Arbor"     => [0 0; 1 1; 2 2; 3 2],
    "Insero"    => [0 0; 0 1; 2 0; 3 1],
    "Biplex"    => [0 1; 0 2; 1 0; 2 2],
    "Pingo"     => [0 0; 1 1; 2 0; 2 2],
    "Calvaria"  => [0 0; 0 3; 1 1; 1 2],
    "Floresco"  => [0 0; 0 1; 1 1; 2 0],
    "Magus"     => [0 1; 0 2; 2 0; 2 3],
    "Veru"      => [0 0; 1 1; 2 1; 3 1],
    "Templum"   => [0 1; 1 1; 2 0; 2 2],
    "Papilio"   => [0 0; 1 0; 1 1; 2 0],
    "Pluma"     => [0 0; 0 1; 1 2; 1 3],
    "Cornus"    => [0 0; 0 1; 1 1; 2 2],
    "Volta"     => [0 1; 0 3; 1 0; 1 2],
    "Flecto"    => [0 0; 1 1; 1 3; 2 2],
    "Aceris"    => [0 1; 1 0; 1 1; 1 2],
    "Tabula"    => [0 1; 1 0; 3 2],
    "Pictoris"  => [0 0; 0 3; 1 2],
    "Shelbus"   => [0 0; 1 0; 1 2],
    "Skeletos"  => [0 0; 0 3; 2 1],
    "Pyramis"   => [0 3; 1 0; 2 1],
    "Lotus"     => [0 0; 1 1; 2 2],
    "Mustacium" => [0 0; 1 1; 2 2],
    "Spira"     => [0 2; 1 0; 2 1],
    "Planeta"   => [0 0; 0 1; 2 2],
    "Mazeus"    => [0 0; 1 1; 2 3],
    "Palma"     => [0 1; 2 0; 2 2],
    "Coclearis" => [0 1; 3 0; 3 2],
    "Axis"      => [0 1; 3 0; 3 2],
    "Igneous"   => [0 1; 2 1; 3 0],
    "Infinitas" => [0 1; 1 0; 1 2],
    "Janus"     => [0 1; 1 0; 1 2],
    "Gemma"     => [0 1; 1 0; 1 2],
    "Nodare"    => [0 1; 1 0; 2 0],
    "Dimidius"  => [0 1; 1 0; 2 1],
    "Scutulata" => [0 0; 0 2; 2 2],
    "Arcus"     => [0 0; 2 1; 3 2],
    "Botulus"   => [0 0; 1 1; 2 0],
    "Americano" => [0 0; 0 1; 2 0],
    "Symphonia" => [0 0; 0 2; 1 0],
    "Vicis"     => [0 0; 2 1; 3 0],
    "Levar"     => [0 2; 1 0; 1 2],
    "Favus"     => [0 2; 1 0; 2 3],
    "Tessera"   => [0 2; 2 0; 3 1],
    "Frondis"   => [0 1; 1 2; 2 0],
    "Pistrix"   => [0 1; 2 0; 2 3],
    "Nebula"    => [0 1; 3 0],
    "Fractus"   => [0 1; 2 0],
    "Spargo"    => [0 1; 2 0],
    "Hennae"    => [0 1; 1 0],
    "Fortuno"   => [0 1; 1 0],
    "Lentium"   => [0 1; 1 0],
    "Pulvillus" => [0 0; 2 2],
    "Foramen"   => [0 0; 2 2],
    "Quilta"    => [0 0; 1 1],
    "Emblema"   => [0 2; 2 0],
    "Splendico" => [0 0; 0 3],
    "Hexas"     => [0 0; 2 3],
    "Latus"     => [0 0; 0 2],
    "Altilium"  => [0 1; 2 1],
    "Procursus" => [0 0; 3 0],
    "Stillas"   => [0 0; 1 2],
    # "Pompeius"  => [0 2; 2 0; 3 1],
    "Pompeius"  => [0 2; 3 0],
    "Inverso"   => [0 2; 3 0],
    "Exclamo"   => [0 0; 1 0],
    "Garcius"   => [0 0; 2 2],
    "Conexus"   => [0 1; 2 0],
    "Losgann"   => [0 0; 2 2],
    "Topologia" => [0 2; 3 0]
)

zoo_patterns = Dict(
    # Farm region
    "Pig"          => [0 0; 0 1; 1 0; 1 1],
    "Sheep"        => [0 0; 0 1; 0 2; 0 3],
    "Rabbit"       => [0 0; 1 0; 2 0; 3 0],
    "Cow"          => [0 0; 0 1; 0 2],
    "Horse"        => [0 0; 1 0; 2 0],
    "Unicorn"      => [0 0; 1 1; 1 2],
    "Chicken"      => [0 2; 1 1; 2 0],
    # Outback region
    "Kangaroo"     => [0 0; 1 1; 2 2; 3 3],
    "Crocodile"    => [0 0; 0 1; 0 2; 0 3],
    "Platypus"     => [0 0; 0 1; 1 1; 1 2],
    "Koala"        => [0 0; 0 1; 1 1],
    "Cockatoo"     => [0 0; 1 1; 2 1],
    "Tiddaklik"    => [0 1; 1 0; 1 2],
    "Echidna"      => [0 2; 1 0; 1 1],
    # Savanna region
    "Zebra"        => [0 1; 1 0; 1 2; 2 1],
    "Hippo"        => [0 0; 0 2; 2 0; 2 2],
    "Giraffe"      => [0 0; 1 0; 2 0; 3 0],
    "Lion"         => [0 0; 0 1; 0 2],
    "Elephant"     => [0 0; 0 1; 1 0],
    "Gryphon"      => [0 0; 0 2; 1 1],
    "Rhinoceros"   => [0 1; 1 0; 2 1],
    # Northern region
    "Bear"         => [0 0; 0 1; 1 1; 2 1],
    "Skunk"        => [0 1; 0 2; 1 0; 1 1],
    "Beaver"       => [0 2; 1 0; 1 1; 2 2],
    "Moose"        => [0 0; 0 2; 1 1],
    "Fox"          => [0 0; 0 1; 1 2],
    "Sasquatch"    => [0 0; 1 0],
    "Otter"        => [0 0; 1 0; 1 1],
    # Polar region
    "Penguin"      => [0 1; 1 1; 2 0; 2 2],
    "Seal"         => [0 0; 1 1; 1 3; 2 2],
    "Muskox"       => [0 0; 0 1; 1 0; 1 2],
    "Polar Bear"   => [0 0; 0 2; 1 2],
    "Walrus"       => [0 0; 1 1; 1 2],
    "Yeti"         => [0 0; 2 0],
    "Snowy Owl"    => [0 0; 0 1; 1 1],
    # Jungle region
    "Toucan"       => [0 1; 1 0; 2 1; 3 1],
    "Monkey"       => [0 0; 0 2; 1 1; 1 3],
    "Gorilla"      => [0 0; 0 2; 1 0; 1 2],
    "Panda"        => [0 2; 1 0; 2 2],
    "Tiger"        => [0 0; 0 2; 0 3],
    "Phoenix"      => [0 0; 2 2],
    "Lemur"        => [0 0; 1 1; 2 0],
    # Jurassic region
    "Diplodocus"   => [0 0; 1 1; 1 2; 2 1],
    "Stegosaurus"  => [0 1; 0 2; 1 0; 1 3],
    "Raptors"      => [0 0; 0 1; 1 1; 2 2],
    "T-Rex"        => [0 0; 2 0; 2 1],
    "Triceratops"  => [0 0; 1 2; 2 0],
    "Dragon"       => [0 0; 1 2],
    "Ankylosaurus" => [0 2; 1 0; 1 2],
    # Ice Age region
    "Giant Sloth"  => [0 0; 1 2; 2 0; 2 2],
    "Wooly Rhino"  => [0 2; 1 0; 1 3; 2 1],
    "Dire Wolf"    => [0 1; 1 0; 1 3; 2 1],
    "Saber Tooth"  => [0 0; 1 2; 2 1],
    "Mammoth"      => [0 1; 1 0; 2 2],
    "Akhlut"       => [0 2; 1 0; 2 2],
    "Yukon Camel"  => [0 2; 1 0; 2 3],
    # City region
    "Raccoon"      => [0 0; 0 2; 1 0; 1 3],
    "Pigeon"       => [0 0; 1 1; 2 1; 2 2],
    "Rat"          => [0 0; 0 1; 1 1; 1 3],
    "Squirrel"     => [0 2; 1 0; 2 1],
    "Opossum"      => [0 0; 1 0; 1 2],
    "Sewer Turtle" => [0 0; 0 1],
    "Chipmunk"     => [0 1; 1 0; 1 3],
    # Mountain region
    "Goat"         => [0 0; 1 0; 1 1; 1 2],
    "Cougar"       => [0 0; 1 1; 2 0; 2 2],
    "Elk"          => [0 0; 0 2; 1 1; 1 2],
    "Eagle"        => [0 0; 1 0; 2 1],
    "Coyote"       => [0 0; 0 1; 1 2],
    "Aatxe"        => [0 2; 1 0],
    "Pika"         => [0 0; 0 2; 1 2],
    # Nocturnal region: https://discozoo.fandom.com/wiki/Nocturnal
    "Badger"          => [0 2; 1 0; 1 2; 2 0],
    "Bat"             => [0 0; 0 2; 1 1; 2 1],
    "Kiwi"            => [0 1; 1 0; 1 2; 2 2],
    "Flying squirrel" => [0 2; 1 0; 1 3],
    "Kakapo"          => [0 0; 0 1; 2 1],
    "Ghost"           => [0 0; 1 1],
    "Firefly"         => [0 0; 1 0; 2 2],
    # Moon region
    "Moonkey"         => [0 0; 1 0; 1 2; 2 2],
    "Lunar tick"      => [0 1; 2 1; 3 0; 3 2],
    "Tribble"         => [0 1; 1 0; 1 1; 1 2],
     "Moonicorn"      => [0 0; 1 0; 1 1],
    "Luna moth"       => [0 0; 0 2; 2 1],
    "Jade rabbit"     => [0 0; 2 1],
    "Babmoon"         => [0 1; 1 2; 2 0],
    # Mars region
    "Marsmot"         => [0 1; 1 1; 2 0; 2 1],
    "Marsmoset"       => [0 0; 0 2; 1 2; 2 1],
    "Rock"            => [0 0; 0 1; 1 0; 1 1],
    "Rover"           => [0 1; 1 0; 1 2],
    "Martian"         => [0 0; 0 2; 1 1],
    "Marsmallow"      => [0 0; 2 0],
    "Marsten"         => [0 0; 0 2; 1 3],
    # Constellation region
    "Chamaeleon"  => [0 1; 0 3; 1 0; 1 1],
    "Corvus"      => [0 1; 0 2; 2 0; 2 2],
    "Lynx"        => [0 1; 1 0; 1 1; 2 0],
    "Pisces"      => [0 1; 1 2; 2 0],
    "Capricornus" => [0 0; 0 3; 1 2],
    "Pegasus"     => [0 2; 2 0],
    "Horologium"  => [0 0; 1 2; 2 2],
    # Pets? https://discozoo.fandom.com/wiki/Pets -- incomplete or inaccurate?
    "nothing"     => [0 0],
    "pet_rabbit"  => [0 0; 0 2; 1 0; 1 2; 2 1],
    "pet_bird"    => [0 0; 0 2; 0 4; 1 1; 1 3],
    "pet_dog"     => [0 1; 1 0; 1 1; 2 0; 2 1],
    "pet_cat"     => [0 0; 0 1; 1 0; 2 0; 2 1],
    "pet_fish"    => [0 3; 1 0; 1 1; 1 2; 2 3],
    "pet_turtle"  => [0 0; 0 1; 0 2; 1 1; 2 1],
    "pet_lizard"  => [0 0; 1 0; 2 0; 2 1; 2 2],
    "pet_hamster" => [0 0; 0 2; 1 1; 2 0; 2 2]
)

function match_string(input, possibilities)
    # Slightly fuzzy matching function for input within possibilities
    #
    # Complete full match -- highest priority (in case short full name contained within other names)
    matches = filter(s -> occursin(Regex("^"*input*"\$", "i"), s), possibilities) |> collect
    if length(matches) == 1
        return(matches[1])
    end

    # Any match, anchored to beginning
    matches = filter(s -> occursin(Regex("^"*input, "i"), s), possibilities) |> collect
    # if lowercase(input) ∈ lowercase.(possibilities)
    if length(matches) == 1
        match = matches[1]
        return(match)
    else
        print("No unique match for '$input': $matches\n\n")
        return ""
    end
end

function get_input(possibilities, prompt = "")
    # Prompt for comma-separated input of the target species and return array of matched species
    # - uses match_string() function for fuzzy, case-insensitive matching
    print(prompt)
    inputs = [match_string(strip(s), possibilities) for s in split(readline(), ",")]
    if all(s -> s != "", inputs)
        return inputs
    else
        return String[] # length 0 if any invalid
    end
end

function goal_starts(type, number, patterns)
    # Create all possible pattern starts for "type", assigning value "number" in positions of a 5x5 matrix
    # - cycle through all positions and place pattern if legal
    # - returns array of 5x5 matrices (each containing a possible pattern location)
    pattern = patterns[type]
    s_valid = Matrix{Int64}[]
    for i in 1:5
        for j in 1:5
            if (maximum(pattern[:,1]) + i > 5) | (maximum(pattern[:,2]) + j > 5)
                continue # Skip if highest row and col offsets out of matrix bounds
            else
                m = zeros(Int, 5, 5)
                for loc in eachrow(pattern)
                    m[i + loc[1], j + loc[2]] = number
                end
                push!(s_valid, copy(m))
            end
        end
    end
    return s_valid
end

function check_valid_combo(combo)
    # Determine whether an interable of starting patterns locations (`combo`) can exist on grid without position conflicts
    # - each element of `combo` is a 5x5 matrix containing 0's at empty locations and a # where the pattern is
    # - return `true` if all elements of `combo` can co-exist on the grid without positiojn conflicts
    for i in 1:5
        for j in 1:5
            vals = [x[i, j] for x in combo]
            if length([x for x in unique(vals) if x != 0]) > 1
                return false # conflict at position i,j
            end
        end
    end
    return true
end

function game_starts(goals, patterns)
    # Given a list of goal species, return a set of all possible game matrices with all goals placed
    # - this is basically returning a set of ALL possible starting positions
    # - later, each move will filter the set to remaining possible positions
    goal_starts_array = [goal_starts(type, i, patterns) for(i, type) in enumerate(goals)]
    s_all = Set{Matrix{Int64}}() # Set of all possibilities
    for combo in Iterators.product(goal_starts_array...) # tuples
        if check_valid_combo(combo)
            push!(s_all, sum(combo))
        end
    end
    return s_all
end

function game_move(game_set, row, col, val)
    # Given a set of possible game positions and a value at a given row/col coordinate, return all possible remaining sets
    # - after a position is checked, position `row` and `col` will be known to contain `val` (0 for empty, # for species)
    # - the function will remove from the set of remaining possible starting positions those that conflict with row/col/val
    game_filter = Set{Matrix{Int64}}() # empty set
    for s in game_set
        if s[row, col] == val
            push!(game_filter, s)
        end
    end
    if length(game_filter) == 0
        print("\n*** ERROR *** no valid positions -- ignoring move\n\n")
        return(game_set) # return original input -- data entry error at some point
    else
        return(game_filter) # return remaining possibilities
    end
end

function game_prob(game_set, value; num_digits = 2)
    # For a set of matrices of all game possibilities, return matrix of probability of value at each position in matrix
    # - from the remaining possibilities, return a 5x5 matrix that returns the probability of `value` appearing at each position
    probs = round.(sum([m .== value for m in game_set]) ./ length(game_set); digits = num_digits)
    return probs
end

function game_info(game_set, goals)
    # Return expected information content of each position in remaining positions
    #
    # From 3Blue1Brown Wordle information theory: https://www.youtube.com/watch?v=v68zYyaEmEA
    # - I = -log2(p), where p = probability of target "present" with this test
    # - expected information: summation [ p(x) • log2(1/p(x)) ] over all the possibilities = Entropy
    x = [ sum([m .== value for m in game_set]) ./ length(game_set) for value in 0:length(goals) ]
    info_matrix = [map(x -> x == 0.0 ? 0.0 : -log2(x), matrix) for matrix in x]
    prob_matrix = [game_prob(game_set, goal; num_digits = 3) for goal in 0:length(goals)] # this will be ONE-indexed though
    sum([ a.* b for (a,b) in zip(info_matrix, prob_matrix)])
end

function print_grid(game_set; per_row = 10)
    # display set of all possible game positions, in a grid for compactness
    games = collect(game_set)
    n = length(games)
    for batch_start in 1:per_row:n
        batch = games[batch_start:min(batch_start+per_row-1, n)]
        rows = size(batch[1], 1)  # assumes all game are the same size
        for r in 1:rows
            for m in batch
                line = join(map(x -> x == 0 ? "." : x, m[r, :]), "")
                print(line, "   ")
            end
            println()
        end
        println()  # blank line between matrix rows
    end
    println("$n different possibilities")
end

function game_status(game, goals)
    # Given remaining game matrix possibilities and vector of goal strings:
    # - display probabilities for each goal
    # - display expected information content
    for goal in 0:length(goals) # 0:length(goals) if want to show probability of empties
        probs = game_prob(game, goal)
        if goal == 0
            println("Probability empty")
        else
            if (all(x -> x in (0.0, 1.0), probs))
                println("\n*** SOLVED ***: $goal: $(goals[goal])\n")
            else
                println("Probability of $goal: $(goals[goal])")
            end
        end
        display(probs)
        println("\n")
    end
    println("Expected information")
    display(round.(game_info(game, goals), digits = 2))
    println("\n")
    println(string(length(game)) * " possibilities remaining\n")
end

function play_game(;game_mode = "frog") # game_mode = "zoo" or "frog"
    # Wrapper function for the entire game loop
    # - returns an array of remaining game possibilities
    # - call with `;` to silence showing return value
    if game_mode == "zoo"
        patterns = zoo_patterns
    else
        patterns = frog_patterns
    end
    possibilities = keys(patterns) # global variable needed for all possible goals
    goals = String[]
    while true
        goals = get_input(possibilities, "Enter comma-separated list of species: ")
        if length(goals) != 0
            break # something matched, so continue to game play
        end
    end
    game = game_starts(goals, patterns) # generate set of all possible starting positions
    game_status(game, goals) # display current status
    println("* <row> and <col> range from 1 to 5\n* <val> is 0 for empty space or # for goal number\n* <display> shows all remaining possibilities\n")
    while length(game) != 1
        print("Enter <row><col><val>, <display> to show possibilities, or <enter> to exit: ")
        line = readline()
        if line == ""
            break
        elseif line == "display"
            print_grid(game; per_row = 10) # show all remaining possibilities for game positions
        else
            game = game_move(game, parse.(Int, collect(line))...) # parse string ### into array of 3 Int of row / col / val
            game_status(game, goals) # display current status
        end
    end
    print("\n\n")
    print_grid(game)
    return game
end