Morpion solitaire/Julia

From Rosetta Code
Revision as of 23:31, 24 April 2020 by Wherrera (talk | contribs) (add save game to file)

GUI Gtk app version. <lang julia>using Cairo, Colors, GeometryTypes, Gtk

  1. Note: Eastward and southward are positive directions. Grid coordinates in the
  2. far left or upper areas may be negative.
  3. The convention in this program is to normalize coordinates to the origin from
  4. the point due east of the top of the cross, due north of the left of the cross:
  5. R XXXX
  6. X X
  7. X X
  8. XXXX XXXX
  9. X x
  10. X x
  11. XXXX XXXX
  12. X X
  13. X X
  14. XXXX
  1. ------------- 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

  1. --------- 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

  1. ----------- 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])
   savegame = GtkButton("Save Game")
   set_gtk_property!(savegame, :label, "Save Game")
   push!(hbox1, newgame, gamemode, savegame, 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 = ["(25,25)"], 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)
       game, gameover, filename, movehistory = MorpionGame(), false, "", ["(25,25)"]
       draw(can)
   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)) ||
               !nooverlap(move, game.moves)
               @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)
           movehistory = ["(25,25)"]
           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
   function savegametofile(w)
       filename = Gtk.save_dialog_native("File Name to Save")
       fp = open(filename, "w")
       write(fp, prod(["""
       #
       #  R  XXXX
       #     X  X
       #     X  X
       #  XXXX  XXXX
       #  X        X
       #  X        X
       #  XXXX  XXXX
       #     X  X
       #     X  X
       #     XXXX
       #
       # R = reference point
       # List of moves starts with reference point (col,row)  i.e. (25,25) below
       #
       # Annotations:
       # Lines are

# (col,row) <direction>

       #    direction   -      +
       #       |        up     down
       #       -        left   right
       #       \\        left   right
       #       /        left   right
       #    center is the distance -2, -1, ..., +2 from
       #    the move coordinate to the center of the line being drawn
       #
       (25,25)
       """; [pentasol(m) * "\n" for m in game.moves]]))
       close(fp)
   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(savegametofile, savegame, :clicked)
   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>