Black box

From Rosetta Code
Black box is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

Implement a version of the Black Box game beginners configuration: 4 Atoms in an 8 x 8 grid.

Determine where the hidden atoms are in the box, by observing how the light beams fired into the box react when leaving it.
Possible results:
'H': the beam hit an atom and stopped
'R': Either the beam was reflected back the way it came or there was a ball just to one side of its entry point
'Numbers': indicate that the beam entered one of those squares and emerged from the other


Extra credit (Different game types):
-More or less atoms (maybe random)
-Different grid sizes

AutoHotkey

SetBatchLines -1
;--------------------------------------------------------------------------------------
BoardSize := 8
GUI()
setupBoard()
OnMessage(0x0201, "WM_LBUTTONDOWN")
return
;--------------------------------------------------------------------------------------
GUI(){
    global
    BoardSize += 2                ; add left/right and top/buttom
    lastIndex := BoardSize-1    ; 0-based
    symbol := {}, w := h := 30
    Menu, FileMenu, Add, Manual Entry, MenuHandler
    Menu, FileMenu, Add, E&xit, MenuHandler
    Menu, MyMenuBar, Add, &File, :FileMenu
    Gui, Menu, MyMenuBar
    Gui, font, s14, Consolas
    loop % BoardSize**2
    {
        r := (A_Index-1)//BoardSize, c := Mod(A_Index-1, BoardSize)
        options := r = 0            ?    " v" r "_" c " gSendRay"
                :  c = 0            ?    " v" r "_" c " gSendRay"
                :  c = lastIndex    ?    " v" r "_" c " gSendRay"
                :  r = lastIndex    ?    " v" r "_" c " gSendRay"
                :                        " v" r "_" c
        
        if (c = 0 && r = 0)
            Gui, add, button, % "section x14 y14 w" w " h" h options
        else if c = 0
            Gui, add, button, % "section x14 y+0 w" w " h" h options
        else
            Gui, add, button, % "x+0 w" w " h" h options
    }
    for i, v in StrSplit("0_0,0_" lastIndex "," lastIndex "_0," lastIndex "_" lastIndex "", ",")
        GuiControl, hide, % v
    Gui, font, s10, Consolas
    Gui, add, button, xs w80 vButtonDone gDone Disabled, % ButtonDoneText := "Done"
    Gui, add, text, x+10, % "?? = Hit, ? = Reflection"
    Gui, add, text, y+5 , % "Atoms Found = "
    Gui, add, text, x+0 vTextAtom w80
    Gui, +AlwaysOnTop
    Gui, show,, Black Box
}
;--------------------------------------------------------------------------------------
GuiClose:
ExitApp
return
;--------------------------------------------------------------------------------------
MenuHandler(){
    global
    if (A_ThisMenuItem = "Manual Entry")
    {
        Menu, FileMenu, ToggleCheck, Manual Entry
        if (Manual_Entry := !Manual_Entry)
            resetBoard()
        else
            Board := [], mapBoard()
    }
    if (A_ThisMenuItem = "E&xit")
        ExitApp
}
;--------------------------------------------------------------------------------------
setupBoard(){    ; land mines in random spots on PlayField
    global
    resetBoard()
    if Manual_Entry
        return

    Random, atoms, % Floor(BoardSize/2)-1, % Floor(BoardSize/2)
    ;~ atoms += 8
    loop % atoms
    {
        Random, rnd, 1, PlayField.Count()
        x := PlayField.RemoveAt(rnd)
        Mines[x.1, x.2] := true
    }
    mapBoard()
}
;--------------------------------------------------------------------------------------
resetBoard(){    ; Reset All
    global
    Board:=[], PlayField:=[], Mines:=[], Solution:=[], symbol:=[], found:=atoms:=0
    loop % BoardSize*4
        symbol.Push(Chr(0x0387+A_Index))
    loop % BoardSize**2
    {
        r := (A_Index-1)//BoardSize, c := Mod(A_Index-1, BoardSize)
        if (r>0 && r<lastIndex && c>0 && c<lastIndex)
            PlayField.Push([r , c])
    }
    mapBoard()
}
;--------------------------------------------------------------------------------------
mapBoard(){        ; map all buttons to reflect Board
    global
    loop % BoardSize**2
    {
        r := (A_Index-1)//BoardSize, c := Mod(A_Index-1, BoardSize)
        GuiControl,, % r "_" c, % ""
        GuiControl,, % r "_" c, % v := Board[r, c]
        if (r>0 && r<lastIndex && c>0 && c<lastIndex)
            GuiControl, % (v = "" || v = "?" || v = "+") ? "Disable" : "Enable", % r "_" c
    }
    GuiControl,, ButtonDone, % ButtonDoneText
    GuiControl,, TextAtom, % found " / " atoms
    GuiControl, % found = atoms ? "Enable" : "Disable", ButtonDone
}
;--------------------------------------------------------------------------------------
WM_LBUTTONDOWN(){
    global
    MouseGetPos, mx, my, mw, buttonNum
    buttonNum := StrReplace(buttonNum, "Button") - 1
    r := buttonNum//BoardSize, c := Mod(buttonNum, BoardSize)
    if !(R>0 && r<lastIndex && c>0 && c<lastIndex)
        return
    
    if Manual_Entry
    {
        Mines[r, c] := !Mines[r, c]
        Board[r, c] := Mines[r, c] ? "?" : ""
        atoms := Mines[r, c] ? atoms+1 : atoms-1
    }
    else
    {
        Solution[r, c] := !Solution[r, c]
        Board[r, c] := Solution[r, c] ? "??" : ""
        found := Board[r, c] ? found + 1 : found -1
    }
    mapBoard()
}
;--------------------------------------------------------------------------------------
Done(){
    global
    if (ButtonDoneText = "done")
    {
        ButtonDoneText := ":)"
        for r, obj in Solution
            for c, bool in obj
                if Solution[r, c] && (Mines[r, c] = Solution[r, c])
                    Board[r, c] := "?"    ; right
                else if Solution[r, c] && (Mines[r, c] <> Solution[r, c])
                    Board[r, c] := "?"    , ButtonDoneText := ":(" ; wrong marking
        for r, obj in Mines
            for c, bool in obj
                if Mines[r, c] && (Mines[r, c] <> Solution[r, c])
                    Board[r, c] := "?"    , ButtonDoneText := ":(" ; missed marking
        mapBoard()
    }
    else
    {
        ButtonDoneText := "Done"
        setupBoard()
    }
}
;--------------------------------------------------------------------------------------
SendRay(){
    global
    
    ; troubleshooting
    if TroubleShooting
    {
        loop % BoardSize**2
            r := (A_Index-1)//BoardSize, c := Mod(A_Index-1, BoardSize)
            , Board[r, c] := Board[r, c] = "+" ? "" : Board[r, c]
        mapBoard()
    }

    x := StrSplit(A_GuiControl, "_")
    r := x.1, c := x.2
    dir := (r = 0) ? "D" : (r = lastIndex) ? "U" : (c = 0) ? "R" : (c = lastIndex) ? "L" : ""
    t := Board[r, c]
    if (t && t<>"??" && t<>"?")
        symbol.Push(t)

    BlackBox([r, c, dir])
    mapBoard()
}
;--------------------------------------------------------------------------------------
BlackBox(Coord){
    global
    end := Ray(Coord)
    r := Coord.1, c := Coord.2
    endR := end.1, endC := end.2
    if (end.3 = "hit")
        Board[r, c] := "??"        ; Hit
    else if (r = endR && c = endC)
        Board[r, c] := "?"        ; Reflection
    else if (end.3 = "miss")
    {
        Random, rnd, 1, % symbol.Count()
        ch := symbol.RemoveAt(rnd)
        Board[r, c] := ch
        Board[endR, endC] := ch    ; Miss
    }
}
;--------------------------------------------------------------------------------------
Ray(Coord){
    global
    r := Coord.1, c := Coord.2, dir := Coord.3
    deltaR := dir = "D" ? 1 : dir = "U" ? -1 : 0
    deltaC := dir = "R" ? 1 : dir = "L" ? -1 : 0
    
    if TroubleShooting
    {
        Board[r, c] := "+"
        GuiControl,, % r "_" c, % "+"
        Sleep 5
    }
    
    ; Hit
    if (dir = "R" && Mines[r, c+1])
        return [r, c, "hit"]
    if (dir = "L" && Mines[r, c-1])
        return [r, c, "hit"]
    if (dir = "U" && Mines[r-1, c])
        return [r, c, "hit"]
    if (dir = "D" && Mines[r+1, c])
        return [r, c, "hit"]
    
    ; Deflection
    if (dir = "R" && Mines[r+1, c+1])
        return c=0 ? [r, c, "deflect"] : Ray([r, c, "U"])            ; right to up
    if (dir = "R" && Mines[r-1, c+1])
        return c=0 ? [r, c, "deflect"] : Ray([r, c, "D"])            ; right to down
    if (dir = "L" && Mines[r+1, c-1])
        return c=lastIndex ? [r, c, "deflect"] : Ray([r, c, "U"])    ; left to up
    if (dir = "L" && Mines[r-1, c-1])
        return c=lastIndex ? [r, c, "deflect"] : Ray([r, c, "D"])    ; left to down
    if (dir = "U" && Mines[r-1, c+1])
        return r=lastIndex ? [r, c, "deflect"] : Ray([r, c, "L"])    ; up to left
    if (dir = "U" && Mines[r-1, c-1])
        return r=lastIndex ? [r, c, "deflect"] : Ray([r, c, "R"])    ; up to down
    if (dir = "D" && Mines[r+1, c+1])
        return r=0 ? [r, c, "deflect"] : Ray([r, c, "L"])            ; down to left
    if (dir = "D" && Mines[r+1, c-1])
        return r=0 ? [r, c, "deflect"] : Ray([r, c, "R"])            ; down to right
    
    r += deltaR, c += deltaC                                        ; advance
    ; Miss
    if (r=0 || r=lastIndex || c=0 || c=lastIndex)
        return [r, c, "miss"]
    return Ray([r, c, dir])
}
;--------------------------------------------------------------------------------------
Alt::    ; for troubleshooting purposes only ;-)
TroubleShooting := !TroubleShooting
Gui, show,, % TroubleShooting ? "Black Box - TroubleShooting Mode" : "Black Box"
return
;--------------------------------------------------------------------------------------

Go

Terminal based game.

Just the basic configuration - 4 atoms in an 8 x 8 grid.

To test it against known output (as opposed to playing a sensible game), the program has been fixed (wikiGame = true) to reproduce the atom position in the Wikipedia article's example game, followed by a complete set of beams and one incorrect and three correct guesses.

Set wikiGame to false to play a normal 'random' game.

package main

import (
    "bufio"
    "fmt"
    "log"
    "math/rand"
    "os"
    "strings"
    "time"
)

var (
    b        = make([]rune, 100) // displayed board
    h        = make([]rune, 100) // hidden atoms
    scanner  = bufio.NewScanner(os.Stdin)
    wikiGame = true // set to false for a 'random' game
)

func initialize() {
    for i := 0; i < 100; i++ {
        b[i] = ' '
        h[i] = 'F'
    }
    if !wikiGame {
        hideAtoms()
    } else {
        h[32] = 'T'
        h[37] = 'T'
        h[64] = 'T'
        h[87] = 'T'
    }
    fmt.Println(`
    === BLACK BOX ===

    H    Hit (scores 1)
    R    Reflection (scores 1)
    1-9, Detour (scores 2)
    a-c  Detour for 10-12 (scores 2)
    G    Guess (maximum 4)
    Y    Correct guess
    N    Incorrect guess (scores 5)
    A    Unguessed atom
  
    Cells are numbered a0 to j9.
    Corner cells do nothing.
    Use edge cells to fire beam.
    Use middle cells to add/delete a guess.
    Game ends automatically after 4 guesses.
    Enter q to abort game at any time.
    `)
}

