Black box

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

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.

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

AutoHotkeyEdit

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
;--------------------------------------------------------------------------------------

GoEdit

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

JEdit

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.

JavaScriptEdit

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();
}

JuliaEdit

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()

NimEdit

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

PhixEdit

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()

WrenEdit

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

zklEdit

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