Morpion solitaire/Julia
GUI Gtk app version.
<lang julia>using Cairo, Colors, GeometryTypes, Gtk
- Note: Eastward and southward are positive directions. Grid coordinates in the
- far left or upper areas may be negative.
- The convention in this program is to normalize coordinates to the origin from
- the point due east of the top of the cross, due north of the left of the cross:
- R XXXX
- X X
- X X
- XXXX XXXX
- X x
- X x
- XXXX XXXX
- X X
- X X
- XXXX
- ------------- geometry related general functions ------------------#
Point2i = Point2{Int}
@enum Direction None SE E NE S # 0:4 const delta = Dict(SE => [1, 1], E => [1, 0], NE => [1, -1], S => [0, 1]) function direction(x1, y1, x2, y2)
dx, dy = x2 - x1, y2 - y1 return dx == 0 ? S : dy == 0 ? E : dx == dy ? NE : SE
end direction(p1, p2) = direction(p1[1], p1[2], p2[1], p2[2]) offset(p1, p2) = (direction(p1, p2) == S ? p2[2] - p1[2] : p2[1] - p1[1])
struct Move
p::Point2i d::Direction o::Int seg::Vector{Point2i} Move(p, dir, offs, seg) = (@assert(length(seg) == 5); new(p, dir, offs, seg))
end function Move(p, d::Direction, o)
return Move(p, d, o, [Point2i(p[1] + delta[d][1] * i, p[2] + delta[d][2] * i) for i in collect(-2:2) .+ o])
end function Move(pentasol, ref::Point2i, Rposition::Point2i)
if (mat = match(r"\((\d+),(\d+)\) ([\\\-/|]) ([+-]?\d+)", pentasol)) != nothing p = Point2i(parse(Int, mat[1]), parse(Int, mat[2])) .- ref .+ Rposition d = Direction(findfirst(mat[3], "\\-/|")[1]) o = parse(Int, mat[4]) return Move(p, d, o) else throw("Bad Pentasol input") end
end
function pentasol(m::Move, ref=Point2i(25, 25))
p = m.p .+ ref return "($(p[1]),$(p[2])) " * "\\-/|"[Int(m.d)] * (m.o > 0 ? " +" : " ") * "$(m.o)"
end
nooverlap(move, moves) = all(m -> m.d != move.d || isempty(move.seg ∩ m.seg), moves)
function surround(p::Point2i)
x, y = p[1], p[2] delt = [-1, 0, +1] return vec([Point2i(x + i, y + j) for i in delt, j in delt if (i !=0 || i != j)])
end
function adjacent(pvec::Vector{Point2i})
points = Point2i[] for p in pvec, adj in surround(p) !(adj in pvec) && !(adj in points) && push!(points, adj) end return points
end
- --------- game structures and functions ---------------------#
struct Board
Rvalue::Point2i points::Vector{Point2i}
end
const initialboard() = Board(
Point2i(0, 0), Point2i.([[3, 0], [4, 0], [5, 0], [6, 0], [3, 1], [6, 1], [3, 2], [6, 2], [0, 3], [1, 3], [2, 3], [3, 3], [6, 3], [7, 3], [8, 3], [9, 3], [0, 4], [9, 4], [0, 5], [9, 5], [0, 6], [1, 6], [2, 6], [3, 6], [6, 6], [7, 6], [8, 6], [9, 6], [3, 7], [6, 7], [3, 8], [6, 8], [3, 9], [4, 9], [5, 9], [6, 9]]),
)
struct MorpionGame
board::Board moves::Vector{Move}
end MorpionGame() = MorpionGame(initialboard(), Vector{Move}())
score(mg::MorpionGame) = length(mg.moves) moveok(m, b) = !(m.p in b.points) && all(x -> x in b.points, filter(x -> x != m.p, m.seg)) addmove(m, b) = if moveok(m.p, b) push!(b.points, m.p) else error("Bad move $m with $b") end
function has4inarowbefore(p, pvec, d)
xsign, ysign = delta[d] return all(i -> Point2i(p[1] + xsign * i, p[2] + ysign * i) in pvec, -1:-1:-4)
end
function has4inarowafter(p, pvec, d)
xsign, ysign = delta[d] return all(i -> Point2i(p[1] + xsign * i, p[2] + ysign * i) in pvec, 1:4)
end
function has1gap3inarow(p, pvec, d)
xsign, ysign = delta[d] return all(i -> Point2i(p[1] + xsign * i, p[2] + ysign * i) in pvec, [-1, 1, 2, 3])
end
function has3gap1inarow(p, pvec, d)
xsign, ysign = delta[d] return all(i -> Point2i(p[1] + xsign * i, p[2] + ysign * i) in pvec, [-3, -2, -1, 1])
end
function has2gap2inarow(p, pvec, d)
xsign, ysign = delta[d] return all(i -> Point2i(p[1] + xsign * i, p[2] + ysign * i) in pvec, [-2, -1, 1, 2])
end
function any5inarow(p, pvec, d)
moves = Move[] if has4inarowbefore(p, pvec, d) push!(moves, Move(p, d, -2)) end if has4inarowafter(p, pvec, d) push!(moves, Move(p, d, 2)) end if has1gap3inarow(p, pvec, d) push!(moves, Move(p, d, 1)) end if has3gap1inarow(p, pvec, d) push!(moves, Move(p, d, -1)) end if has2gap2inarow(p, pvec, d) push!(moves, Move(p, d, 0)) end return moves
end
function possiblemovesforpoint(p, game)
return filter(m -> nooverlap(m, game.moves), mapreduce(d -> any5inarow(p, game.board.points, d), vcat, [NE, E, SE, S]))
end
function readmorpion(filename)
foundlisting, foundR, setR, leftmargin, ry, rx = false, false, false, 1000, 0, 1000 lines = strip.(split(read(filename, String), "\n")) nullpoint = Point2i(typemin(Int), typemin(Int)) xyreference, Rpoint, moves = nullpoint, nullpoint, Move[] for line in lines isempty(line) && continue if !setR if line[1] == '#' && occursin("X", line[2:end]) && !occursin(r"[a-zA-QS-WYZ]", line) if !foundR ry += 1 end if (x = findfirst("X", line[2:end])) != nothing leftmargin = min(leftmargin, x[1] - 1) end if occursin("R", line) && !occursin(r"[a-zA-QS-WYZ]", line) foundR = true if (x = findfirst("R", line[2:end])) != nothing rx = x[1] end end end end if !foundlisting if (mat = match(r"^\((\d+),(\d+)\)$", line)) != nothing rx -= leftmargin Rpoint = Point2i(rx - 1, ry - 1) xyreference = Point2i(parse(Int, mat[1]), parse(Int, mat[2])) setR, foundlisting = true, true end else # all the rest should be move listings push!(moves, Move(line, xyreference, Rpoint)) end end return xyreference, Rpoint, moves
end
- ----------- game app ------------------------------#
function morpionapp(gridsize=42)
win = GtkWindow("Morpion Game", 720, 640) |> (GtkFrame() |> (box = GtkBox(:v))) hbox1 = GtkBox(:h) newgame = GtkButton("New Game") set_gtk_property!(newgame, :label, "New Game") gamemode = GtkComboBoxText() modechoices = ["Random Demo", "Solitaire Play", "Pre-Recorded"] modelabels = [" Random Play in Progress", " Choose A Move", " Playing Back File"] append!(gamemode, modechoices) set_gtk_property!(gamemode, :active, 0) modelabel = GtkLabel(modelabels[1]) push!(hbox1, newgame, gamemode, modelabel) hbox2 = GtkBox(:h) can = GtkCanvas(500, 500) set_gtk_property!(can, :expand, true) push!(hbox2, can) scrwin = GtkScrolledWindow() txtbuffer = GtkTextBuffer() set_gtk_property!(txtbuffer, :text, "Move History") historywindow = GtkTextView() set_gtk_property!(historywindow, :editable, false) set_gtk_property!(historywindow, :expand, true) set_gtk_property!(historywindow, "left-margin", 40) set_gtk_property!(historywindow, :buffer, txtbuffer) push!(scrwin, historywindow) push!(hbox2, scrwin) push!(box, hbox1, hbox2) solitaire_gotfirst, solp1, solp2 = false, Point2i(0, 0), Point2i(0, 0) movehistory, colorhistory, playbackmoves = ["(28,28)"], Color[], Move[] game = MorpionGame() gameover, mode, exitrequested, margin, r, filename = false, 0, false, 10, 16, "" linecolors = distinguishable_colors(200, colorant"black", dropseed=true)
function newgame!(w) empty!(movehistory) game = MorpionGame() gameover = false draw(can) filename = "" end @guarded function solitaireclick!(w, event) get_gtk_property(gamemode, :active, Int) != 1 && return delta = div(gridsize, 2) - 5 x, y = Int(round(event.x / r - delta)), Int(round(event.y / r - delta)) if solitaire_gotfirst solp2 = Point2i(x, y) if solp1 == solp2 movs = filter(m -> m.o == 0, possiblemovesforpoint(solp1, game)) d = isempty(movs) ? SE : movs[1].d else d = direction(solp1, solp2) end move = Move(solp1, d, offset(solp1, solp2)) if isempty(any5inarow(move.p, game.board.points, move.d)) @warn("bad move $move") else pushmove!(game, move) end solitaire_gotfirst = false else solp1 = Point2i(x, y) solitaire_gotfirst = true end end can.mouse.button1press = solitaireclick! function gamemode!(w) mode = get_gtk_property(gamemode, :active, Int) if mode == 0 # demo mode set_gtk_property!(modelabel, :label, modelabels[1]) elseif mode == 1 # solitaire set_gtk_property!(modelabel, :label, modelabels[2]) info_dialog("Click on a valid empty nearby square to move, " * "then immediately click on the midpoint square for that move.", win) newgame!(win) else # playback set_gtk_property!(modelabel, :label, modelabels[3]) info_dialog("Pick a Pentasol format text file of the game to replay.", win) newgame!(win) end end function pushmove!(game, move) push!(game.board.points, move.p) push!(game.moves, move) push!(movehistory, pentasol(move)) push!(colorhistory, rand(linecolors)) draw(can) show(can) end function addmove!(verbose=true) candidates = mapreduce(p -> possiblemovesforpoint(p, game), vcat, adjacent(game.board.points)) if isempty(candidates) outofmoves() else move = rand(candidates) pushmove!(game, move) end end function playback!() if filename == "" filename = open_dialog("Pick a Pentasol formal Morpion game file.") if filename == "" sleep(3) return end xyreference, Rpoint, playbackmoves = readmorpion(filename) empty!(movehistory) game = MorpionGame() gameover = false end if isempty(playbackmoves) outofmoves() else move = popfirst!(playbackmoves) pushmove!(game, move) end end function outofmoves() warn_dialog("Out of moves! Game over.\nTotal moves: $(length(game.moves))", win) gameover = true end @guarded draw(can) do widget ctx = Gtk.getgc(can) select_font_face(ctx, "Courier", Cairo.FONT_SLANT_NORMAL, Cairo.FONT_WEIGHT_BOLD) fontpoints, delt = 12, div(gridsize, 2) - 6 set_font_size(ctx, fontpoints) function squareat(color, x, y, siz) set_source(ctx, color) move_to(ctx, margin + (x + delt) * r, margin + (y + delt) * r) rectangle(ctx, margin + (x + delt) * r, margin + (y + delt) * r, siz, siz) fill(ctx) end function textat(color, x, y, text) set_source(ctx, color) move_to(ctx, margin + (x + delt) * r + 2, margin + (y + delt) * r + fontpoints) show_text(ctx, text) stroke(ctx) end function point2line(color, p1, p2, siz) set_line_width(ctx, 3) set_source(ctx, color) move_to(ctx, margin + (p1[1] + delt) * r + 5, margin + (p1[2] + delt) * r + 5) line_to(ctx, margin + (p2[1] + delt) * r + r - 5, margin + (p2[2] + delt) * r + r - 5) stroke(ctx) end pts, adj = game.board.points, adjacent(game.board.points) # print black box graphic for i in -delt+1:delt+3, j in -delt+1:delt+3 squareat(colorant"wheat", i, j, r) i == j == 0 && textat(colorant"maroon", i, j, "R") pt = Point2i(i, j) # show current points with an x char pt in pts && textat(colorant"black", i, j, "x") pt in adj && squareat(colorant"gold", i, j, r) # if there is a move, mark it end # draw dividing lines set_line_width(ctx, 1) set_source(ctx, colorant"navy") linelen = delt * 2 + 4 for i in 1:linelen move_to(ctx, margin + i * r, margin + r) line_to(ctx, margin + i * r, margin + linelen * r) stroke(ctx) move_to(ctx, margin + r, margin + i * r) line_to(ctx, margin + linelen * r, margin + i * r) stroke(ctx) end for (i, m) in enumerate(game.moves) point2line(colorhistory[i], m.seg[1], m.seg[5], r) end # print move record at the same time by sending lines to the label listing set_gtk_property!(txtbuffer, :text, join(["Move History"; movehistory], "\n")) set_gtk_property!(historywindow, :buffer, txtbuffer) Gtk.showall(win) end endit(w) = (exitrequested = true) signal_connect(newgame!, newgame, :clicked) signal_connect(gamemode!, gamemode, :changed) signal_connect(endit, win, :destroy)
while !exitrequested showall(win) if gameover sleep(0.3) elseif mode == 0 # demo mode sleep(2) addmove!() elseif mode == 1 # solitaire mode sleep(0.1) yield() else # playback mode sleep(2) playback!() end yield() end
end
morpionapp() </lang>