func drawGrid(score, guesses int) {
    fmt.Printf("      0   1   2   3   4   5   6   7   8   9 \n")
    fmt.Printf("\n")
    fmt.Printf("        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗\n")
    fmt.Printf("a     %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c\n",
        b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9])
    fmt.Printf("    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗\n")
    fmt.Printf("b   ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║\n",
        b[10], b[11], b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19])
    fmt.Printf("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣\n")
    fmt.Printf("c   ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║\n",
        b[20], b[21], b[22], b[23], b[24], b[25], b[26], b[27], b[28], b[29])
    fmt.Printf("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣\n")
    fmt.Printf("d   ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║\n",
        b[30], b[31], b[32], b[33], b[34], b[35], b[36], b[37], b[38], b[39])
    fmt.Printf("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣\n")
    fmt.Printf("e   ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║\n",
        b[40], b[41], b[42], b[43], b[44], b[45], b[46], b[47], b[48], b[49])
    fmt.Printf("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣\n")
    fmt.Printf("f   ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║\n",
        b[50], b[51], b[52], b[53], b[54], b[55], b[56], b[57], b[58], b[59])
    fmt.Printf("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣\n")
    fmt.Printf("g   ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║\n",
        b[60], b[61], b[62], b[63], b[64], b[65], b[66], b[67], b[68], b[69])
    fmt.Printf("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣\n")
    fmt.Printf("h   ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║\n",
        b[70], b[71], b[72], b[73], b[74], b[75], b[76], b[77], b[78], b[79])
    fmt.Printf("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣\n")
    fmt.Printf("i   ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║\n",
        b[80], b[81], b[82], b[83], b[84], b[85], b[86], b[87], b[88], b[89])
    fmt.Printf("    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝\n")
    fmt.Printf("j     %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c ║ %c\n",
        b[90], b[91], b[92], b[93], b[94], b[95], b[96], b[97], b[98], b[99])
    fmt.Printf("        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝\n")
    status := "In play"
    if guesses == 4 {
        status = "Game over!"
    }
    fmt.Println("\n        Score =", score, "\tGuesses =", guesses, "\t Status =", status, "\n")
}

func hideAtoms() {
    placed := 0
    for placed < 4 {
        a := 11 + rand.Intn(78) // 11 to 88 inclusive
        m := a % 10
        if m == 0 || m == 9 || h[a] == 'T' {
            continue
        }
        h[a] = 'T'
        placed++
    }
}

func nextCell() int {
    var ix int
    for {
        fmt.Print("    Choose cell : ")
        scanner.Scan()
        sq := strings.ToLower(scanner.Text())
        if len(sq) == 1 && sq[0] == 'q' {
            log.Fatal("program aborted")
        }
        if len(sq) != 2 || sq[0] < 'a' || sq[0] > 'j' || sq[1] < '0' || sq[1] > '9' {
            continue
        }
        ix = int((sq[0]-'a')*10 + sq[1] - 48)
        if atCorner(ix) {
            continue
        }
        break
    }
    check(scanner.Err())
    fmt.Println()
    return ix
}

func atCorner(ix int) bool { return ix == 0 || ix == 9 || ix == 90 || ix == 99 }

func inRange(ix int) bool { return ix >= 1 && ix <= 98 && ix != 9 && ix != 90 }

func atTop(ix int) bool { return ix >= 1 && ix <= 8 }

func atBottom(ix int) bool { return ix >= 91 && ix <= 98 }

func atLeft(ix int) bool { return inRange(ix) && ix%10 == 0 }

func atRight(ix int) bool { return inRange(ix) && ix%10 == 9 }

func inMiddle(ix int) bool {
    return inRange(ix) && !atTop(ix) && !atBottom(ix) && !atLeft(ix) && !atRight(ix)
}

func play() {
    score, guesses := 0, 0
    num := '0'
outer:
    for {
        drawGrid(score, guesses)
        ix := nextCell()
        if !inMiddle(ix) && b[ix] != ' ' { // already processed
            continue
        }
        var inc, def int
        switch {
        case atTop(ix):
            inc, def = 10, 1
        case atBottom(ix):
            inc, def = -10, 1
        case atLeft(ix):
            inc, def = 1, 10
        case atRight(ix):
            inc, def = -1, 10
        default:
            if b[ix] != 'G' {
                b[ix] = 'G'
                guesses++
                if guesses == 4 {
                    break outer
                }
            } else {
                b[ix] = ' '
                guesses--
            }
            continue
        }
        var x int
        first := true
        for x = ix + inc; inMiddle(x); x += inc {
            if h[x] == 'T' { // hit
                b[ix] = 'H'
                score++
                first = false
                continue outer
            }
            if first && (inMiddle(x+def) && h[x+def] == 'T') ||
                (inMiddle(x-def) && h[x-def] == 'T') { // reflection
                b[ix] = 'R'
                score++
                first = false
                continue outer
            }
            first = false
            y := x + inc - def
            if inMiddle(y) && h[y] == 'T' { // deflection
                switch inc {
                case 1, -1:
                    inc, def = 10, 1
                case 10, -10:
                    inc, def = 1, 10
                }
            }
            y = x + inc + def
            if inMiddle(y) && h[y] == 'T' { // deflection or double deflection
                switch inc {
                case 1, -1:
                    inc, def = -10, 1
                case 10, -10:
                    inc, def = -1, 10
                }
            }
        }
        if num != '9' {
            num++
        } else {
            num = 'a'
        }
        if b[ix] == ' ' {
            score++
        }
        b[ix] = num
        if inRange(x) {
            if ix == x {
                b[ix] = 'R'
            } else {
                if b[x] == ' ' {
                    score++
                }
                b[x] = num
            }
        }
    }
    drawGrid(score, guesses)
    finalScore(score, guesses)
}

