############################################################################ # # File: hb.icn # # Subject: Program for Hearts & Bones game # # Author: Robert J. Alexander # # Date: March 10, 1998 # ############################################################################ # # This file is in the public domain. # ############################################################################ # # Hearts & Bones # # Usage: hb [-h ] [-w ] [-b <# bones>] [-B] # # -B says to print the actual number of bones placed. # # For best results, use odd board heights and widths, and even # square heights and widths. # # Defaults: board height = 9, board width = 13, # bones = 25. # # --- Game Play --- # # Hit "q" to quit, "r" to start a new game. # # The object is to visit all the safe squares without stepping on a # bone. # # You *visit* a square by clicking the left mouse button in it. If the # square is safe, a number is posted in it that reveals the number of # squares in the eight neighboring squares the contain bones. Squares # containing hearts (represented by $) are always safe. # # You can only visit squares that are adjacent to squares already # visited. At the start of a game, the upper left square (a heart # square) is pre-visited for you. If a visited square has no # neighbors, its adjacent squares are automatically visited for you, as # a convenience. # # At any time you can *mark* a square that you believe has a bone by # clicking the right (or center) mouse button on it. This is a memory # aid only -- if you visit it later (and you were right), you're dead. # There is no confirmation whether a square you have marked really # contains a bone, although you will probably find out later when it # causes you to make a mistake. A right-button click on a marked # square unmarks it. # # The game ends when you have visited all safe squares or stepped on a # bone. (Presently, there is no automatic detection of a winning board # -- you just have to notice that for yourself). # # NOTE: If you use the command line options to alter the setup # parameters (e.g. increasing the number of squares, or *decreasing* # the number of bones), you might get a stack overflow due, I think, to # deep recursion. I have found that setting the environment variable # MSTKSIZE=30000 works well. # ############################################################################ # # Links: options, random, wopen # ############################################################################ # # Requires: Version 9 graphics # ############################################################################ link options link random link wopen global height, width, nbr_bones, x1, y1, sq, print_bone_count procedure main(arg) initialize(arg) play() return end procedure draw_board(win) local x, y, x2, y2 x2 := x1 + width * sq y2 := y1 + height * sq x := x1 every 1 to width + 1 do { DrawLine(win, x, y1, x, y2) x +:= sq } y := y1 every 1 to height + 1 do { DrawLine(win, x1, y, x2, y) y +:= sq } return end procedure set_up_board(win, visited) local board, pt EraseArea(win) board := make_board() set_bones(board, nbr_bones) calc_neighbors(board) draw_board(win) draw_hearts(win) every pt := spread_zeros(board, 1, 1) do { write_to_square(win, pt[1], pt[2], pt[3]) visited[pt[1], pt[2]] := 1 } return board end procedure draw_hearts(win) local pt every pt := generate_heart_squares() do write_to_square(win, pt[1], pt[2], "$") return end procedure legal_move(x, y, visited) local xx, yy every xx := x - 1 to x + 1 & yy := y - 1 to y + 1 do if \visited[xx, yy] then { visited[x, y] := 1 return } end procedure play() local win, x, y, evt, mark, marks, board, visited, pt, value sq := (if match("OS/2", &host) then 30 else 20) x1 := 10 y1 := 10 win := WOpen("label=HB", "size=" || width * sq + 2 * x1 || "," || height * sq + 2 * y1) repeat { visited := make_board() board := set_up_board(win, visited) marks := make_board(" ") repeat { evt := Event(win) case type(evt) of { "string": case map(evt) of { "q": exit() "r": break next } "integer": { if evt = &lrelease then { x := (&x - x1) / sq + 1 y := (&y - y1) / sq + 1 if legal_move(x, y, visited) then { value := board[x, y] if value ~=== "X" then { # # Visited a safe square. # if value = 0 then every pt := spread_zeros(board, x, y) do { write_to_square(win, pt[1], pt[2], pt[3]) visited[pt[1], pt[2]] := 1 } else write_to_square(win, x, y, value) } else { # # Stepped on a bone -- game over. # every x := 1 to width & y := 1 to height do { value := board[x, y] write_to_square(win, x, y, "X" === value) } draw_hearts(win) repeat { evt := Event(win) case type(evt) of { "integer": if evt = &lrelease then break "string": case map(evt) of { "q": exit() "r": break } } } break } } } else if evt = (&mrelease | &rrelease) then { x := (&x - x1) / sq + 1 y := (&y - y1) / sq + 1 mark := marks[x, y] := if marks[x, y] == " " then "#" else " " write_to_square(win, x, y, mark) } } } } } end procedure spread_zeros(board, x, y, doneset) local xx, yy, v, donekey /doneset := set() donekey := x || "," || y if member(doneset, donekey) then fail insert(doneset, donekey) (v := board[x, y]) | fail suspend [x, y, v] if v === 0 then { every xx := x - 1 to x + 1 & yy := y - 1 to y + 1 do if not(x = xx & y = yy) & board[xx, yy] then suspend spread_zeros(board, xx, yy, doneset) } end procedure write_to_square(win, x, y, s) WAttrib(win, "x=" || x1 + (x - 1) * sq + sq / 2 - 2, "y=" || y1 + (y - 1) * sq + sq / 2 + 4) return writes(win, s) end procedure get_options(arg) local opt opt := options(arg, "h+w+b+B") height := \opt["h"] | 9 width := \opt["w"] | 15 nbr_bones := \opt["b"] | (height * width - 9) / 5 print_bone_count := opt["B"] width <:= 5 height <:= 5 nbr_bones >:= height * width * 2 / 3 return opt end procedure initialize(arg) randomize() get_options(arg) return end procedure make_board(init_value) local board board := list(width) every !board := list(height, init_value) return board end procedure generate_heart_squares() suspend [1 | (width + 1) / 2 | width, 1 | (height + 1) / 2 | height] end procedure set_bones(board, nbr_bones) local i, j, pt, bone_count every pt := generate_heart_squares() do board[pt[1], pt[2]] := "$" board[1, 2] := board[2, 1] := board[2, 2] := "$" bone_count := 0 every 1 to nbr_bones do { # # Loop to find a spot with a path back to the start. If we don't # find one after several tries, quit placing bones. # (every 1 to 20 do { i := ?width j := ?height if /board[i, j] then { board[i, j] := "X" if hearts_reachable(board) then { bone_count +:= 1 break } else board[i, j] := &null } }) | break } if \print_bone_count then write(&errout, bone_count, " bones") return end procedure calc_neighbors(board) local i, j, ii, jj, neighbors every i := 1 to width & j := 1 to height do { if board[i, j] ~=== "X" then { neighbors := 0 every ii := i - 1 to i + 1 & jj := j - 1 to j + 1 do { if board[ii, jj] === "X" then neighbors +:= 1 } board[i, j] := neighbors } } return end procedure hearts_reachable(board) local pt every pt := generate_heart_squares() do { if not path_to_start(pt[1], pt[2], board) then fail } return end procedure path_to_start(x, y, board, doneset) local xx, yy, donekey /doneset := set() if not(board[x, y] ~=== "X") then fail if x = 1 & y = 1 then return donekey := x || "," || y if member(doneset, donekey) then fail insert(doneset, donekey) every xx := x - 1 to x + 1 & yy := y - 1 to y + 1 do { if x = xx & y == yy then next if path_to_start(xx, yy, board, doneset) then return } end