Initial attempt at a kif to Prolog translator

Edit I’ve changed [60,61] to “<=” and similarly other codes I was using to strings thanks to @jan pointing out that works.

After a bit of a hiatus, I’ve started tinkering around with automating the conversion of some of the game rules from Stanford’s General Game Playing project written in kif to Prolog.

Here’s how far I’ve got so far:

#!/usr/bin/env swipl
:- use_module(library(dcg/basics)).

:- initialization(main, main).

main(Argv) :-
  last(Argv, File),
  phrase_from_file(terms(Terms), File),
  maplist(format('~w.~n'), Terms).

terms([Term|Terms]) --> blanks, term(Term), blanks, !, terms(Terms).
terms([])             --> [].

term(Comment)  --> comment(Cs), { string_codes(Comment, Cs) }.
term(Compound) --> compound_term(L), { Compound =.. L }.
term(Variable) --> variable(Cs), { string_codes(Variable, Cs) }.
term(Constant) --> constant(Cs), { atom_codes(Constant, Cs) }.
term(':-')     --> "<=".

compound_term(Terms) --> "(", terms(Terms) ,")".

variable([U|Ls])      --> [63,L], { to_upper(L, U) }, word_rest(Ls).
constant([L|Ls])      --> [L], { code_type(L, alnum) }, word_rest(Ls).
word_rest([L|Ls]) --> [L], { code_type(L, alnum) }, word_rest(Ls).
word_rest([])     --> [].

comment([37|Cs])  --> ";", comment_rest(Cs).
comment_rest([37|Cs])  --> ";", comment_rest(Cs).
comment_rest([C|Cs])  --> [C], { \+ code_type(C, end_of_line) }, comment_rest(Cs).
comment_rest([])  --> [C], { code_type(C, end_of_line) }.

Using TicTacToe as an example, running

./kif2prolog.pl ticTacToe.kif > test.pl

produces

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
%%% Tictactoe.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
%% Roles.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
role(xplayer).
role(oplayer).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
%% Base & Input.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
index(1).
index(2).
index(3).
:-(base(cell(X,Y,b)),index(X),index(Y)).
:-(base(cell(X,Y,x)),index(X),index(Y)).
:-(base(cell(X,Y,o)),index(X),index(Y)).
base(control(P)):-role(P).
:-(input(P,mark(X,Y)),index(X),index(Y),role(P)).
input(P,noop):-role(P).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
%% Initial State.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
init(cell(1,1,b)).
init(cell(1,2,b)).
init(cell(1,3,b)).
init(cell(2,1,b)).
init(cell(2,2,b)).
init(cell(2,3,b)).
init(cell(3,1,b)).
init(cell(3,2,b)).
init(cell(3,3,b)).
init(control(xplayer)).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
%% Dynamic Components.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
%% Cell.
:-(next(cell(M,N,x)),does(xplayer,mark(M,N)),true(cell(M,N,b))).
:-(next(cell(M,N,o)),does(oplayer,mark(M,N)),true(cell(M,N,b))).
:-(next(cell(M,N,W)),true(cell(M,N,W)),distinct(W,b)).
:-(next(cell(M,N,b)),does(W,mark(J,K)),true(cell(M,N,b)),or(distinct(M,J),distinct(N,K))).
next(control(xplayer)):-true(control(oplayer)).
next(control(oplayer)):-true(control(xplayer)).
:-(row(M,X),true(cell(M,1,X)),true(cell(M,2,X)),true(cell(M,3,X))).
:-(column(N,X),true(cell(1,N,X)),true(cell(2,N,X)),true(cell(3,N,X))).
:-(diagonal(X),true(cell(1,1,X)),true(cell(2,2,X)),true(cell(3,3,X))).
:-(diagonal(X),true(cell(1,3,X)),true(cell(2,2,X)),true(cell(3,1,X))).
line(X):-row(M,X).
line(X):-column(M,X).
line(X):-diagonal(X).
open:-true(cell(M,N,b)).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
:-(legal(W,mark(X,Y)),true(cell(X,Y,b)),true(control(W))).
legal(xplayer,noop):-true(control(oplayer)).
legal(oplayer,noop):-true(control(xplayer)).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
goal(xplayer,100):-line(x).
:-(goal(xplayer,50),not(line(x)),not(line(o)),not(open)).
goal(xplayer,0):-line(o).
goal(oplayer,100):-line(o).
:-(goal(oplayer,50),not(line(x)),not(line(o)),not(open)).
goal(oplayer,0):-line(x).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.
terminal:-line(x).
terminal:-line(o).
terminal:-not(open).