func check(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func finalScore(score, guesses int) {
    for i := 11; i <= 88; i++ {
        m := i % 10
        if m == 0 || m == 9 {
            continue
        }
        if b[i] == 'G' && h[i] == 'T' {
            b[i] = 'Y'
        } else if b[i] == 'G' && h[i] == 'F' {
            b[i] = 'N'
            score += 5
        } else if b[i] == ' ' && h[i] == 'T' {
            b[i] = 'A'
        }
    }
    drawGrid(score, guesses)
}

func main() {
    rand.Seed(time.Now().UnixNano())
    for {
        initialize()
        play()
    inner:
        for {
            fmt.Print("    Play again y/n : ")
            scanner.Scan()
            yn := strings.ToLower(scanner.Text())
            switch yn {
            case "n":
                return
            case "y":
                break inner
            }
        }
        check(scanner.Err())
    }
}
Output:

As the grid is displayed 29 times in all, this has been abbreviated to show just the first 2 and the last 3.

    === BLACK BOX ===

    H    Hit (scores 1)
    R    Reflection (scores 1)
    1-9, Detour (scores 2)
    a-c  Detour for 10-12 (scores 2)
    G    Guess (maximum 4)
    Y    Correct guess
    N    Incorrect guess (scores 5)
    A    Unguessed atom
  
    Cells are numbered a0 to j9.
    Corner cells do nothing.
    Use edge cells to fire beam.
    Use middle cells to add/delete a guess.
    Game ends automatically after 4 guesses.
    Enter q to abort game at any time.
    
      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║   ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 0 	Guesses = 0 	 Status = In play 

    Choose cell : b0

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║   ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 2 	Guesses = 0 	 Status = In play 

    Choose cell : c0

................ (Screens 3 to 26 omitted) ................

        Score = 32 	Guesses = 2 	 Status = In play 

    Choose cell : g4

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║ H ║ 9 ║ H ║ 7 ║ 9 ║ H ║ 8 ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║ 8 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║ H ║ G ║   ║   ║   ║   ║   ║ G ║   ║ H ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║ 3 ║   ║   ║   ║   ║   ║   ║   ║   ║ 6 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║ 4 ║   ║   ║   ║   ║   ║   ║   ║   ║ 7 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║ H ║   ║   ║   ║ G ║   ║   ║   ║   ║ H ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║ 5 ║   ║   ║   ║   ║   ║   ║   ║   ║ 6 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║ H ║   ║   ║   ║   ║   ║   ║   ║   ║ H ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║ 3 ║ H ║ 5 ║ H ║ 4 ║ R ║ H ║ R ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 32 	Guesses = 3 	 Status = In play 

    Choose cell : i7

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║ H ║ 9 ║ H ║ 7 ║ 9 ║ H ║ 8 ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║ 8 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║ H ║ G ║   ║   ║   ║   ║   ║ G ║   ║ H ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║ 3 ║   ║   ║   ║   ║   ║   ║   ║   ║ 6 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║ 4 ║   ║   ║   ║   ║   ║   ║   ║   ║ 7 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║ H ║   ║   ║   ║ G ║   ║   ║   ║   ║ H ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║ 5 ║   ║   ║   ║   ║   ║   ║   ║   ║ 6 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║ H ║   ║   ║   ║   ║   ║   ║ G ║   ║ H ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║ 3 ║ H ║ 5 ║ H ║ 4 ║ R ║ H ║ R ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 32 	Guesses = 4 	 Status = Game over! 

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║ H ║ 9 ║ H ║ 7 ║ 9 ║ H ║ 8 ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║ 8 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║ H ║ N ║ A ║   ║   ║   ║   ║ Y ║   ║ H ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║ 3 ║   ║   ║   ║   ║   ║   ║   ║   ║ 6 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║ 4 ║   ║   ║   ║   ║   ║   ║   ║   ║ 7 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║ H ║   ║   ║   ║ Y ║   ║   ║   ║   ║ H ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║ 5 ║   ║   ║   ║   ║   ║   ║   ║   ║ 6 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║ H ║   ║   ║   ║   ║   ║   ║ Y ║   ║ H ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║ 3 ║ H ║ 5 ║ H ║ 4 ║ R ║ H ║ R ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 37 	Guesses = 4 	 Status = Game over! 

    Play again y/n : n

J

Requires a recent (release 9) jqt:
require'ide/qt/gl2'
coinsert'jgl2'

NB. event handlers
game_board_mbldown=: {{
  xy=.<.40%~2{._ ".sysdata
  if. xy e. EDGES do.
    probe xy
  elseif. xy e. BLACK do.
    guess xy
  end.
  draw''
}}

game_finish_button=: {{ draw FINISHED=: 1 [BEAM=: EMPTY }}

NB. support code
crds=: 2 {."1 ]
dirs=:_2 {."1 ]

probe=: {{
  BEAM=: ,:y
  dir=. DIRS{~EDGES i.y
  while. -. ({:BEAM) e. ATOMS do.
    bar=. (#~ 1-0 e.,@dirs)MIRRORS#~(crds MIRRORS) e. _1{.BEAM
    if. 1=#bar do. dir=.{.(/: |@j./"1) dir(+,:-),dirs bar
    elseif. 2=#bar do. dir=. -dir
    end.
    BEAM=: BEAM,dir+{:BEAM
    if. -.({:BEAM) e. BLACK do. break. end.
  end.
  if. 1 e. BEAM e. BLACK do.
    select. #e=.BEAM([-.-.)EDGES
      case. 1 do. BEAM remember 'H'
      case. 2 do. BEAM remember (#~.e){::'?';'R';0
    end.
  else.
    (BEAM=:1{.BEAM) remember 'R'
  end.
}}
remember=: {{
  ndx=. EDGES i.x([-.-.)EDGES
  if. 0=y do. y=. ":{.(0-.~~.0,,0".&>ndx{LABELS),1+>./0,,0".&>LABELS end.
  LABELS=: (<y) ndx} LABELS
}}  

guess=: {{ if.-.FINISHED do. GUESSES=: GUESSES ,`-.@.(e.~) y end. }}

NB. rendering
bbox=: {{
  4 bbox y
:
wd{{)n
  pc game closeok;
  cc message static center;
  cc board isidraw;
  set board wh SZ SZ;
  cc finish button;
  pshow;
}} rplc 'SZ';":40*2+SIZE=:y
  BLACK=: ,/1+DIM#:i.DIM=:,~SIZE
  EDGES=: (,/(2+DIM)#:i.2+DIM)-.BLACK,>,{;~0 1*DIM+1
  DIRS=: (1+SIZE) ((*@| |."1) * _1^=) EDGES
  LABELS=: (#EDGES)#a:
  ATOMS=: ({~ x?#) BLACK
  MIRRORS=: /:~,/ATOMS(+,])"1/0 0-.~>,{;~i:1
  GUESSES=: EMPTY
  BEAM=: EMPTY
  FINISHED=: 0
  draw''
}}

boxes=: {{ 40*4{.!.1"1 y }}
drawatoms=: {{ glellipse 5 5 _10 _10+"1]boxes y[glpen glbrush glrgb x }}

draw=: {{
  glclear''
  glfont '"Lucidia Console" 15' [gltextcolor glrgb 0 255 255 NB. yellow
  glpen 2 1[glrgb 3#255 NB. white
  glrect boxes EDGES [glbrush glrgb 184 0 0 NB. dark red
  glrect boxes BLACK [glbrush glrgb 0 0 0
  wd 'set message text ',N,&":' point','s'#~1~:N=.(FINISHED*5*#GUESSES-.ATOMS)++/LABELS~:a:
  if. FINISHED do.
    255 0 0 drawatoms GUESSES -. ATOMS
    0 255 0 drawatoms ATOMS([-.-.)GUESSES
    0 0 255 drawatoms ATOMS-.GUESSES
    if.#BEAM do.
      gllines 20+,40*BEAM [glpen 2 1 [glbrush glrgb 255 255 0 
      glellipse 15 15 10 10+4{.40*{.BEAM
    end.
  else.
    128 128 128 drawatoms GUESSES
  end.
  (10+40*EDGES) {{ gltext;y [ gltextxy x }}"_1 LABELS
  wd 'set finish enable ',":FINISHED<GUESSES=&#ATOMS
  glpaint''
}}
Example use:
   bbox 8
. Or, for 10 atoms in a 15x15 grid:
   10 bbox 15
.

This version allows the user to place and observe individual beams after finishing a game.

JavaScript

Play it here.

var sel, again, check, score, done, atoms, guesses, beamCnt, brdSize;

function updateScore( s ) {
    score += s || 0;
    para.innerHTML = "Score: " + score;
}
function checkIt() {
    check.className = "hide";
    again.className = "again";
    done = true;
    var b, id;
    for( var j = 0; j < brdSize; j++ ) {
        for( var i = 0; i < brdSize; i++ ) {
            if( board[i][j].H ) {
                b = document.getElementById( "atom" + ( i + j * brdSize ) );
                b.innerHTML = "&#x2688;";
                if( board[i][j].T ) {
                    b.style.color = "#0a2";
                } else {
                    b.style.color = "#f00";
                     updateScore( 5 );
                }
            } 
        }
    }
}
function isValid( n ) {
    return n > -1 && n < brdSize;
}
function stepBeam( sx, sy, dx, dy ) {
    var s = brdSize - 2
    if( dx ) {
        if( board[sx][sy].H ) return {r:"H", x:sx, y:sy};
        if( ( (sx == 1 && dx == 1) || (sx == s && dx == -1) ) && ( ( sy > 0 && board[sx][sy - 1].H ) || 
            ( sy < s && board[sx][sy + 1].H ) ) ) return {r:"R", x:sx, y:sy};
        if( isValid( sx + dx ) ) {
            if( isValid( sy - 1 ) && board[sx + dx][sy - 1].H ) {
                dx = 0; dy = 1;
            }
            if( isValid( sy + 1 ) && board[sx + dx][sy + 1].H ) {
                dx = 0; dy = -1;
            }
            sx += dx;
            return stepBeam( sx, sy, dx, dy );
        } else {
            return {r:"O", x:sx, y:sy};
        }
    } else {
        if( board[sx][sy].H ) return {r:"H", x:sx, y:sy}; 
        if( ( (sy == 1 && dy == 1) || (sy == s && dy == -1) ) && ( ( sx > 0 && board[sx - 1][sy].H ) || 
           ( sx < s && board[sx + 1][sy].H ) ) ) return {r:"R", x:sx, y:sy};
        if( isValid( sy + dy ) ) {
            if( isValid( sx - 1 ) && board[sx - 1][sy + dy].H ) {
                dy = 0; dx = 1;
            }
            if( isValid( sx + 1 ) && board[sx + 1][sy + dy].H ) {
                dy = 0; dx = -1;
            }
            sy += dy;
            return stepBeam( sx, sy, dx, dy );
        } else {
            return {r:"O", x:sx, y:sy};
        }
    }
}
function fireBeam( btn ) {
    var sx = btn.i, sy = btn.j, dx = 0, dy = 0;

    if( sx == 0 || sx == brdSize - 1 ) dx = sx == 0 ? 1 : - 1;
    else if( sy == 0 || sy == brdSize - 1 ) dy = sy == 0 ? 1 : - 1;
    var s = stepBeam( sx + dx, sy + dy, dx, dy );
    switch( s.r ) {
        case "H": 
            btn.innerHTML = "H"; 
            updateScore( 1 );
            break;
        case "R":
            btn.innerHTML = "R";
            updateScore( 1 );
            break;
        case "O":
            if( s.x == sx && s.y == sy ) {
                btn.innerHTML = "R";
                updateScore( 1 );
            }
            else {
                var b = document.getElementById( "fire" + ( s.x + s.y * brdSize ) );
                btn.innerHTML = "" + beamCnt;
                b.innerHTML = "" + beamCnt;
                beamCnt++;
                updateScore( 2 );
            }
    }
}
function setAtom( btn ) {
    if( done ) return;
    
    var b = document.getElementById( "atom" + ( btn.i + btn.j * brdSize ) );
    if( board[btn.i][btn.j].T == 0 && guesses < atoms ) {
        board[btn.i][btn.j].T = 1;
        guesses++;
        b.innerHTML = "&#x2688;";
    } else if( board[btn.i][btn.j].T == 1 && guesses > 0 ) {
        board[btn.i][btn.j].T = 0;
        guesses--;
        b.innerHTML = " ";
    }
    if( guesses == atoms ) check.className = "check";
    else check.className = "hide";
}
function startGame() {
    score = 0;
    updateScore();
    check.className = again.className = "hide";
    var e = document.getElementById( "mid" );
    if( e.firstChild ) e.removeChild( e.firstChild );
    
    brdSize = sel.value;
    done = false;

    if( brdSize < 5 ) return;

    var brd = document.createElement( "div" );
    brd.id = "board";
    brd.style.height = brd.style.width = 5.2 * brdSize + "vh"
    e.appendChild( brd );
    
    var b, c, d;
    for( var j = 0; j < brdSize; j++ ) {
        for( var i = 0; i < brdSize; i++ ) {
            b = document.createElement( "button" );
            b.i = i; b.j = j;
            if( j == 0 && i == 0 || j == 0 && i == brdSize - 1 ||
                j == brdSize - 1 && i == 0 || j == brdSize - 1 && i == brdSize - 1 ) {
                b.className = "corner";
            } else {
                if( j == 0 || j == brdSize - 1 || i == 0 || i == brdSize - 1 ) {
                    b.className = "fire";
                    b.id = "fire" + ( i + j * brdSize );
                } else {
                    b.className = "atom";
                    b.id = "atom" + ( i + j * brdSize );
                }
                b.addEventListener( "click", 
                    function( e ) {
                        if( e.target.className == "fire" && e.target.innerHTML == " " ) fireBeam( e.target );
                        else if( e.target.className == "atom" ) setAtom( e.target );
                    }, false );
            }
            b.appendChild( document.createTextNode( " " ) );
            brd.appendChild( b );
        }
    }

    board = new Array( brdSize );
    for( var j = 0; j < brdSize; j++ ) {
        board[j] = new Array( brdSize );
        for( i = 0; i < brdSize; i++ ) {
            board[j][i] = {H: 0, T: 0};
        }
    }

    guesses = 0; beamCnt = 1;
    atoms = brdSize == 7 ? 3 : brdSize == 10 ? 4 : 4 + Math.floor( Math.random() * 5 );

    var s = brdSize - 2, i, j;
    for( var k = 0; k < atoms; k++ ) {
        while( true ) {
            i = 1 + Math.floor( Math.random() * s );
            j = 1 + Math.floor( Math.random() * s );
            if( board[i][j].H == 0 ) break;
        }
        board[i][j].H = 1;
    }
}
function init() {
    sel = document.createElement( "select");
    sel.options.add( new Option( "5 x 5 [3 atoms]", 7 ) );
    sel.options.add( new Option( "8 x 8 [4 atoms]", 10 ) );
    sel.options.add( new Option( "10 x 10 [4 - 8 atoms]", 12 ) );
    sel.addEventListener( "change", startGame, false );
    document.getElementById( "top" ).appendChild( sel );
    
    check = document.createElement( "button" );
    check.appendChild( document.createTextNode( "Check it!" ) );
    check.className = "hide";
    check.addEventListener( "click", checkIt, false );
    
    again = document.createElement( "button" );
    again.appendChild( document.createTextNode( "Again" ) );
    again.className = "hide";
    again.addEventListener( "click", startGame, false );
    
    para = document.createElement( "p" );
    para.className = "txt";
    var d = document.getElementById( "bot" );
    
    d.appendChild( para );
    d.appendChild( check );
    d.appendChild( again );
    startGame();
}

Julia

Gtk library GUI version.

using Colors, Cairo, Graphics, Gtk

struct BoxPosition
    x::Int
    y::Int
    BoxPosition(i = 0, j = 0) = new(i, j)
end

@enum TrialResult Miss Hit Reflect Detour

struct TrialBeam
    entry::BoxPosition
    exit::Union{BoxPosition, Nothing}
    result::TrialResult
end

function blackboxapp(boxlength=8, boxwidth=8, numballs=4)
    r, turncount, guesses, guesscount, correctguesses = 20, 0, BoxPosition[], 0, 0
    showballs, boxoffsetx, boxoffsety = false, r, r
    boxes = fill(colorant"wheat", boxlength + 4, boxwidth + 4)
    beamhistory, ballpositions = Vector{TrialBeam}(), Vector{BoxPosition}()
    win = GtkWindow("Black Box Game", 348, 800) |> (GtkFrame() |> (box = GtkBox(:v)))
    settingsbox = GtkBox(:v)
    playtoolbar = GtkToolbar()

    newgame = GtkToolButton("New Game")
    set_gtk_property!(newgame, :label, "New Game")
    set_gtk_property!(newgame, :is_important, true)

    reveal = GtkToolButton("Reveal")
    set_gtk_property!(reveal, :label, "Reveal Box")
    set_gtk_property!(reveal, :is_important, true)

    map(w->push!(playtoolbar, w),[newgame, reveal])

    scrwin = GtkScrolledWindow()
    can = GtkCanvas()
    set_gtk_property!(can, :expand, true)
    map(w -> push!(box, w),[settingsbox, playtoolbar, scrwin])
    push!(scrwin, can)

    function newgame!(w)
        empty!(ballpositions)
        empty!(guesses)
        empty!(beamhistory)
        guessing, showballs, guesscount, correctguesses = false, false, 0, 0
        fill!(boxes, colorant"wheat")
        boxes[2, 3:end-2] .= boxes[end-1, 3:end-2] .= colorant"red"
        boxes[3:end-2, 2] .= boxes[3:end-2, end-1] .= colorant"red"
        boxes[3:end-2, 3:end-2] .= colorant"black"
        while length(ballpositions) < numballs
            p = BoxPosition(rand(3:boxlength+2), rand(3:boxwidth+2))
            if !(p in ballpositions)
                push!(ballpositions, p)
            end
        end
        draw(can)
    end

    @guarded draw(can) do widget
        ctx = Gtk.getgc(can)
        select_font_face(ctx, "Courier", Cairo.FONT_SLANT_NORMAL, Cairo.FONT_WEIGHT_BOLD)
        fontpointsize = 12
        set_font_size(ctx, fontpointsize)
        # print black box graphic
        for i in 1:boxlength + 4, j in 1:boxwidth + 4
            set_source(ctx, boxes[i, j])
            move_to(ctx, boxoffsetx + i * r, boxoffsety + j * r)
            rectangle(ctx, boxoffsetx + i * r, boxoffsety + j * r, r, r)
            fill(ctx)
            p = BoxPosition(i, j)
            # show current guesses
            if p in guesses
                set_source(ctx, colorant"red")
                move_to(ctx, boxoffsetx + i * r + 2, boxoffsety + j * r + fontpointsize)
                show_text(ctx, p in ballpositions ? "+" : "-")
                stroke(ctx)
            end
            # show ball placements if reveal -> showballs
            if showballs && p in ballpositions
                set_source(ctx, colorant"green")
                circle(ctx, boxoffsetx + (i + 0.5) * r , boxoffsety + (j + 0.5) * r, 0.4 * r)
                fill(ctx)
            end
        end
        # draw dividing lines
        set_line_width(ctx, 2)
        set_source(ctx, colorant"wheat")
        for i in 4:boxlength + 2
            move_to(ctx, boxoffsetx + i * r, boxoffsety + 3 * r)
            line_to(ctx, boxoffsetx + i * r, boxoffsety + (boxlength + 3) * r)
            stroke(ctx)
        end
        for j in 4:boxwidth + 2
            move_to(ctx, boxoffsetx + 3 * r, boxoffsety + j * r)
            line_to(ctx, boxoffsetx + (boxlength + 3) * r, boxoffsety + j * r)
            stroke(ctx)
        end
        # show scoring update
        set_source(ctx, colorant"white")
        rectangle(ctx, 0, 305, 400, 50)
        fill(ctx)
        correct, incorrect = string(correctguesses), string(guesscount - correctguesses)
        score = string(2 * correctguesses - guesscount)
        set_source(ctx, colorant"black")
        move_to(ctx, 0, 320)
        show_text(ctx, " Correct: $correct  Incorrect: $incorrect  Score: $score")
        stroke(ctx)
        # show latest trial beams and results and trial history
        set_source(ctx, colorant"white")
        rectangle(ctx, 0, 360, 400, 420)
        fill(ctx)
        set_source(ctx, colorant"black")
        move_to(ctx, 0, 360)
        show_text(ctx, "      Test Beam History")
        stroke(ctx)
        move_to(ctx, 0, 360 + fontpointsize * 1.5)
        show_text(ctx, " #  Start   Result      End")
        stroke(ctx)
        for (i, p) in enumerate(beamhistory)
            move_to(ctx, 0, 360 + fontpointsize * (i + 1.5))
            set_source(ctx, colorant"black")
            s = " " * rpad(i, 3) * rpad("($(p.entry.x - 2),$(p.entry.y - 2))", 8) * 
                rpad(p.result, 12) * (p.exit == nothing ? " " : 
                    "($(p.exit.x - 2), $(p.exit.y - 2))")
            show_text(ctx, s)
            stroke(ctx)
            move_to(ctx, graphicxyfrombox(p.entry, 0.5 * fontpointsize)...)
            set_source(ctx, colorant"yellow")
            show_text(ctx, string(i))
            stroke(ctx)
            if p.exit != nothing
                move_to(ctx, graphicxyfrombox(p.exit, 0.5 * fontpointsize)...)
                set_source(ctx, colorant"lightblue")
                show_text(ctx, string(i))
                stroke(ctx)
            end
        end
        Gtk.showall(win)
    end

    reveal!(w) = (showballs = !showballs; draw(can); Gtk.showall(win))
    boxfromgraphicxy(x, y) = Int(round(x / r - 1.5)), Int(round(y / r - 1.5))
    graphicxyfrombox(p, oset) = boxoffsetx + p.x * r + oset/2, boxoffsety + p.y * r + oset * 2
    dirnext(x, y, dir) = x + dir[1], y + dir[2]
    rightward(d) = (-d[2], d[1])
    leftward(d) = (d[2], -d[1])
    rearward(direction) = (-direction[1], -direction[2])
    ballfront(x, y, d) = BoxPosition(x + d[1], y + d[2]) in ballpositions
    ballright(x, y, d) = BoxPosition((dirnext(x, y, d) .+ rightward(d))...) in ballpositions
    ballleft(x, y, d) = BoxPosition((dirnext(x, y, d) .+ leftward(d))...) in ballpositions
    twocorners(x, y, d) = !ballfront(x, y, d) && ballright(x, y, d) && ballleft(x, y, d)
    enteringstartzone(x, y, direction) = atstart(dirnext(x, y, direction)...)

    function atstart(x, y)
        return ((x == 2 || x == boxlength + 3) && (2 < y <= boxwidth + 3)) ||
               ((y == 2 || y == boxwidth + 3) && (2 < x <= boxlength + 3))
    end

    function runpath(x, y)
        startp = BoxPosition(x, y)
        direction = (x == 2) ? (1, 0) : (x == boxlength + 3) ? (-1, 0) :
                    (y == 2) ? (0, 1) : (0, -1)
        while true
            if ballfront(x, y, direction)
                return Hit, nothing
            elseif twocorners(x, y, direction)
                if atstart(x, y)
                    return Reflect, startp
                end
                direction = rearward(direction)
                continue
            elseif ballleft(x, y, direction)
                if atstart(x, y)
                    return Reflect, startp
                end
                direction = rightward(direction)
                continue
            elseif ballright(x, y, direction)
                if atstart(x, y)
                    return Reflect, startp
                end
                direction = leftward(direction)
                continue
            elseif enteringstartzone(x, y, direction)
                x2, y2 = dirnext(x, y, direction)
                endp = BoxPosition(x2, y2)
                if x2 == startp.x && y2 == startp.y
                    return Reflect, endp
                else
                    if startp.x == x2 ||  startp.y == y2
                        return Miss, endp
                    else
                        return Detour, endp
                    end
                end
            end
            x, y = dirnext(x, y, direction)
            @assert((2 < x < boxlength + 3) && (2 < y < boxwidth + 3))
        end
    end

    can.mouse.button1press = @guarded (widget, event) -> begin
        x, y = boxfromgraphicxy(event.x, event.y)
        # get click in blackbox area as a guess
        if 2 < x < boxlength + 3 && 2 < y < boxwidth + 3
            p = BoxPosition(x, y)
            if !(p in guesses)
                push!(guesses, BoxPosition(x, y))
                guesscount += 1
                if p in ballpositions
                    correctguesses += 1
                end
            end
            draw(can)
        # test beam
        elseif atstart(x, y)
            result, endpoint = runpath(x, y)
            push!(beamhistory, TrialBeam(BoxPosition(x, y), endpoint, result))
            if length(beamhistory) > 32
                popfirst!(beamhistory)
            end
            draw(can)
        end
    end

    condition = Condition()
    endit(w) = notify(condition)
    signal_connect(endit, win, :destroy)
    signal_connect(newgame!, newgame, :clicked)
    signal_connect(reveal!, reveal, :clicked)

    newgame!(win)
    Gtk.showall(win)
    wait(condition)
end

blackboxapp()

Nim

Translation of: Go
import random, sequtils, strutils

const WikiGame = true

type

  Game = object
    b: array[100, char]   # displayed board.
    h: array[100, char]   # hidden atoms.


proc hideAtoms(game: var Game) =
  var placed = 0
  while placed < 4:
    let a = rand(11..88)
    let m = a mod 10
    if m == 0 or m == 9 or game.h[a] == 'T':
      continue
    game.h[a] = 'T'
    inc placed


proc initGame(): Game =
  for i in 0..99:
    result.b[i] = ' '
    result.h[i] = 'F'
  if not WikiGame:
    result.hideAtoms()
  else:
    result.h[32] = 'T'
    result.h[37] = 'T'
    result.h[64] = 'T'
    result.h[87] = 'T'


proc drawGrid(game: Game; score, guesses: Natural) =
  echo "      0   1   2   3   4   5   6   7   8   9\n"
  echo "        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗"
  echo "a     $#$#$#$#$#$#$#$#$#$#".format(game.b[0..9].mapIt($it))
  echo "    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗"
  echo "b   ║ $#$#$#$#$#$#$#$#$#$# ║".format(game.b[10..19].mapIt($it))
  echo "    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣"
  echo "c   ║ $#$#$#$#$#$#$#$#$#$# ║".format(game.b[20..29].mapIt($it))
  echo "    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣"
  echo "d   ║ $#$#$#$#$#$#$#$#$#$# ║".format(game.b[30..39].mapIt($it))
  echo "    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣"
  echo "e   ║ $#$#$#$#$#$#$#$#$#$# ║".format(game.b[40..49].mapIt($it))
  echo "    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣"
  echo "f   ║ $#$#$#$#$#$#$#$#$#$# ║".format(game.b[50..59].mapIt($it))
  echo "    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣"
  echo "g   ║ $#$#$#$#$#$#$#$#$#$# ║".format(game.b[60..69].mapIt($it))
  echo "    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣"
  echo "h   ║ $#$#$#$#$#$#$#$#$#$# ║".format(game.b[70..79].mapIt($it))
  echo "    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣"
  echo "i   ║ $#$#$#$#$#$#$#$#$#$# ║".format(game.b[80..89].mapIt($it))
  echo "    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝"
  echo "j     $#$#$#$#$#$#$#$#$#$#".format(game.b[90..99].mapIt($it))
  echo "        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝"

  let status = if guesses == 4: "Game over!" else: "In play"
  echo "\n        Score = ", score, "\tGuesses = ", guesses, "\t Status = ", status, '\n'


proc finalScore(game: var Game; score, guesses: Natural) =
  var score = score
  for i in 11..88:
    let m = i mod 10
    if m in [0, 9]: continue
    if game.b[i] == 'G':
      if game.h[i] == 'T':
        game.b[i] = 'Y'
      else:
        game.b[i] = 'N'
        inc score, 5
    elif game.b[i] == ' ' and game.h[i] == 'T':
      game.b[i] = 'A'
  game.drawGrid(score, guesses)


func atCorner(ix: int): bool = ix in [0, 9, 90, 99]

func inRange(ix: int): bool = ix in 1..98 and ix notin [9, 90]

func atTop(ix: int): bool = ix in 1..8

func atBottom(ix: int): bool = ix in 91..98

func atLeft(ix: int): bool = ix.inRange and ix mod 10 == 0

func atRight(ix: int): bool = ix.inRange and ix mod 10 == 9

func inMiddle(ix: int): bool =
  ix.inRange and not (ix.atTop or ix.atBottom or ix.atLeft or ix.atRight)


proc nextCell(game: Game): int =
  while true:
    stdout.write "    Choose cell: "
    stdout.flushFile()
    try:
      let sq = stdin.readLine().toLowerAscii
      if sq == "q":
        quit "Quitting.", QuitSuccess
      if sq.len != 2 or sq[0] notin 'a'..'j' or sq[1] notin '0'..'9':
        continue
      result = int((ord(sq[0]) - ord('a')) * 10 + ord(sq[1]) - ord('0'))
      if not result.atCorner: break
    except EOFError:
      echo()
      quit "Encountered end of file. Quitting.", QuitFailure
  echo()


proc play(game: var Game) =
  var score, guesses = 0
  var num = '0'

  block outer:
    while true:
      block inner:
        game.drawGrid(score, guesses)
        let ix = game.nextCell()
        if not ix.inMiddle and game.b[ix] != ' ':     # already processed.
          continue
        var incr, def: int
        if ix.atTop:
          (incr, def) = (10, 1)
        elif ix.atBottom:
          (incr, def) = (-10, 1)
        elif ix.atLeft:
          (incr, def) = (1, 10)
        elif ix.atRight:
          (incr, def) = (-1, 10)
        else:
          if game.b[ix] != 'G':
            game.b[ix] = 'G'
            inc guesses
            if guesses == 4: break outer
          else:
            game.b[ix] = ' '
            dec guesses
          continue

        var first = true
        var x = ix + incr
        while x.inMiddle:

          if game.h[x] == 'T':
            # Hit.
            game.b[ix] = 'H'
            inc score
            first = false
            break inner

          if first and (x + def).inMiddle and game.h[x + def] == 'T' or
                       (x - def).inMiddle and game.h[x - def] == 'T':
            # Reflection.
            game.b[ix] = 'R'
            inc score
            first = false
            break inner

          first = false
          var y = x + incr - def
          if y.inMiddle and game.h[y] == 'T':
            # Deflection.
            (incr, def) = if incr in [-1, 1]: (10, 1) else: (1, 10)

          y = x + incr + def
          if y.inMiddle and game.h[y] == 'T':
            # Deflection or double deflection.
            (incr, def) = if incr in [-1, 1]: (-10, 1) else: (-1, 10)

          inc x, incr

        num = if num != '9': succ(num) else: 'a'
        if game.b[ix] == ' ': inc score
        game.b[ix] = num
        if x.inRange:
          if ix == x:
            game.b[ix] = 'R'
          else:
            if game.b[x] == ' ': inc score
            game.b[x] = num

  game.drawGrid(score, guesses)
  game.finalScore(score, guesses)


proc main() =

  randomize()
  while true:
    var game = initGame()
    echo """
    === BLACK BOX ===

      H    Hit (scores 1)
      R    Reflection (scores 1)
      1-9, Detour (scores 2)
      a-c  Detour for 10-12 (scores 2)
      G    Guess (maximum 4)
      Y    Correct guess
      N    Incorrect guess (scores 5)
      A    Unguessed atom

      Cells are numbered a0 to j9.
      Corner cells do nothing.
      Use edge cells to fire beam.
      Use middle cells to add/delete a guess.
      Game ends automatically after 4 guesses.
      Enter q to abort game at any time.
    """

    game.play()

    while true:
      stdout.write "    Play again (y/n): "
      stdout.flushFile()
      case stdin.readLine().toLowerAscii()
      of "n": return
      of "y": break

main()
Output:

Using same input as in Wren entry with WikiGame = true:

    === BLACK BOX ===

      H    Hit (scores 1)
      R    Reflection (scores 1)
      1-9, Detour (scores 2)
      a-c  Detour for 10-12 (scores 2)
      G    Guess (maximum 4)
      Y    Correct guess
      N    Incorrect guess (scores 5)
      A    Unguessed atom

      Cells are numbered a0 to j9.
      Corner cells do nothing.
      Use edge cells to fire beam.
      Use middle cells to add/delete a guess.
      Game ends automatically after 4 guesses.
      Enter q to abort game at any time.
    
      0   1   2   3   4   5   6   7   8   9

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║   ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 0	Guesses = 0	 Status = In play

    Choose cell: b0

      0   1   2   3   4   5   6   7   8   9

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║   ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 2	Guesses = 0	 Status = In play

    Choose cell: c0

      0   1   2   3   4   5   6   7   8   9

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 4	Guesses = 0	 Status = In play

    Choose cell: d7

      0   1   2   3   4   5   6   7   8   9

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║   ║   ║   ║ G ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 4	Guesses = 1	 Status = In play

    Choose cell: d4

      0   1   2   3   4   5   6   7   8   9

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║ G ║   ║   ║ G ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 4	Guesses = 2	 Status = In play

    Choose cell: e3

      0   1   2   3   4   5   6   7   8   9

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║ G ║   ║   ║ G ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║ G ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 4	Guesses = 3	 Status = In play

    Choose cell: h2

      0   1   2   3   4   5   6   7   8   9

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║ G ║   ║   ║ G ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║ G ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║ G ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 4	Guesses = 4	 Status = Game over!

      0   1   2   3   4   5   6   7   8   9

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║ A ║   ║ N ║   ║   ║ Y ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║ N ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║ A ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║ N ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║ A ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 19	Guesses = 4	 Status = Game over!

    Play again (y/n): n

Phix

Library: Phix/pGUI

A configurable GUI version of the Black Box game, with a Knuth solver/helper.

-- demo\rosetta\Black_Box.exw
constant title = "Black Box",
help_text = """
Discover the location of objects/atoms using the fewest probes/rays.

See distributed version for much longer help text and other comments.
"""
integer size,           -- eg 8
        s1, s2,         -- size+1|2
        count,          -- eg 4
        mask            -- eg #0b100000 (first such >size^2-count+1)
                        -- Note that new_game() contains limiting code.

sequence gameboard, -- actual, count 1's and size*size-count 0's.
         eboard,    -- one of "", as being enumerated through
         results,   -- results of rays/probes, {x,y,c,x,y} format
         guessxy,   -- locations (each element is {x,y})
         guessclr,  -- colours of "" (CD_BLUE for a guess,
                    --                CD_GREEN for correct,
                    --                CD_RED for wrong,
                    --                CD_YELLOW/CYAN for hints.
         hidden,    -- "" as saved during setup
         possibles, -- up to 635,376 integer codes for 8*8 with 4 game,
                    -- each entry being possible for content of results, 
                    -- but never deliberately driven over 100,000.
         knowns,    -- these "are" atoms (but "maybe" if tried<maxtry)
         minmaxmove -- best move available, see minmaxcount

integer possible,   -- # of possibles checked to be plausible(), ie
                    -- [posssible+1..$] are all subject to imminent
                    -- deletion by the idle handler, if invalid.
        hinted,     -- # of probes analysed by hint_explore().
        minmaxcount -- best (so far)

atom tried, maxtry  -- # of enumerations attempted/theoretical max.
bool hints_used = false -- (affects the scoring)

function probe(integer x, y, sequence board, bool bSort=true)
--
-- returns {x,y,c,rx,ry} primarily for use in redraw_cb(), and 
--                       secondarily for use in plausible().
-- where c is: -1 for reflection, 0 for hit, and +1 otherwise.
-- Note that for the latter you need to allocate an actual colour
-- elsewhere (if this did that it would spanner plausible() etc),
-- and also note that -2 is now in use for the ray/probe hint.
-- Also x,y and rx,ry re-ordered lowest-first to avoid duplicates,
-- except for hint exploration, which passes a bSort of false.
--
    integer rx = x, ry = y, -- current/emerge point (ray)
            dx = 0, dy = 0, -- direction of travel
            moves = 0       -- debug aid

    if    x=0 then  dx = +1     -- left entry, moving right
    elsif y=0 then  dy = +1     -- top        "		   down
    elsif x=s1 then dx = -1     -- right      "		   left	
    elsif y=s1 then dy = -1     -- btm        "			 up
    else ?9/0 -- (sanity check)
    end if

    while true do

        integer nx = rx+dx,     -- next logical position
                ny = ry+dy,
                idx = (ny-1)*size+nx

        if nx=0 or nx=s1 or ny=0 or ny=s1 then
            if x=nx and y=ny then
                return {x,y,-1,0,0} -- Reflection
            elsif bSort then
                {{x,y},{nx,ny}} = sort({{x,y},{nx,ny}})
            end if
            return {x,y,1,nx,ny}    -- Emerges here
        elsif idx<=0 then
            ?9/0                    -- (sanity check)
        elsif board[idx] then
            return {x,y,0,0,0}      -- Hit
        --
        -- aside: rather than check diagonally, nx/ny are
        --      simply discarded when a deflection occurs,
        --      and we actually check things laterally.
        --
        elsif dx=0 then
            -- up/down movement, check sides
            if nx>1 and board[idx-1] then
                if nx<size and board[idx+1] then
                    dy = -dy            -- 180
                else
                    {dx,dy} = {1,0}     -- right
                    -- (yep, both up & down deflected
                    --  right by an atom on the left)
                end if
            elsif nx<size and board[idx+1] then
                {dx,dy} = {-1,0}        -- left
                --  (ditto left by one on the right)
            else
                {rx,ry} = {nx,ny}
            end if
        elsif dy=0 then
            -- left/right movement, check above/below
            if ny>1 and board[idx-size] then
                if ny<size and board[idx+size] then
                    dx = -dx            -- 180
                else
                    {dx,dy} = {0,1}     -- down
                    -- (yep, left & right are both
                    --  deflected down by one above)
                end if
            elsif ny<size and board[idx+size] then
                {dx,dy} = {0,-1}        -- up
                -- (ditto up by one below)
            else
                {rx,ry} = {nx,ny}
            end if
        else
            ?9/0 -- (sanity check, dx,dy=={0,0}!?)
        end if
        if rx=0 or rx=s1 or ry=0 or ry=s1 then
            {dx,dy} = {0,0} -- (outer swivel===reflection)
        end if
        -- guard against infinite loops, why not.
        -- *2 because swivel/move counted separately.
        moves += 1
        if moves>2*size*size then ?9/0 end if
    end while       
end function

function plausible(sequence board)
    for i=1 to length(results) do
        sequence ri = results[i]
        integer {x,y} = ri
        if probe(x,y,board)!=ri then return false end if
    end for
    return true
end function

--
-- For the smaller games we could use almost any storage method, but to facilitate larger 
-- boards with more atoms we should be as stingy with memory as possible. To that end an
-- enumeration is stored as a compact set of offsets to the next piece. For instance the
-- board {0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,....1} is stored as offsets {4,6,8,64-18}
-- further using an appropriate mask to give (((((46*#40)+8)*#40)+6)*#40)+4 which can be
-- stored as a single integer/atom, yet unpacked quite easily, see next. Note there is
-- code in new_game(), for valuechanged_cb(), that ensures we can store count*bits, and
-- more by luck than judgement that (partly) helps avoid configurations that would take
-- far longer than the universe has existed to enumerate and scan even just the once.
--
function unpack(atom code)
    sequence board = repeat(0,size*size)
    integer offset = 0, r, check = 0
    while code do
        r = remainder(code,mask)
        if r<=0 then ?9/0 end if        -- sanity check
        offset += r
        board[offset] = 1
        code = floor(code/mask)
        check += 1
    end while
    if check!=count then ?9/0 end if    -- sanity check
    return board
end function

function pack(sequence board)
    atom code = 0, pmask = 1
    integer idx = 0, check = 0
    while true do
        integer prev = idx
        idx = find(1,board,idx+1)
        if idx=0 then exit end if
        code = code + (idx-prev)*pmask
        check += 1
        pmask *= mask
    end while
    if check!=count then ?9/0 end if    -- sanity check
--  if unpack(code)!=board then ?9/0 end if -- ""
    return code
end function

procedure trim_possibles()
--
-- Re-process the possibles table as follows:
--  111...222322323...$
-- where 111... is possibly empty ok [1..possible],
-- and 222322323 is some chunk [possible+1..limit],
-- with 2s for oks and 3s for now-failing entries,
-- which gets processed in a right-to-left order,
-- such that fails(3) get replaced from the (1)s,
-- being careful to quit early on any overlap, and
-- /or re-test same slot if the 111... exhausted.
-- Finally, trim off the dead head of possibles[].
-- The result is quite scrambled, but care we not.
--
    integer limit = min(possible+100_000,length(possibles)),
            limit0 = limit,
            kill = 1 -- (actually 1 over)
    while limit>max(possible,kill-1) do
        if not plausible(unpack(possibles[limit])) then
            possibles[limit] = possibles[kill]
            if kill<=possible then
                limit -= 1
            end if
            kill += 1
        else
            limit -= 1
        end if
    end while
    possibles = possibles[kill..$]
    possible = limit0-kill+1
end procedure

procedure enumerate()
    atom limit = min(tried+100_000,maxtry)
    while tried<limit and length(possibles)<100_000 do
        tried += 1
        if plausible(eboard) then
            possibles &= pack(eboard)
            possible += 1
        end if
        --
        -- think abacus: find the first bead you can shift left,
        --                and slam the rest of them hard right.
        -- similar to binary counting, but you must always have
        --                exactly 'count' beads (ie 1's), eg
        -- choose(2*2,2) is 6:
        --       0b0011  0b0101  0b0110  0b1001  0b1010  0b1100
        --
        -- However, because we are scanning from top left down
        -- to bottom right, it turned out better to do them in
        -- reverse order, hence shift right and slam left (not
        -- quite an exact mirror, but close enough).
        --
        integer idx = find(1,eboard), last = 1
        while true do
            eboard[idx] = 0
            idx += 1
            if idx>size*size then exit end if
            if eboard[idx]=0 then 
                eboard[idx] = 1
                exit
            end if
            eboard[last] = 1
            last += 1
        end while
        if idx=0 then exit end if
    end while
end procedure

function idx_from_edge(integer x,y)
-- convert {x,y}, where one but not both of x,y are either 0 
-- or s1, and the other is strictly 1..size, into 1..4*size.
--  if x=0 then x = 0  -- (logically, but obvs. pointless)
    if x=s1 then x = size
    elsif y=0 then y = size*2
    elsif y=s1 then y = size*3
    elsif x!=0 then ?9/0 end if -- not an edge?!
    return x+y
end function

function edge_from_idx(integer xy)
-- convert 1..4*size into {0,1..size}/{s1,1..size}/{1..size,0}/{1..size,s1}
    sequence res
    integer c = floor((xy-1)/size)
    switch c do
        case 0: res = {0,xy}
        case 1: res = {s1,xy-size}
        case 2: res = {xy-size*2,0}
        case 3: res = {xy-size*3,s1}
        default: ?9/0
    end switch
    return res
end function

-- this is currently inlined, in case you were looking for it:
--procedure idx_from_x_y(integer x, y)
-- convert {1,1}..{size,size} to 1..size*size, for flat indexing
--  return (y-1)*size+x
--end function

function x_y_from_idx(integer idx)
-- convert 1..size*size to {1,1}..{size,size}
-- (absence of floor() on /size is a deliberate sanity check)
    integer x = remainder(idx-1,size)+1,
            y = (idx-x)/size + 1
    return {x,y}
end function

function next_hint()
    sequence edges = repeat(0,size*4)
    integer x,y,r
    for i=1 to length(results) do
        {x,y,r} = results[i]
        for j=1 to 1+(r==1) do
            integer idx = idx_from_edge(x,y)
            if edges[idx] then ?9/0 end if
            edges[idx] = 1
            {?,?,?,x,y} = results[i]
        end for
    end for
    integer new_hinted = find(0,edges,hinted+1)
    return new_hinted
end function

procedure explore_hints(integer new_hinted)
    if new_hinted then
        -- originally, it proved better to scan these backwards...
        -- it now breaks (wrong tiles, I guess) if not flipped...
        new_hinted = size*4+1-new_hinted
        integer {x,y} = edge_from_idx(new_hinted), k
        sequence rxy = {}, counts = {}
        for i=1 to possible do
            sequence p = probe(x,y,unpack(possibles[i]),false)
            k = find(p,rxy)
            if k=0 then
                rxy = append(rxy,p)
                counts = append(counts,1)
            else
                counts[k] += 1
            end if
        end for
        k = max(counts)
        if hinted=0
        or minmaxcount=0
        or k<minmaxcount then
            minmaxcount = k
            k = maxsq(counts,true)
            minmaxmove = rxy[k]
            minmaxmove[3] = -2
        end if
        new_hinted = size*4+1-new_hinted  -- unflip
        hinted = new_hinted
    else
        hinted = size*4
    end if
end procedure

procedure find_common()
    sequence all = repeat(1,size*size),
             none = repeat(0,size*size)
    for i=1 to possible do
        all = sq_and(all,unpack(possibles[i]))
        if all==none then exit end if
    end for
    knowns = {}
    for i=1 to length(all) do
        if all[i] then
            knowns = append(knowns,x_y_from_idx(i))
        end if
    end for
end procedure

include pGUI.e
Ihandle dlg, game_canvas, gridsize, atoms, score, hints, debug, 
        progress, declare

constant colour_table = {CD_RED,
                         CD_LIGHT_GREEN,
                         CD_YELLOW,
                         CD_BLUE,
                         CD_ORANGE,
                         CD_PURPLE,
                         CD_CYAN,
                         CD_MAGENTA,
                         CD_GREEN,
                         CD_DARK_GREEN,
                         #bfef45,   -- Lime
                         #fabebe,   -- Pink
                         #469990,   -- Teal
                         #e6beff,   -- Lavender
                         #9A6324,   -- Brown
                         #fffac8,   -- Beige
                         #800000,   -- Maroon
                         #aaffc3,   -- Mint
                         #808000,   -- Olive
                         #ffd8b1,   -- Apricot
                         #000075}   -- Navy

function colour(integer c)
    c = mod(c-1,length(colour_table))+1
    return colour_table[c]
end function

constant CD_HINTS = CD_DARK_GREY,   -- (where to fire probe)
         CD_MAYBE = CD_YELLOW,      -- (probably an atom [scan not yet finished])
         CD_KNOWN = CD_CYAN         -- (known atoms [scan finished])

procedure redraw()
    IupUpdate(game_canvas)
end procedure

function idle_action()
    integer new_hinted = 0
    if possible<length(possibles) then  
        trim_possibles()
        hinted = 0
    elsif tried<maxtry and length(possibles)<100_000 then
        enumerate()
        hinted = 0
    elsif IupGetInt(hints,"VALUE")
      and hinted<size*4 then
        if possible>1 
        and hinted<size*4 then
            new_hinted = next_hint()
            explore_hints(new_hinted)
            redraw()
        end if
        if possible=1
        or hinted=size*4 then
            hinted = size*4
            find_common()
            redraw()
        end if
    else
        return IUP_IGNORE -- (disables idle)
    end if
    string title = sprintf("%,d / %,d (%d%%)",{possible,tried,100*(tried/maxtry)})
    if new_hinted then
        title &= sprintf(", move %d/%d",{new_hinted,size*4})
    end if
    IupSetStrAttribute(progress,"TITLE",title)
    return IUP_DEFAULT
end function
constant idle_action_cb = Icallback("idle_action")

procedure start_idle()
    IupSetAttribute(progress,"TITLE","-")
    IupSetGlobalFunction("IDLE_ACTION",idle_action_cb)
end procedure

procedure new_game()
    size = IupGetInt(gridsize,"VALUE")
    s1 = size+1
    s2 = size+2
    count = IupGetInt(atoms,"VALUE")
    while true do -- in case count too big
        mask = #02
        integer bits = 1
        while mask<=size*size-count+1 do mask*=2 bits+=1 end while
        --
        -- Prevent overflow: must be able to store count*bits in a Phix atom.
        -- count limits are therefore 13 on 5x5, 7 on 10x10, and 5 on 20x20,
        -- on 32-bit, but 64-bit does 16 on 5x5, 9 on 10x10, and 7 on 20x20.
        -- Many if not all of the silly-sized games this prohibits could not 
        -- possibly be fully analysed within a typical human lifespan anyway.
        -- Besides just 5 atoms allows ambiguous/therefore unplayable games.
        -- See also the comments before unpack() above. Trying to store too
        -- many bits would trigger the sanity checks in pack()/unpack().
        --
        integer mb = iff(machine_bits()=32?53:64),
                maxcount = min(floor(mb/bits),size*size)
        if count<=maxcount then exit end if
        count = maxcount
        IupSetInt(atoms,"VALUE",count)
    end while

    eboard = repeat(0,size*size)
    eboard[1..count] = 1
    tried = 0
    maxtry = choose(size*size,count)
    possibles = {}
    possible = 0
    results = {}
    guessxy = {}
    guessclr = {}
    hidden = {}
    knowns = {}
    minmaxcount = 0
    gameboard = repeat(0,size*size)
    bool active = IupGetInt(debug,"VALUE")
    integer done = 0, x, y, xy
    while done<count do
        x = rand(size)
        y = rand(size)
        xy = (y-1)*size+x
        if gameboard[xy]=0 then
            gameboard[xy] = 1
            hidden = append(hidden,{x,y})
            done += 1
        elsif not find(0,gameboard) then
            ?9/0 -- let's not loop forever!
                 -- (should now be prevented by maxcount above)
        end if
    end while
    IupSetInt(declare, "ACTIVE", active)
    if active then
        guessxy = hidden
        guessclr = repeat(CD_BLUE,length(guessxy))
    end if
    hints_used = (IupGetInt(hints,"VALUE") and not active)
    start_idle()
end procedure

-- saved in redraw_cb(), for click testing in button_cb():
integer wh, -- width and height
        mx, my  -- margins

-- saved in declare_cb(), for adding to the score (10 each)
integer wrong = 0

function redraw_cb(Ihandle ih, integer /*posx*/, integer /*posy*/)
    integer {w,h} = IupGetIntInt(ih, "DRAWSIZE")
    -- calc width/height and margins (saved for button_cb):
    wh = min(floor((w-10)/s2),floor((h-10)/s2))
    mx = floor((w-wh*(s2))/2)
    my = floor((h-wh*(s2))/2)
    
    cdCanvas cddbuffer = IupGetAttributePtr(ih,"DBUFFER")
    IupGLMakeCurrent(ih)
    cdCanvasActivate(cddbuffer)
    cdCanvasClear(cddbuffer)

    -- outer edges (using one huge '+' shape)   
    cdCanvasSetForeground(cddbuffer,CD_GREY)
    cdCanvasBox(cddbuffer,mx+wh,mx+wh*s1,my,my+wh*s2)
    cdCanvasBox(cddbuffer,mx,mx+wh*s2,my+wh,my+wh*s1)
    -- the inner size*size board (square)
    cdCanvasSetForeground(cddbuffer,CD_LIGHT_GREY)
    cdCanvasBox(cddbuffer,mx+wh,mx+wh*s1,my+wh,my+wh*s1)
    -- draw the grid lines
    cdCanvasSetForeground(cddbuffer,CD_WHITE)
    integer {lx,ly} = {mx,my}
    for i=1 to size+1 do
        lx += wh
        ly += wh
        cdCanvasLine(cddbuffer,lx,my,lx,my+wh*s2)
        cdCanvasLine(cddbuffer,mx,ly,mx+wh*s2,ly)
    end for

    sequence edges = repeat(0,size*4)
    integer x,y,c = 1, h2 = floor(wh/2), r,
            rfrom = (minmaxcount==0 or IupGetInt(hints,"VALUE")=0)
    for i=rfrom to length(results) do
        {x,y,r} = iff(i=0?minmaxmove:results[i])
        integer cb, ct
        string txt
        {txt,cb,ct} = iff(r=-2?{"+",CD_HINTS,CD_BLACK}:
                      iff(r=-1?{"R",CD_WHITE,CD_BLACK}:
                      iff(r==0?{"H",CD_BLACK,CD_WHITE}:
                   {sprintf("%d",c),CD_GREY,colour(c)})))
        for j=1 to 1+(r==1) do
            cdCanvasSetForeground(cddbuffer,cb)
            integer cx = mx+wh*x,
                    cy = my+wh*(s1-y)
            cdCanvasBox(cddbuffer,cx+1,cx+wh,cy+1,cy+wh)
            cdCanvasSetForeground(cddbuffer,ct)
            cdCanvasFont(cddbuffer, "Helvetica", CD_BOLD, h2)
            cdCanvasText(cddbuffer, cx+h2, cy+h2, txt)
            if i!=0 then
                integer idx = idx_from_edge(x,y)
                if edges[idx] then ?9/0 end if
                edges[idx] = 1
                if r=1 then
                    {?,?,?,x,y} = results[i]
                    c += (j=2)
                end if
            end if
        end for
    end for
    sequence gxy = guessxy,
             gclr = guessclr
    if IupGetInt(hints,"VALUE") then
        for i=1 to length(knowns) do
            sequence ki = knowns[i]
            if not find(ki,gxy) then
                gxy = append(gxy,ki)
                gclr = append(gclr,iff(tried<maxtry?CD_MAYBE:CD_KNOWN))
            end if
        end for
    end if
    for i=1 to length(gxy) do
        {x,y} = gxy[i]
        atom cx = mx+(x+0.5)*wh,
             cy = my+(s1-y+0.5)*wh
        r = floor(wh*4/5)
        cdCanvasSetForeground(cddbuffer,gclr[i])
        cdCanvasCircle(cddbuffer, cx, cy, r)
    end for
    cdCanvasFlush(cddbuffer)
--  IupSetStrAttribute(score,"TITLE","%d",{iff(hints_used?9999:sum(edges)+wrong*10)})
    IupSetStrAttribute(score,"TITLE","%d",{sum(edges)+wrong*10})
    return IUP_DEFAULT
end function

function map_cb(Ihandle ih)
    IupGLMakeCurrent(ih)
    atom res = IupGetDouble(NULL, "SCREENDPI")/25.4
    cdCanvas cddbuffer = cdCreateCanvas(CD_GL, "10x10 %g", {res})
    IupSetAttributePtr(ih,"DBUFFER",cddbuffer)
    cdCanvasSetBackground(cddbuffer, CD_PARCHMENT)
    {} = cdCanvasTextAlignment(cddbuffer, CD_CENTER)
    return IUP_DEFAULT
end function

function canvas_resize_cb(Ihandle canvas)
    cdCanvas cddbuffer = IupGetAttributePtr(canvas,"DBUFFER")
    integer {canvas_width, canvas_height} = IupGetIntInt(canvas, "DRAWSIZE")
    atom res = IupGetDouble(NULL, "SCREENDPI")/25.4
    cdCanvasSetAttribute(cddbuffer, "SIZE", "%dx%d %g", {canvas_width, canvas_height, res})
    return IUP_DEFAULT
end function

function declare_cb(Ihandle /*declare*/)
    sequence add_h = repeat(true,length(hidden))
    wrong = max(0,count-length(guessxy))
    for i=1 to length(guessxy) do
        integer k = find(guessxy[i],hidden)
        if k then
            guessclr[i] = CD_GREEN
            add_h[k] = false
        else
            guessclr[i] = CD_RED
            wrong += 1
        end if
    end for
    for i=1 to length(add_h) do
        if add_h[i] then
            guessxy = append(guessxy,hidden[i])
            guessclr = append(guessclr,CD_BLUE)
        end if
    end for
    IupSetAttribute(declare, "ACTIVE", "NO")
    redraw()
    return IUP_DEFAULT
end function

function button_cb(Ihandle canvas, integer button, pressed, x, y, atom /*pStatus*/)
    Ihandle frame = IupGetParent(canvas)
    string title = IupGetAttribute(frame,"TITLE")
    if button=IUP_BUTTON1 and not pressed then      -- (left button released)
        x = floor((x-mx)/wh)
        y = floor((y-my)/wh)
        -- obviously, an x/y of 0 means left/top,
        --            whereas s1 means right/btm,
        --            and 1..size(both) is inner.
        bool outerx = (x>=0 and x<=s1),
             outery = (y>=0 and y<=s1),
             innerx = (x>=1 and x<=size),
             innery = (y>=1 and y<=size)
        if innerx and innery then
            sequence guess = {x,y}
            integer k = find(guess,guessxy)
            if k then
                guessxy[k..k] = {}
                guessclr[k..k] = {}
            else
                guessxy = append(guessxy,guess)
                guessclr = append(guessclr,CD_BLUE)
            end if
            bool bActive = (length(guessxy)==count)
            IupSetInt(declare, "ACTIVE", bActive)
            if IupGetInt(debug,"VALUE")
            and length(guessxy)=count then
                hidden = guessxy
                gameboard = repeat(0,size*size)
                for i=1 to count do
                    {x,y} = hidden[i]
                    integer xy = (y-1)*size+x
                    gameboard[xy] = 1
                end for
                results = {}
            end if
            redraw()
        elsif (outerx and innery)
           or (outery and innerx) then
            sequence r = probe(x,y,gameboard)
            if not find(r,results) then
                results = append(results,r)
                possible = 0
                start_idle()
            end if
            redraw()
        end if
    end if
    return IUP_CONTINUE
end function

function new_game_cb(Ihandle /*ih*/)
    new_game()
    redraw()
    return IUP_DEFAULT
end function

function exit_cb(Ihandle /*ih*/)
    return IUP_CLOSE
end function

function help_cb(Ihandln /*ih*/)
    IupMessage(title,help_text)
    return IUP_DEFAULT
end function

function key_cb(Ihandle /*dlg*/, atom c)
    if c=K_ESC then return IUP_CLOSE end if
    if c=K_F1 then return help_cb(NULL) end if
    if c='?' then
        -- an old diagnostic that I kept in...
        for i=1 to min(5,length(possibles)) do
            sequence s = unpack(possibles[i])
            for j=1 to size do
                ?s[1..size]
                s = s[size+1..$]
            end for
            puts(1,"\n")
        end for
        possible = 0
        start_idle()
    end if
    return IUP_CONTINUE
end function

function valuechanged_cb(Ihandle ih)
    if ih=hints then
        hints_used = true
        start_idle()
    else
        new_game()
    end if
    redraw()
    return IUP_DEFAULT
end function
constant cb_valuechanged = Icallback("valuechanged_cb")

procedure main()
    IupOpen()
 
    gridsize = IupText("SPIN=Yes, SPINMIN=1, SPINMAX=20, VALUE=8, RASTERSIZE=34x")
    atoms = IupText("SPIN=Yes, SPINMIN=1, SPINMAX=16, VALUE=4, RASTERSIZE=34x")
    score = IupLabel("","EXPAND=HORIZONTAL, PADDING=5x4")
    hints = IupToggle("  Show Hints?","VALUE=YES, RIGHTBUTTON=YES, PADDING=5x4")
    debug = IupToggle("Debug Mode?","VALUE=NO, RIGHTBUTTON=YES, PADDING=5x4")
    progress = IupLabel("-","EXPAND=HORIZONTAL, PADDING=5x4")
    declare = IupButton("Declare",Icallback("declare_cb"),"PADDING=5x4, ACTIVE=NO")
    game_canvas = IupGLCanvas("RASTERSIZE=400x400")
    Ihandle newgame = IupButton("New Game",Icallback("new_game_cb"),"PADDING=5x4"),
            help = IupButton("Help (F1)",Icallback("help_cb"),"PADDING=5x4"),
            quit = IupButton("E&xit",Icallback("exit_cb"),"PADDING=5x4"),
            vbox = IupVbox({IupHbox({IupLabel("Size","PADDING=5x4"),gridsize,
                                     IupFill(),
                                     IupLabel("Atoms","PADDING=5x4"),atoms}),
                            IupHbox({hints,IupFill(),debug}),
                            IupHbox({progress}),
                            IupHbox({IupLabel("Score","PADDING=5x4"),score}),
                            IupHbox({declare,newgame,help,quit})},"MARGIN=5x5"),
            game_frame = IupFrame(IupHbox({game_canvas},"MARGIN=3x3"),"TITLE=Game"),
            option_frame = IupFrame(vbox,"TITLE=Options"),
            full = IupHbox({game_frame,option_frame})
    IupSetCallbacks({gridsize,atoms,hints,debug}, {"VALUECHANGED_CB", cb_valuechanged})
    IupSetCallbacks(game_canvas, {"ACTION", Icallback("redraw_cb"),
                                  "MAP_CB", Icallback("map_cb"),
                                  "RESIZE_CB", Icallback("canvas_resize_cb"),
                                  "BUTTON_CB", Icallback("button_cb")})
    dlg = IupDialog(IupHbox({full},"MARGIN=3x3"))
    IupSetAttribute(dlg, "TITLE", title)
    IupSetCallback(dlg, "K_ANY", Icallback("key_cb"))
    IupSetAttributeHandle(dlg,"DEFAULTENTER", declare)  --erm...??

    new_game()

    IupShowXY(dlg,IUP_CENTER,IUP_CENTER)
    IupSetAttribute(dlg, "RASTERSIZE", NULL)
    IupSetStrAttribute(dlg, "MINSIZE", IupGetAttribute(dlg,"RASTERSIZE"))
    sequence fixsize = {score,progress}
    for i=1 to length(fixsize) do
        Ihandle fi = fixsize[i]
        IupSetAttributes(fi, "RASTERSIZE=%s, EXPAND=NO", {IupGetAttribute(fi,"RASTERSIZE")})
    end for
    IupMainLoop()
    IupClose()
end procedure
 
main()

Wren

Translation of: Go
Library: Wren-fmt
Library: Wren-ioutil
Library: Wren-str
import "random" for Random
import "./fmt" for Fmt
import "./ioutil" for Input
import "./str" for Str 

var b = List.filled(100, null)  // displayed board
var h = List.filled(100, null)  // hidden atoms
var wikiGame = true             // set to false for a 'random' game
var rand = Random.new()

var hideAtoms = Fn.new {
    var placed = 0
    while (placed < 4) {
        var a = rand.int(11, 89) // 11 to 88 inclusive
        var m = a % 10
        if (m == 0 || m == 9 || h[a] == "T") continue
        h[a] = "T"
        placed = placed + 1
    }
}

var initialize = Fn.new {
    for (i in 0..99) {
        b[i] = " "
        h[i] = "F"
    }
    if (!wikiGame) {
        hideAtoms.call()
    } else {
        h[32] = "T"
        h[37] = "T"
        h[64] = "T"
        h[87] = "T"
    }
    System.print("""
    === BLACK BOX ===
 
    H    Hit (scores 1)
    R    Reflection (scores 1)
    1-9, Detour (scores 2)
    a-c  Detour for 10-12 (scores 2)
    G    Guess (maximum 4)
    Y    Correct guess
    N    Incorrect guess (scores 5)
    A    Unguessed atom
 
    Cells are numbered a0 to j9.
    Corner cells do nothing.
    Use edge cells to fire beam.
    Use middle cells to add/delete a guess.
    Game ends automatically after 4 guesses.
    Enter q to abort game at any time.
    """)
}

var drawGrid = Fn.new { |score, guesses|
    System.print("      0   1   2   3   4   5   6   7   8   9 ")
    System.print()
    System.print("        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗")
    Fmt.lprint("a     $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s",
        [b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9]])
    System.print("    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗")
    Fmt.lprint  ("b   ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║",
        [b[10], b[11], b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19]])
    System.print("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣")
    Fmt.lprint  ("c   ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║",
        [b[20], b[21], b[22], b[23], b[24], b[25], b[26], b[27], b[28], b[29]])
    System.print("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣")
    Fmt.lprint  ("d   ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║",
        [b[30], b[31], b[32], b[33], b[34], b[35], b[36], b[37], b[38], b[39]])
    System.print("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣")
    Fmt.lprint  ("e   ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║",
        [b[40], b[41], b[42], b[43], b[44], b[45], b[46], b[47], b[48], b[49]])
    System.print("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣")
    Fmt.lprint  ("f   ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║",
        [b[50], b[51], b[52], b[53], b[54], b[55], b[56], b[57], b[58], b[59]])
    System.print("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣")
    Fmt.lprint  ("g   ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║",
        [b[60], b[61], b[62], b[63], b[64], b[65], b[66], b[67], b[68], b[69]])
    System.print("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣")
    Fmt.lprint  ("h   ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║",
        [b[70], b[71], b[72], b[73], b[74], b[75], b[76], b[77], b[78], b[79]])
    System.print("    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣")
    Fmt.lprint  ("i   ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║",
        [b[80], b[81], b[82], b[83], b[84], b[85], b[86], b[87], b[88], b[89]])
    System.print("    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝")
    Fmt.lprint  ("j     $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s ║ $s",
        [b[90], b[91], b[92], b[93], b[94], b[95], b[96], b[97], b[98], b[99]])
    System.print("        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝")
    var status = (guesses != 4) ? "In play" : "Game over!"
    System.print("\n        Score = %(score)\tGuesses = %(guesses)\t Status = %(status)\n")
}

var atCorner = Fn.new { |ix| ix == 0 || ix == 9 || ix == 90 || ix == 99 }

var inRange  = Fn.new { |ix| ix >= 1 && ix <= 98 && ix != 9 && ix != 90 }

var atTop    = Fn.new { |ix| ix >= 1 && ix <= 8 }

var atBottom = Fn.new { |ix| ix >= 91 && ix <= 98 }

var atLeft   = Fn.new { |ix| inRange.call(ix) && ix%10 == 0 }

var atRight  = Fn.new { |ix| inRange.call(ix) && ix%10 == 9 }

var inMiddle = Fn.new { |ix|
    return inRange.call(ix) && !atTop.call(ix) && !atBottom.call(ix) &&
           !atLeft.call(ix) && !atRight.call(ix)
}

var nextCell = Fn.new {
    var ix
    while (true) {
        var sq = Str.lower(Input.text("    Choose cell : ", 1))
        if (sq.count == 1 && sq[0] == "q") {
            Fiber.abort("program aborted")
        }
        if (sq.count != 2 || !"abcdefghij".contains(sq[0]) || !"0123456789".contains(sq[1])) {
            continue
        }
        ix = (sq[0].bytes[0] - 97) * 10 + sq[1].bytes[0] - 48
        if (atCorner.call(ix)) continue
        break
    }
    System.print()
    return ix
}

var finalScore = Fn.new { |score, guesses|
    for (i in 11..88) {
        var m = i % 10
        if (m == 0 || m == 9) continue
        if (b[i] == "G" && h[i] == "T") {
            b[i] = "Y"
        } else if (b[i] == "G" && h[i] == "F") {
            b[i] = "N"
            score = score + 5
        } else if (b[i] == " " && h[i] == "T") {
            b[i] = "A"
        }
    }
    drawGrid.call(score, guesses)
}

var play = Fn.new {
    var score = 0
    var guesses = 0
    var num = "0"
    while (true) {
        var outer = false
        drawGrid.call(score, guesses)
        var ix = nextCell.call()
        if (!inMiddle.call(ix) && b[ix] != " ") continue  // already processed
        var inc
        var def
        if (atTop.call(ix)) {
            inc = 10
            def = 1
        } else if (atBottom.call(ix)) {
            inc = -10
            def = 1
        } else if (atLeft.call(ix)) {
            inc = 1
            def = 10
        } else if (atRight.call(ix)) {
            inc = -1
            def = 10
        } else {
            if (b[ix] != "G") {
                b[ix] = "G"
                guesses = guesses + 1
                if (guesses == 4) break
            } else {
                b[ix] = " "
                guesses = guesses - 1
            }
            continue
        }
        var x = ix + inc
        var first = true
        while (inMiddle.call(x)) {
            if (h[x] == "T" ) {  // hit
                b[ix] = "H"
                score = score + 1
                first = false
                outer = true
                break
            }
            if (first && (inMiddle.call(x+def) && h[x+def] == "T") ||
                (inMiddle.call(x-def) && h[x-def] == "T")) {  // reflection
                b[ix] = "R"
                score = score + 1
                first = false
                outer = true
                break
            }
            first = false
            var y = x + inc - def
            if (inMiddle.call(y) && h[y] == "T") {  // deflection
                if (inc.abs == 1) {
                    inc = 10
                    def = 1
                } else if (inc.abs == 10) {
                    inc = 1
                    def = 10
                }
            }
            y = x + inc + def
            if (inMiddle.call(y) && h[y] == "T") {  // deflection or double deflection
                if (inc.abs == 1) {
                    inc = -10
                    def = 1
                } else if (inc.abs == 10) {
                    inc = -1
                    def = 10
                }
            }
            x = x + inc
        }
        if (outer) continue
        if (num != "9") {
            num = String.fromByte(num.bytes[0] + 1)
        } else {
            num = "a"
        }
        if (b[ix] == " ") score = score + 1
        b[ix] = num
        if (inRange.call(x)) {
            if (ix == x) {
                b[ix] = "R"
            } else {
                if (b[x] == " ") score = score + 1
                b[x] = num
            }
        }
    }
    drawGrid.call(score, guesses)
    finalScore.call(score, guesses)
}

while (true) {
    initialize.call()
    play.call()
    var yn = Str.lower(Input.option("    Play again y/n : ", "ynYN"))
    if (yn == "n") return
}
Output:

Sample game (wikiGame == true):

    === BLACK BOX ===
 
    H    Hit (scores 1)
    R    Reflection (scores 1)
    1-9, Detour (scores 2)
    a-c  Detour for 10-12 (scores 2)
    G    Guess (maximum 4)
    Y    Correct guess
    N    Incorrect guess (scores 5)
    A    Unguessed atom
 
    Cells are numbered a0 to j9.
    Corner cells do nothing.
    Use edge cells to fire beam.
    Use middle cells to add/delete a guess.
    Game ends automatically after 4 guesses.
    Enter q to abort game at any time.
      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║   ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 0	Guesses = 0	 Status = In play

    Choose cell : b0

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║   ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 2	Guesses = 0	 Status = In play

    Choose cell : c0

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 4	Guesses = 0	 Status = In play

    Choose cell : d7

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║   ║   ║   ║ G ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 4	Guesses = 1	 Status = In play

    Choose cell : d4

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║ G ║   ║   ║ G ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 4	Guesses = 2	 Status = In play

    Choose cell : e3

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║ G ║   ║   ║ G ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║ G ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 4	Guesses = 3	 Status = In play

    Choose cell : h2

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║   ║   ║ G ║   ║   ║ G ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║ G ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║ G ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 4	Guesses = 4	 Status = Game over!

      0   1   2   3   4   5   6   7   8   9 

        ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗
a       ║ 2 ║   ║   ║   ║   ║   ║   ║   ║  
    ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b   ║ 1 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
c   ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
d   ║   ║   ║ A ║   ║ N ║   ║   ║ Y ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
e   ║   ║   ║   ║ N ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
f   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
g   ║   ║   ║   ║   ║ A ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
h   ║   ║   ║ N ║   ║   ║   ║   ║   ║   ║   ║
    ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣
i   ║   ║   ║   ║   ║   ║   ║   ║ A ║   ║   ║
    ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j       ║   ║   ║   ║   ║   ║   ║   ║   ║  
        ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝

        Score = 19	Guesses = 4	 Status = Game over!

    Play again y/n : n

zkl

Translation of: Go
const ATM="A", F="F", HIT="H", G="G", GN="N", R="R", BLNK=" ", GY="Y";

var
   brd,hdn,	    // displayed board & hidden atoms
   wikiGame = True; // set to False for a 'random' game

fcn initialize{
   brd,hdn = List.createLong(100,BLNK), List.createLong(100,F);

   if(not wikiGame) hideAtoms();
   else hdn[32] = hdn[37] = hdn[64] = hdn[87] = ATM;
//   else hdn[64] = hdn[66] = ATM;	// Double deflection case

   println(
#<<<"
    === BLACK BOX ===
 
    H    Hit (scores 1)
    R    Reflection (scores 1)
    1-9, Detour (scores 2)
    a-c  Detour for 10-12 (scores 2)
    G    Guess (maximum 4)
    Y    Correct guess
    N    Incorrect guess (scores 5)
    A    Unguessed atom
 
    Cells are numbered a0 to j9.
    Corner cells do nothing.
    Use edge cells to fire beam.
    Use middle cells to add/delete a guess.
    Game ends automatically after 4 guesses.
    Enter q to abort game at any time.\n\n");
#<<<
}

fcn drawGrid(score, guesses){
   var [const] vbs="\u2550\u2550\u2550\u256c", bt=(vbs.del(-3,3)),
      be1=String("      %s",vbs*7,bt,"%s").fmt,
      b1=be1("\u2554","\u2557"), e1=be1("\u255a","\u255d"),

      be2=String("  %s", vbs*9, bt,"%s").fmt,
      b2=be2("\u2554", "\u2557"), b3=be2("\u256c", "\u256c"), 
      e2=be2("\u255a", "\u255d"),

      g1=String("%s%s    ","\u2551 %s "*9).fmt,		// a brd[0]=brd[90]=" "
      g2=String("%s ","\u2551 %s "*11).del(-3,3).fmt;	// b c d .. i

   println("    0   1   2   3   4   5   6   7   8   9 \n",b1);
   grid,sep,n := g1, b2, -10;
   foreach c in (["a".."i"]){
      println(grid(c,brd[n+=10,10].xplode()));
      println((c=="i") and e2 or sep);
      grid,sep = g2,b3;
   }
   println(g1("j",brd[90,10].xplode()), "\n", e1);

   status:=(guesses==4) and "Game over!" or "In play";
   println("\n        Score = ", score, "\tGuesses = ", guesses, "\t Status = ", status);
}

fcn hideAtoms{
   n:=4; do{
      a,m:=(11).random(89), a % 10; 	// 11 to 88 inclusive
      if(m==0 or m==9 or hdn[a]==ATM) continue;
      hdn[a]=ATM;
      n-=1;
   }while(n);
}

fcn nextCell{
   while(True){
      s,c,n,sz := ask("    Choose cell [a-j][0-9]: ").strip().toLower(), s[0,1], s[1,1], s.len();
      if(sz==1 and c=="q") System.exit();
      if(not (sz==2 and ("a"<=c<="j") and ("0"<=n<="9"))) continue;
      ix:=10*(c.toAsc() - 97) + n;	// row major, "a"-'a'
      if(not atCorner(ix)){ println(); return(ix); }
   }
}

fcn atCorner(ix){  ix==0 or ix==9 or ix==90 or ix==99 }
fcn inRange(ix) {  (1<=ix<=98) and ix!=9 and ix!=90 }
fcn atTop(ix)   {   1<=ix<= 8 }
fcn atBottom(ix){  91<=ix<=98 }	
fcn atLeft(ix)  {  inRange(ix) and ix%10 ==0 }
fcn atRight(ix) {  inRange(ix) and ix%10 ==9 }
fcn inMiddle(ix){
   inRange(ix) and not ( atTop(ix) or atBottom(ix) or atLeft(ix) or atRight(ix) )
}
fcn play{
   score,guesses,num := 0,0, 0x30;	// '0'
   while(True){
      drawGrid(score, guesses);
      ix:=nextCell();
      if(not inMiddle(ix) and brd[ix]!=BLNK) continue; // already processed
      inc,def := 0,0;
      if     (atTop(ix))    inc,def =  10,  1;
      else if(atBottom(ix)) inc,def = -10,  1;
      else if(atLeft(ix))   inc,def =   1, 10;
      else if(atRight(ix))  inc,def =  -1, 10;
      else{
	 if(brd[ix]!=G){
	    brd[ix]=G;
	    if( (guesses+=1) ==4) break(1);	// you done
	 }else{ brd[ix]=BLNK; guesses-=1; }
	 continue;
      }
      x,first := ix + inc, True;
      while(inMiddle(x)){ 
	 if(hdn[x]==ATM){					// hit
	    brd[ix]=HIT;
	    score+=1;
	    first=False;
	    continue(2);
	 }
	 if(first and (inMiddle(x + def) and hdn[x + def]==ATM) or
	    (inMiddle(x - def) and hdn[x - def]==ATM)){		// reflection
	    brd[ix]=R;
	    score+=1;
	    first=False;
	    continue(2);
	 }
	 first=False;
	 y:=x + inc - def;
	 if(inMiddle(y) and hdn[y]==ATM){			// deflection
	    switch(inc){
	       case( 1,  -1){  inc, def = 10, 1 }
	       case(10, -10){  inc, def =  1,10 }
	    }
	 }
	 y=x + inc + def;
	 if(inMiddle(y) and hdn[y]==ATM){    // deflection or double deflection
	    switch(inc){
	       case( 1,  -1){ inc, def = -10,  1 }
	       case(10, -10){ inc, def =  -1, 10 }
	    }
	 }
	 x+=inc;
      }// while inMiddle

      if(brd[ix]==BLNK) score+=1;
      if(num!=0x39) num+=1; else num=97;	// '0' & 'a'
      brd[ix]=num.toChar();			// right back at ya
      if(inRange(x)){
	 if(ix==x) brd[ix]=R;
	 else{
	    if(brd[x]==BLNK) score+=1;
	    brd[x]=num.toChar();
	 }
      }
   }
   drawGrid(  score, guesses);
   finalScore(score, guesses);
}

fcn finalScore(score, guesses){
println(hdn.toString(*));
   foreach i in ([11..88]){
      m:=i%10;
      if(m==0 or m==9)		  	    continue;
      if(brd[i]==G and hdn[i]==ATM)         brd[i]=GY;
      else if(brd[i]==G and hdn[i]==F){     brd[i]=GN; score+=5; }
      else if(brd[i]==BLNK and hdn[i]==ATM) brd[i]=ATM;
   }
   drawGrid(score, guesses);
}

while(True){
   initialize(); play();
   while(True){
      yn:=ask("    Play again y/n : ").strip().toLower();
      if(yn=="n")      break(2);
      else if(yn=="y") break(1);
   }
}

Showing [results of] most of the Wikipedia actions:

Output:
    === BLACK BOX ===
 
    H    Hit (scores 1)
    R    Reflection (scores 1)
    1-9, Detour (scores 2)
    a-c  Detour for 10-12 (scores 2)
    G    Guess (maximum 4)
    Y    Correct guess
    N    Incorrect guess (scores 5)
    A    Unguessed atom
 
    Cells are numbered a0 to j9.
    Corner cells do nothing.
    Use edge cells to fire beam.
    Use middle cells to add/delete a guess.
    Game ends automatically after 4 guesses.
    Enter q to abort game at any time.


    0   1   2   3   4   5   6   7   8   9 
      ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
a     ║   ║   ║   ║   ║   ║   ║   ║   ║   
  ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
c ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
d ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
e ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
f ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
g ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
h ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
i ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j     ║   ║   ║   ║   ║   ║   ║   ║   ║   
      ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝

        Score = 0	Guesses = 0	 Status = In play
    Choose cell [a-j][0-9]: g9

    0   1   2   3   4   5   6   7   8   9 
      ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
a     ║   ║   ║   ║   ║   ║   ║   ║   ║   
  ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
c ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
d ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
e ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
f ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
g ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ H ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
h ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
i ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j     ║   ║   ║   ║   ║   ║   ║   ║   ║   
      ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝

        Score = 1	Guesses = 0	 Status = In play
...
    Choose cell [a-j][0-9]: f0

    0   1   2   3   4   5   6   7   8   9 
      ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
a     ║   ║   ║ 3 ║   ║ 1 ║ 3 ║   ║   ║   
  ╔═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╗
b ║ 2 ║   ║   ║   ║   ║   ║   ║   ║   ║ 2 ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
c ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
d ║   ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
e ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 4 ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
f ║ 5 ║   ║   ║   ║   ║   ║   ║   ║   ║ 1 ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
g ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ H ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
h ║   ║   ║   ║   ║   ║   ║   ║   ║   ║ 4 ║ 
  ╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬
i ║   ║   ║   ║   ║   ║   ║ G ║   ║   ║   ║ 
  ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝
j     ║   ║   ║   ║   ║ 5 ║ R ║ H ║ R ║   
      ╚═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╝

        Score = 14	Guesses = 1	 Status = In play