Prolog 2018-09-29

By Max Woerner Chase

In this post:


When it comes to writing software, there are really hard problems, really intellectually distracting problems, problems with canonical solutions, problems that just take a bit of effort to get past, and stuff kind of in between all of that. The biggest problem I seem to hit, though, is the problem of avoiding scope creep. It's just so easy to look at a problem statement, and see the underlying generalities that it could be expressed in terms of. Why, with the right framework, the entire system could be expressed with just a handful of numbers! ... Is what I think right before everything goes off the rails and I'm left with a useless hunk of over-engineered nothing. But at long last, actual years of perseverance have paid off, and I've managed to avoid scope creep and built an engine that expresses the core logic and nothing more.

Some of you out there may have sensed a punchline coming.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
:- dynamic
    board/1
.

board(no).

player(x).
player(o).

turn(B,x) :-
    count(B,x,C)
    , count(B,o,C)
.
turn(B,o) :-
    count(B,x,Cx)
    , Co is Cx - 1
    , count(B,o,Co)
.

win(B,P) :-
    player(P)
    , row(B,(P,P,P))
.

subscript((Item,X,Y),0,Item,(X,Y)).
subscript((Y,Item,X),1,Item,(X,Y)).
subscript((X,Y,Item),2,Item,(X,Y)).

count((R1,R2,R3),P,C) :-
    tuple_count(R1, P, C1)
    , tuple_count(R2, P, C2)
    , tuple_count(R3, P, C3)
    , C is C1 + C2 + C3
    , !
.

print_winner(B) :-
    win(B, P)
    , write(P)
    , write(' wins!')
    ,!
.
print_winner(_).

print_row((X,Y,Z)) :-
    write(X)
    , write('|')
    , write(Y)
    , write('|')
    , write(Z)
    , nl
.
print_board((X,Y,Z)) :-
    print_row(X)
    , write('-+-+-')
    , nl
    , print_row(Y)
    , write('-+-+-')
    , nl
    , print_row(Z)
    , print_winner((X,Y,Z))
.

tuple_count((P,P,P), P, 3).
tuple_count((P,P,_), P, 2).
tuple_count((P,_,P), P, 2).
tuple_count((_,P,P), P, 2).
tuple_count((P,_,_), P, 1).
tuple_count((_,P,_), P, 1).
tuple_count((_,_,P), P, 1).
tuple_count(_,_,0).

row((Fst,_,_),Fst).
row((_,Snd,_),Snd).
row((_,_,Thd),Thd).
row(((X,_,_),(Y,_,_),(Z,_,_)),(X,Y,Z)).
row(((_,X,_),(_,Y,_),(_,Z,_)),(X,Y,Z)).
row(((_,_,X),(_,_,Y),(_,_,Z)),(X,Y,Z)).
row(((X,_,_),(_,Y,_),(_,_,Z)),(X,Y,Z)).
row(((_,_,X),(_,Y,_),(Z,_,_)),(X,Y,Z)).

can_move(B,(X,Y)) :-
    \+ win(B,_)
    , subscript(B,Y,R,_)
    , subscript(R,X,' ',_)
.
do_move(B1,(X,Y),B2) :-
    turn(B1,P)
    , subscript(B1,Y,R1,Rows)
    , subscript(R1,X,' ',Items)
    , subscript(R2,X,P,Items)
    , subscript(B2,Y,R2,Rows)
.

move(B1,(X,Y),'',B2) :-
    can_move(B1, (X,Y))
    , do_move(B1,(X,Y),B2)
    , !
.
move(B,_,'cannot move there',B).

start_game :-
    B = ((' ', ' ', ' '),(' ', ' ', ' '),(' ', ' ', ' '))
    , retract(board(_))
    , asserta(board(B))
    , print_board(B)
.

move(X,Y) :-
    board(B1)
    , move(B1,(X,Y),M,B2)
    , write(M)
    , nl
    , retract(board(_))
    , asserta(board(B2))
    , print_board(B2)
.

It's tic-tac-toe. The joke is, I'm proud of writing the core logic of tic-tac-toe, and managing to avoid adding stuff to the spec like extra dimensions or larger boards or extra players or nested boards or drafting rules or crafting mechanics and a sixty-hour story mode. (Things I haven't seen done are in italics.) (I also didn't write an AI, but the core logic is without side effects, so it should be totally doable.)

Point is, using Prolog, I've finally managed to prototype out the full core logic of a game, which is huge to me. For now, I want to focus on taking this logic more or less exactly, and using it with GUI code, so I can try to teach myself GUIs without things going off the rails again.

While I'm here, I should probably talk about my... interesting use of whitespace. Turns out Prolog doesn't seem to like trailing commas, so I decided to move the delimiters to the following line. This will come in handy when I'm diffing this code, or extending it. It gives things a very distinctive shape, which means that syntax errors should be pretty obvious.

And a little bit about the actual design. I put all of the state into a single data structure, so changes to it are as close to atomic as I know how. I want to keep up the setup of writing game rules as mappings between game states, try to compose different systems. Maybe sometime I can try to put these ideas into my own tutorial concept. For now, I have to prove to myself that they can work.

But yeah, this needs a GUI, because typing the move commands in the REPL is... not great.