diff --git a/pegsol/Makefile b/pegsol/Makefile index 532bb0c70..b34738663 100644 --- a/pegsol/Makefile +++ b/pegsol/Makefile @@ -1,2 +1,4 @@ test: + ruby converter.rb +# ruby -I. generator.rb ./pegsol-instances.py # Only analyzes instances, doesn't generate them. diff --git a/pegsol/README.md b/pegsol/README.md new file mode 100644 index 000000000..e7f3e5a2a --- /dev/null +++ b/pegsol/README.md @@ -0,0 +1,33 @@ +Adaption of Puzzle library from Solipeg 2.2 +(http://ourworld.compuserve.com/homepages/cade/psionsof.htm, changed all +unmovable pegs to movable pegs) + +Solipeg, a Classic Marble Puzzle Game for the Psion Series 3a, 3c, Siena +Version 2.2 (and 2.2 Lite) Copyright (C) 1993, 1994, 1995, 1996 J Cade Roux + +"The game Puzzle-Peg (Third Edition, 1924, Lubbers & Bell Mfg. Co.,Clinton, +Iowa, USA, 50 cents) included a small booklet (23 pages),"Problems in +Puzzle-Peg", which details over 100 different end-game-type problems which +can be played on the standard English board, sent in by players of their +earlier versions. It also contained adverts for other games of theirs. All +104 of the problems are included in this distribution - embedded in the +library. The Lite version contains only the standard two starting positions, +although an external library is provided which is comparable to the internal +library." + + + + +# pddl-generators memo + +The original code was probably written for ruby 1.8. +The code was fixed for ruby 2.x. +This ruby code reproduces instances takes from the book. + +`ruby converter.rb` seems to generate a template for `instances.rb`. +`ruby -I. generator.rb` will generate problem files. + + +A new randomized implementation (generate.py) will generate a new board +which is not guaranteed to be solvable. +It generates a random board state and has a customizable board size and a corner drop off size. diff --git a/pegsol/generate.py b/pegsol/generate.py new file mode 100644 index 000000000..0ac14434a --- /dev/null +++ b/pegsol/generate.py @@ -0,0 +1,142 @@ +#!/bin/env python3 + +""" +A randomized Pegsol generator for sequential STRIPS with negative preconditions. +""" + + +import argparse +import random +import numpy as np +import numpy.random as nr +import functools +import textwrap + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--rows", type=int, default=7, help="Number of rows, must be odd.") +parser.add_argument("--cols", type=int, default=7, help="Number of columns, must be odd.") +parser.add_argument("--rowcuts", type=int, default=2, help="Number of rows to cut from each corner. Should be less than floor(rows/2)") +parser.add_argument("--colcuts", type=int, default=2, help="Number of cols to cut from each corner. Should be less than floor(cols/2)") +parser.add_argument("occupancy", type=float, help="Probability that the pegs are initially occupied. (0, 1]") +# parser.add_argument("--european", action="store_true", help="If present, corners are cut in a triangular form (a European pegsol board)") +parser.add_argument("--seed", type=int, default=1) +args = parser.parse_args() + + +random.seed(args.seed) + +# for development +# rows = 7 +# cols = 7 +# rowcuts = 2 +# colcuts = 2 +# occupancy = 0.8 + + +rows = args.rows +cols = args.cols +rowcuts = args.rowcuts +colcuts = args.colcuts +occupancy = args.occupancy + +assert rows % 2 == 1, f"rows={rows} must be odd" +assert cols % 2 == 1, f"cols={cols} must be odd" +assert rowcuts < rows // 2, f"rowcuts={rowcuts} must be less than floor(rows={rows}/2)" +assert rowcuts < rows // 2, f"rowcuts={rowcuts} must be less than floor(rows={rows}/2)" +assert colcuts < cols // 2, f"colcuts={colcuts} must be less than floor(cols={cols}/2)" +assert occupancy <= 1.0, f"occupancy={occupancy} must be in 0 < occupancy <= 1" +assert 0.0 < occupancy, f"occupancy={occupancy} must be in 0 < occupancy <= 1" + +total = rows * cols - 4 * rowcuts * colcuts + +################################################################ +# board generation + +INVALID = -1 +FREE = 0 +PEG = 1 + +# initialize free:0 or peg:1 +board = (nr.random((rows, cols)) < occupancy).astype(int) + +# cut the corners +board[:rowcuts,:colcuts] = -1 +board[-rowcuts:,:colcuts] = -1 +board[:rowcuts,-colcuts:] = -1 +board[-rowcuts:,-colcuts:] = -1 + + +################################################################ +# pddl generation + +def pos(i,j): + return f"pos-{i}-{j}" + + +def objects(): + return [ pos(i,j) for i,j in zip(*np.where(board != INVALID)) ] + ["-", "location"] + + +def init(): + return [ + ["=", ["total-cost"], "0"], + ["move-ended"], + *list(in_line()), + *[ ["free", pos(i,j)] for i,j in zip(*np.where(board == FREE)) ], + *[ ["occupied", pos(i,j)] for i,j in zip(*np.where(board == PEG)) ], + ] + + +def in_line(): + for i,j in zip(*np.where(board != INVALID)): + # horizontal move + if j+2 < cols and board[i, j] != INVALID and board[i, j+1] != INVALID and board[i, j+2] != INVALID: + yield ["IN-LINE", pos(i,j), pos(i,j+1), pos(i,j+2), ] + yield ["IN-LINE", pos(i,j+2), pos(i,j+1), pos(i,j), ] + # vertical move + if i+2 < rows and board[i, j] != INVALID and board[i+1, j] != INVALID and board[i+2, j] != INVALID: + yield ["IN-LINE", pos(i,j), pos(i+1,j), pos(i+2,j), ] + yield ["IN-LINE", pos(i+2,j), pos(i+1,j), pos(i,j), ] + + +def goal(): + return [ + "and", + *[ ["free", pos(i,j)] for i,j in zip(*np.where(board != INVALID)) if i != (rows//2) and j != (cols//2) ], + ["occupied", pos((rows//2), (cols//2))] + ] + + +def problem(): + return [ + "define", ["problem", "pegsolprob"], + [":domain", "pegsolitaire-sequential"], + [":objects", *objects()], + [":init", *init()], + [":goal", goal()], + [":metric", "minimize", ["total-cost"]], + ] + + +def print_sexp(obj,indent=0): + if isinstance(obj, str): + return obj + assert isinstance(obj, list), f"{obj} is not str or list" + if len(obj) > 4: + return textwrap.indent( + "(" + "\n".join( + map(lambda x: print_sexp(x,indent+1), + obj)) + ")", + " "*indent) + else: + return textwrap.indent( + "(" + " ".join( + map(lambda x: print_sexp(x,indent+1), + obj)) + ")", + " "*indent) + +print_sexp(problem()) + + + + diff --git a/pegsol/generator.rb b/pegsol/generator.rb index 562d5bd7c..0a31df533 100644 --- a/pegsol/generator.rb +++ b/pegsol/generator.rb @@ -1,6 +1,6 @@ #!/usr/bin/ruby -require 'ftools' +require 'fileutils' require 'instances.rb' class PegSolitaire @@ -483,19 +483,19 @@ def pad(counter, maxCounter) def processConfig(configName, configNum, maxNum) pegSolitaire = PegSolitaire.new(configName, $config_names[configNum-1]) - File.makedirs("tempo-sat") + FileUtils.mkdir_p("tempo-sat") domainFile = File.new("tempo-sat/domain.pddl", "w") pegSolitaire.printDomainTemporal(domainFile) problemFile = File.new("tempo-sat/p#{pad(configNum,maxNum)}.pddl", "w") pegSolitaire.printProblemTemporal(problemFile, pad(configNum,maxNum), pegSolitaire) - File.makedirs("seq-sat") + FileUtils.mkdir_p("seq-sat") domainFile = File.new("seq-sat/domain.pddl", "w") pegSolitaire.printDomainSequential(domainFile) problemFile = File.new("seq-sat/p#{pad(configNum,maxNum)}.pddl", "w") pegSolitaire.printProblemSequential(problemFile, pad(configNum,maxNum), pegSolitaire) - File.makedirs("netben-opt") + FileUtils.mkdir_p("netben-opt") domainFile = File.new("netben-opt/domain.pddl", "w") pegSolitaire.printDomainNetBenefit(domainFile) problemFile1 = File.new("netben-opt/p#{pad(configNum,maxNum)}-1.pddl", "w")