Loading that into swipl produces lots of warnings about singleton variables and clauses not being together, but it does work.

One horrible hack I’ve done is using format to place the full stop at the end of clauses. I find DCGs fun, but also pretty frustrating because I really struggle to find some simple examples to try understand stuff from.

One little tip: literals may be written as strings, so

term(':-')     --> ":-".

To get rid of the singletons, you can use numbers/4 with the option singletons(true) and than write_term/2 using the option numbervars(true). For the discontiguous you have three options: reorder the clauses by predicate first, disable the warnings using style_check/1 or add the required discontiguous/1 directives.

2 Likes

Thanks for the tip. I swear I tried that earlier and it didn’t work, but it might have been mixed with some other typo at that stage.

As a next step, I was hoping to convert lines like

(<= (base (cell ?x ?y b)) (index ?x) (index ?y))

to

base(cell(X,Y,b)) :- index(X), index(Y).

rather than the

:-(base(cell(X,Y,b)),index(X),index(Y)).

which works, but isn’t as readable.

What I though might work is:

compound_term([Head, ':-', Body) --> "(<=", term(Head), terms(Body), ")".

but that results in “Syntax error: Illegal start of term”.

Any clues on how to write it?

Edit The reason my code above doesn’t work is it should be:

compound_term([Head,':-'|Body]) --> "(<=", blanks, term(Head), blanks, terms(Body) ,")".

ie I did the common noob mistake of using a comma instead of a vertical bar to split the head and tail of the list.

Not really sure what you mean. You are parsing KIF and want to create Prolog clauses, no? But a clause is not :-(Head, G1, G2, …). Instead, it is a term of the shape :-(Head, Body) and if Body is a conjunction this becomes :-(Head, (G1, G2, …)), or simply (Head :- Body).

Does that help?

1 Like

Ideally what I’m trying to do is write a script that pretty prints kif as Prolog.

A couple of years ago I wrote the Python script bellow and it sort of does what I want, ie puts :- between the head and the body. Other snags with kif’s lisp-style syntax such as or(P1 P2 P3 ...) it translates (P1 ; P2 ; P3 …), and it translates not to \+ etc.

I thought it would be fun to redo my old Python script as a Prolog DCG. I’m finding it tougher than I thought it would be, partly because I’ve got very rusty in both languages.

#!/usr/bin/env python
"""
eg 
python kif2prolog.py -f connectFour.kif -o connectFour.pl
produces file
connectFour.pl
"""

import argparse
import shlex

def prolog_rules(rules):
    """
    Translate rules into prolog and return as a long string.
    Specific "trues" and queries are later appended to this on a case
    by case basis
    """
    def rewrite(rule):
        "Recursive helper for nested s-expressions"
        if all([isinstance(atom, str) for atom in rule]) and rule[0] == 'or':
            return '(' + ' ; '.join(rule[1:]) + ')'
        elif rule[0] == 'not':
            rule[0] = '\\+'
            return rewrite(rule)
            # return '\\+' + rewrite(rule[1])
        elif rule[0] == 'distinct':
            rule[0] = 'dif'
            return rewrite(rule)
        elif rule[0] == 'number':
            rule[0] = 'num'
            return rewrite(rule)
        elif rule[0] == 'succ':
            rule[0] = 'successor'
            return rewrite(rule)
        elif all([isinstance(atom, str) for atom in rule]) and rule[0] != '<=':
            return rule[0] + str(rule[1:]).replace('[', '(').replace(']', ')').replace("'", '')
        else:
            rule_copy = list(rule)
            for idx in range(len(rule)):
                if isinstance(rule[idx], list):
                    rule_copy[idx] = rewrite(rule_copy[idx])
            return rewrite(rule_copy)

    prolog = ''
    # prolog += 'distinct(A, B) :- A \\= B.\n'
    for rule in rules:
        if rule[0] != '<=':
            next_rule = rewrite(rule) + '.\n'
        else:
            next_rule = rewrite(rule[1]) + ' :- ' + ", ".join([rewrite(body) \
              for body in rule[2:]]) + '.\n'
        prolog += next_rule
    return prolog

def parse_kif(kif):
    """
    Convert a lisp-style kif read the supplied file name
    into a python list suitable to convert into a prolog string
    """

    def atom(token):
        "Convert ?var into Var if appropriate"
        if token[0] == '?':
            return token[1:].title()
        else:
            return token.lower()

    def parse(tokens):
        "Recursive helper function"
        token = tokens.pop(0)
        if '(' == token:
            term = []
            while tokens[0] != ')':
                term.append(parse(tokens))
            tokens.pop(0)
            return term
        else:
            return atom(token)

    lexer = shlex.shlex(kif)
    lexer.commenters = ';'
    lexer.wordchars += '<=?-'
    lexer = ['('] + list(lexer) + [')']
    return parse(lexer)


arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("-f", "--file", help="Input kif file", type=str)
arg_parser.add_argument("-o", "--output", help="Output prolog file", type=str)
args = arg_parser.parse_args()

with open(args.file, 'r') as kif_file:
    read_data = kif_file.read()
kif_file.closed

# outfile = args.file.split('.')[0] + '.pl'
outfile = args.output
with open(outfile, 'w') as prolog_file:
    prolog_file.write(prolog_rules(parse_kif(read_data)))
prolog_file.closed

I think you need a more high level parser for KIF constructs rather than just KIF tokens. So, you get something like below that returns a Prolog term that captures a larger KIF construct.

 rule(Head:-Body) --> "(", "<=", head(Head), body(Body), ")".

Next you need to deal with white space everywhere, so I’d typically have something like

token(Token) --> ws, ...

so we can use token(‘(’), token(‘<=’), … My S-expression and surely KIF is rather rusty :), so I’m not sure these are the right abstractions. For example, can you tokenize without taking the context into consideration?

1 Like

I’m now tinkering around with starting at a higher level as you suggest. I’ll hopefully post v2 here in a day or two.

Besides preserving comments (which just involved converting lisp’s ; to prolog’s %), I’d also like to preserve the original “white space for readability” which in my initial attempt above gets lost.

So my plan is to first parse into comments, whitespace, and terms, then proceed as above.

Anyways, all very educational for me.

Here’s my current version which is still work in progress. One improvement is it fetches the kif file using http_open/3.

Using some horrible hacks I managed to translate (<= Head Body) to Head :- Body, with the snag being Body is a list of predicates which causes my attempt at Rule =.. [Head, ':-'|Body] to barf, so I’ve sort of got it to work through converting the list to string, but now the variables are all double quoted…

The code is

#!/usr/bin/env swipl

:- use_module(library(http/http_open)).
:- use_module(library(dcg/basics)).

:- initialization(main, main).

main(Argv) :-
  last(Argv, Url),
  http_open(Url, Stream, []),
  phrase_from_stream(lines(_Lines), Stream).

lines([Line|Lines]) --> line(Line), !, lines(Lines).
lines([])           --> [].

line(Comment)    --> comment(Cs), { string_codes(Comment, Cs), format('~w~n', [Comment]) }.
line(Whitespace) --> whitespace(Cs), { string_codes(Whitespace, Cs), format('~w', [Whitespace]) }.
line(_Rule) --> "(<=", blanks, term(Head), blanks, terms(Body), blanks,  ")", 
  { term_string(Body, Str1),
    split_string(Str1, "[]", "", Lst),
    nth0(1, Lst, Str2),
    format('~w :- ~w.', [Head, Str2]) }.
line(Fact)      --> "(", terms(Fact), ")", { Clause =.. Fact, format('~w.', [Clause]) }.
line(_Default)      --> statement(Cs), { string_codes(Other, Cs), format('Default ~w~n', [Other]) }.

terms([Term|Terms]) --> blanks, term(Term), blanks, !, terms(Terms).
terms([])           --> [].

term(Comment)  --> comment(Cs), { string_codes(Comment, Cs) }.
term(Compound) --> compound_term(L), { Compound =.. L }.
term(Variable) --> variable(Cs), { string_codes(Variable, Cs) }.
term(Constant) --> constant(Cs), { atom_codes(Constant, Cs) }.

compound_term(Terms) --> "(", terms(Terms) ,")".

variable([U|Ls])      --> [63,L], { to_upper(L, U) }, word_rest(Ls).
constant([L|Ls])      --> [L], { code_type(L, alnum) }, word_rest(Ls).
word_rest([L|Ls]) --> [L], { code_type(L, alnum) }, word_rest(Ls).
word_rest([])     --> [].

comment([37|Cs])  --> ";", comment_rest(Cs).
comment_rest([37|Cs])  --> ";", comment_rest(Cs).
comment_rest([C|Cs])  --> [C], { \+ code_type(C, end_of_line) }, comment_rest(Cs).
comment_rest([])  --> [C], { code_type(C, end_of_line) }.

whitespace([C|Cs])    --> [C], { code_type(C, space) }, whitespace_rest(Cs).
whitespace_rest([C|Cs])    --> [C], { code_type(C, space) }, whitespace_rest(Cs).
whitespace_rest([])   --> [].

statement([C|Cs]) --> [C], { C \= 59 }, statement_rest(Cs).
statement_rest([C|Cs]) --> [C], { C \= 59 }, statement_rest(Cs).
statement_rest([]) --> [].

Running

./kif2prolog.pl http://games.ggp.org/base/games/ticTacToe/ticTacToe.kif

produces

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Tictactoe
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Roles
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

role(xplayer).
role(oplayer).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Base & Input
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

index(1). index(2). index(3).
base(cell(X,Y,b)) :- index("X"),index("Y").
base(cell(X,Y,x)) :- index("X"),index("Y").
base(cell(X,Y,o)) :- index("X"),index("Y").
base(control(P)) :- role("P").

input(P,mark(X,Y)) :- index("X"),index("Y"),role("P").
input(P,noop) :- role("P").

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Initial State
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

init(cell(1,1,b)).
init(cell(1,2,b)).
init(cell(1,3,b)).
init(cell(2,1,b)).
init(cell(2,2,b)).
init(cell(2,3,b)).
init(cell(3,1,b)).
init(cell(3,2,b)).
init(cell(3,3,b)).
init(control(xplayer)).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Dynamic Components
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Cell

next(cell(M,N,x)) :- does(xplayer,mark("M","N")),true(cell("M","N",b)).

next(cell(M,N,o)) :- does(oplayer,mark("M","N")),true(cell("M","N",b)).

next(cell(M,N,W)) :- true(cell("M","N","W")),distinct("W",b).

next(cell(M,N,b)) :- does("W",mark("J","K")),true(cell("M","N",b)),or(distinct("M","J"),distinct("N","K")).

next(control(xplayer)) :- true(control(oplayer)).

next(control(oplayer)) :- true(control(xplayer)).


row(M,X) :- true(cell("M",'1',"X")),true(cell("M",'2',"X")),true(cell("M",'3',"X")).

column(N,X) :- true(cell('1',"N","X")),true(cell('2',"N","X")),true(cell('3',"N","X")).

diagonal(X) :- true(cell('1','1',"X")),true(cell('2','2',"X")),true(cell('3','3',"X")).

diagonal(X) :- true(cell('1','3',"X")),true(cell('2','2',"X")),true(cell('3','1',"X")).


line(X) :- row("M","X").
line(X) :- column("M","X").
line(X) :- diagonal("X").


open :- true(cell("M","N",b)).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

legal(W,mark(X,Y)) :- true(cell("X","Y",b)),true(control("W")).

legal(xplayer,noop) :- true(control(oplayer)).

legal(oplayer,noop) :- true(control(xplayer)).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

goal(xplayer,100) :- line(x).

goal(xplayer,50) :- not(line(x)),not(line(o)),not(open).

goal(xplayer,0) :- line(o).

goal(oplayer,100) :- line(o).

goal(oplayer,50) :- not(line(x)),not(line(o)),not(open).

goal(oplayer,0) :- line(x).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

terminal :- line(x).

terminal :- line(o).

terminal :- not(open).

Here’s my latest version:

#!/usr/bin/env swipl

:- use_module(library(http/http_open)).
:- use_module(library(dcg/basics)).

:- initialization(main, main).


main(Argv) :-
  last(Argv, Url),
  http_open(Url, Stream, []),
  phrase_from_stream(lines(_Lines), Stream).

lines([Line|Lines]) --> line(Line), !, lines(Lines).
lines([])           --> [].

line(Comment)    --> comment(Cs), { string_codes(Comment, Cs), format('~w~n', [Comment]) }, !.
line(Whitespace) --> whitespace(Cs), { string_codes(Whitespace, Cs), format('~w', [Whitespace]) }, !.
line(Sexp)      --> "(", s_expression(Sexp), ")", { format('~w.', [Sexp]) }.

s_expression(Sexp) --> blanks, term(Operator), blanks, terms(Args), { sexp(Operator, Args, Sexp) }.

sexp('<=', [Head|List], Sexp) :- !,
  comma_list(Body,List),
  Sexp =.. [':-',Head,Body].

sexp('not', Args, Sexp) :- !,
  Sexp =.. ['\\+'|Args].

sexp('or', Args, SemicolonList) :- !,
  semicolon_list(SemicolonList, Args).

sexp('distinct', Args, Sexp) :- !,
  Sexp =.. ['\\='|Args].

sexp(Operator, Args, Sexp) :-
  Sexp =.. [Operator|Args].

terms([Term|Terms]) --> blanks, term(Term), blanks, !, terms(Terms).
terms([])           --> [].

term(Sexp) --> "(", s_expression(Sexp), ")".
term(Variable) --> variable(Cs), { string_codes(Variable, Cs) }.
term(Symbol)   --> symbol(Cs), { atom_codes(Symbol, Cs) }.   

variable([U|Cs])  --> [63,L], { to_upper(L, U) }, symbol_rest(Cs).
symbol([C|Cs])    --> [C], { code_type(C, graph), string_codes("?();", Codes), \+ memberchk(C, Codes) }, symbol_rest(Cs).
symbol_rest([C|Cs]) --> [C], { code_type(C, graph), string_codes("();", Codes), \+ memberchk(C, Codes) }, symbol_rest(Cs).
symbol_rest([])     --> [].

comment([37|Cs])  --> ";", comment_rest(Cs).
comment_rest([37|Cs])  --> ";", comment_rest(Cs).
comment_rest([C|Cs])  --> [C], { \+ code_type(C, end_of_line) }, comment_rest(Cs).
comment_rest([])  --> [C], { code_type(C, end_of_line) }.

whitespace([C|Cs])    --> [C], { code_type(C, space) }, whitespace_rest(Cs).
whitespace_rest([C|Cs])    --> [C], { code_type(C, space) }, whitespace_rest(Cs).
whitespace_rest([])   --> [].

Running ./kif2prolog.pl http://games.ggp.org/base/games/ticTacToe/ticTacToe.kif > ticTacToe.pl

produces

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Tictactoe
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Roles
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

role(xplayer).
role(oplayer).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Base & Input
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

index(1). index(2). index(3).
base(cell(X,Y,b)):-index(X),index(Y).
base(cell(X,Y,x)):-index(X),index(Y).
base(cell(X,Y,o)):-index(X),index(Y).
base(control(P)):-role(P).

input(P,mark(X,Y)):-index(X),index(Y),role(P).
input(P,noop):-role(P).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Initial State
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

init(cell(1,1,b)).
init(cell(1,2,b)).
init(cell(1,3,b)).
init(cell(2,1,b)).
init(cell(2,2,b)).
init(cell(2,3,b)).
init(cell(3,1,b)).
init(cell(3,2,b)).
init(cell(3,3,b)).
init(control(xplayer)).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Dynamic Components
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Cell

next(cell(M,N,x)):-does(xplayer,mark(M,N)),true(cell(M,N,b)).

next(cell(M,N,o)):-does(oplayer,mark(M,N)),true(cell(M,N,b)).

next(cell(M,N,W)):-true(cell(M,N,W)),W\=b.

next(cell(M,N,b)):-does(W,mark(J,K)),true(cell(M,N,b)),(M\=J;N\=K).

next(control(xplayer)):-true(control(oplayer)).

next(control(oplayer)):-true(control(xplayer)).


row(M,X):-true(cell(M,1,X)),true(cell(M,2,X)),true(cell(M,3,X)).

column(N,X):-true(cell(1,N,X)),true(cell(2,N,X)),true(cell(3,N,X)).

diagonal(X):-true(cell(1,1,X)),true(cell(2,2,X)),true(cell(3,3,X)).

diagonal(X):-true(cell(1,3,X)),true(cell(2,2,X)),true(cell(3,1,X)).


line(X):-row(M,X).
line(X):-column(M,X).
line(X):-diagonal(X).


open:-true(cell(M,N,b)).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

legal(W,mark(X,Y)):-true(cell(X,Y,b)),true(control(W)).

legal(xplayer,noop):-true(control(oplayer)).

legal(oplayer,noop):-true(control(xplayer)).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

goal(xplayer,100):-line(x).

goal(xplayer,50):- \+line(x),\+line(o),\+open.

goal(xplayer,0):-line(o).

goal(oplayer,100):-line(o).

goal(oplayer,50):- \+line(x),\+line(o),\+open.

goal(oplayer,0):-line(x).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

terminal:-line(x).

terminal:-line(o).

terminal:- \+open.

Loading that into swipl produces

Warning: /home/roblaing/games/ticTacToe.pl:52:
Warning:    Singleton variables: [W]
Warning: /home/roblaing/games/ticTacToe.pl:68:
Warning:    Singleton variables: [M]
Warning: /home/roblaing/games/ticTacToe.pl:69:
Warning:    Singleton variables: [M]
Warning: /home/roblaing/games/ticTacToe.pl:73:
Warning:    Singleton variables: [M,N]
true.

The next step is to maybe experiment with term_singletons(+Term, -List)
to put underscores before variables in the body that don’t appear in the head and get listing(:What) to pretty print, but it’s already an improvement on my old python script.