13.1 IntroductionIn the last few chapters we looked at propositional nets as an alternative to the Game Description Language for encoding games. In this and the following chapters we return to GDL as the typical language in which the rules of games are communicated in general game playing. We first look at the task of computing logical consequences from a logic program. This provides the background for implementing the basic functionality of a general game player to generate legal moves, compute state update and decide termination from a GDL game description (see Chapter 4). The ability to draw inferences from a logic program is also needed for converting a GDL input into a propnet or any other data structure that can be more efficient to compute with. We will show how logic can be used to find structure in GDL games such as symmetries, which help improve the performance of a general gameplaying system. We will look into how singleplayer games can be solved in logic and how logic can be used to aid decision making through the automatic construction of a goalpriented evaluation function. The basic type of inference we are concerned with can be formulated as queries that ask whether a literal L, or a conjunction of literals L_{1} & L_{2} & … & L_{n}, follows from a set of clauses. Often a query contains variables, and then we are interested in obtaining values for these variables under which the query becomes true. Let's suppose that we want to infer a legal move for a player, say white, in a particular state of a game. We can formulate this as the query legal(white,L), which means to determine for which L, if any, this is a logical consequence of the given rules. Let's use as an example the GDL description of a 2player game known as Nim. Players take turns removing one or more objects from one of several heaps. The game rules 110 below define a legal move as reducing the size of a selected heap to a smaller value. The facts in lines 1215 encode a randomly chosen current state.
13.2 UnificationThe most basic step in computing a query to a logic program is called unification. It means the process of replacing variables so that two logical expressions become similar. This is needed to determine which rule from the program could provide an answer to an atomic query such as legal(white,L). Generally speaking, a program fact A, or a program rule A : B_{1} & … & B_{m}, can only provide an answer to a query atom L if it is possible to replace the variables occurring in L and A in such a way that the two atoms become similar. Definition 13.1 A substitution is a finite set of replacements {x_{1}/t_{1}, …, x_{n}/t_{n}} such that
The result SUBST(E,σ) of applying a substitution σ = {x_{1}/t_{1}, …, x_{n}/t_{n}} to an expression E is obtained by simultaneously replacing in E every x_{i} by its replacement t_{i}. Definition 13.2 Two expressions E_{1} and E_{2} are unifiable if we can find a substitution σ such that SUBST(E_{1},σ) = SUBST(E_{2},σ). Any such σ is called a unifier of E_{1} and E_{2}. Recalling the example from above, the atomic query legal(white,L) and the head of the first rule in Figure 13.1, legal(P,reduce(X,N)), are unifiable. Indeed there are many different unifiers for the two, including all of the following.
Comparing the three substitutions, the last one, θ, appears to be the most general way of unifying our two atoms: While variable L is assigned a move of the form reduce(X,N) in all three cases, unifier σ_{1} additionally fixes specific values for X,N whereas σ_{2} binds the two variables together. But neither is necessary to unify legal(white,L) and legal(P,reduce(X,N)). This leads to Definition 13.3 below, which refers to the composition of two substitutions θ_{1} ⚬ θ_{2} as a substitution that, for any expression E, satisfies Definition 13.3 A unifier θ is more general than a unifier σ if there is a third substitution τ such that θ ⚬ τ = σ. A most general unifier for expressions E_{1} and E_{2} is one that is more general than any other unifier of the two expressions. It follows that indeed our unifier θ from above is more general than both σ_{1} and σ_{2}, as can be seen from
In fact, θ is a most general unifier for our example. Fortunately it is a known fact that whenever two atomic formulas are unifiable, then a most general unifier always exists and is unique up to variable permutation. Moreover, most general unifiers can be easily computed. The recursive algorithm below takes as input two expressions and a partially computed unifier (which should be empty at the first function call). It is assumed that expressions as well as partial unifiers are encoded as lists. For example, the function call
returns the following most general unifier.
The algorithm works by comparing the structure of the two expressions, argument by argument. Function replacement_for(x,sigma) is assumed to return the replacement for variable x according to unifier sigma if such a replacement exists; and 0 otherwise. Function occurs_in(x,y) should return true just in case variable x occurs anywhere inside the (possibly nested) list y. 13.3 Derivation Steps (Without Negation)Backwardchaining is the standard technique for query answering in clausal logic. For this we work our way backwards from the query, first by finding a rule that applies to the leading query element and then proving that the conditions of the rule hold. A clausee is applicable to an atom that can be unified with the head of the rule. There is a small subtlety though. Suppose we had formulated the search for legal moves of our player as legal(white,X) instead of legal(white,L). We would then be unable to unify the query atom with the head of the clause shown below.
The reason is that no substitution σ can possibly exist such that SUBST(legal(white,X), σ) = SUBST(legal(P,reduce(X,N)), σ) because X cannot be equated with reduce(X,N). But the scope of a variable should never extend beyond the clause in which it appears. For this reason, each application of a rule should be preceded by generating a "fresh" copy in which the variables have been consistently replaced by new names that do not occur in the original query or any other step in the same derivation. For a single derivation step we then proceed as follows. Let's first consider the case that neither the query nor any of the program clauses contains a negated literal.
A derivation step thus replaces the leading element of the query by the body of a suitable clause and applies the necessary variable bindings to all of the new query. Here is an example that uses the first clause of Figure 13.1.
When the applied clause is a fact, i.e., a rule with empty body, then the respective query element is removed without replacement, as in the following derivation step.
13.4 DerivationsA complete derivation for a query is obtained by the repeated application of derivation steps until all subgoals have been resolved. If the original query includes variables, then the computed answer is determined by the variable bindings made along the way. Definition 13.4 A series of derivation steps is called a derivation. A successful derivation is one that ends with the empty query, denoted as "□". The answer computed by a successful derivation is obtained by composing the unifiers σ_{1} ⚬ σ_{2} ⚬ … ⚬ σ_{k} of each step and restricting the result to the variables in the original query. An example is shown below, where again we use the clauses of Figure 13.1.
Here is another successful derivation for the same query.
Derivations aren't always successful. Definition 13.5 A derivation fails if it leads to a query whose first element does not unify with the head of any available clause. Here is an example of a derivation for the same query as above that leads to a deadend.
13.5 Derivation Tree SearchSome of the basic functions from Chapter 4 for a general game player require to consider all possible derivations of a query. For instance, findlegals(role,state,description) is expected to deliver every computable answer to legal(role,M) for a given state and game description. Backtracking provides a systematic way to compute these: After having found the first successful or failed derivation, one goes back to the most recent choice where a different clause could have been applied to the query, until all choices have been exhausted. We can draw a tree with the original query as the root to illustrate the search space of backwardchaining. Each computed answer then corresponds to one branch that ends with the empty query. Switching to a different example, consider the problem of computing all legal moves for a player in a given TicTacToe position. Let's use the clauses listed below in Figure 13.2. Specifically, the facts in lines 110 together encode the following game state, with white to move.
The tree depicted in Figure 13.3 shows the 3 legal moves that can be computed for white in our example TicTacToe position. The labels at the arcs indicate which clause has been selected. Backtracking can be implemented by a depthfirst search through this tree. For black there is just one legal move in the same position, as the computation tree below illustrates. A correct implementation of a complete search through a derivation tree requires you to address the problem of potentially infinite derivations that may arise with some game descriptions. Even if a GDL describes a finite game, there is always the risk that a computation loops because of a recursive clause. Suppose, for example, the following rule were added to the game description of Nim in Figure 13.1.
This clause is obviously redundant but perfectly correct, both logically and syntactically. A straightforward implementation to compute derivations with this rule will eventually enter a nonterminating loop.
Although it may be unlikely that you encounter such a tricky game description in practice, it can be a good idea to add a simple loop check to avoid nonterminating computations. Note that potential loops may be less obvious to detect from the GDL itself than in our example if they are spread over several clauses. Recursive clauses may even lead to nonterminating derivations in which the query grows forever without repetition. To see why, consider the following alternative formalization of clause 5 from Figure 13.1.
Logically, this means the very same thing, but it may give rise to a nonlooping, infinite derivation as follows.
This problem can be avoided simply by reordering the atoms in a rule in such a way that recursive calls always come last. 13.6 Handling NegationLet's now generalize our derivation procedure to queries and programs that include negative literals. A subgoal of the form ~A can be treated according to a principle known as negationbyfailure: If A itself cannot be derived, then ~A is true. This is justified by our use of the minimal model as the semantics of a set of GDL clauses. For a single derivation step in the general case we thus proceed as follows.
Recall, for example, one of the termination condition in TicTacToe.
As long as there is at least one blank cell in the current state, the subgoal true(cell(M,N,b)) has a successful derivation, and hence so has the atom open. Consequently, the negative subgoal ~open fails unless all cells have been marked. The query terminal therefore fails too, provided that no other terminating conditions hold that allow it to be derived. But when a state is reached in which all cells have been marked, then true(cell(M,N,b)) can no longer be derived, and hence open fails. Accordingly, ~open now succeeds, and so does the query terminal. The special, gameindependent predicate distinct is computed very much like a negative subgoal.
It is crucial that only negative subgoals without variables can be subjected to the negationbyfailure computation. The same holds for the distinct relation. In some cases it may be necessary to reorder the subgoals to ensure that all variables have been replaced by nonvariable terms before a negative literal gets selected. The GDL description of Multiple Buttons And Light (see Chapter 11) is a point in case. Consider the following encoding of a given position and move in that game along with one of the update rules.
Naturally we expect to be able to derive two answers to the query next(p(Z)), namely {Z/1} and {Z/3}, since the first and third light are not affected by the action. So let's consider the situation after the first step in a derivation.
But now if we were to ignore the restriction about negative literals with variables and chose the subgoals in the given order, then the query would fail. For the positive part of the negative literal, does(white,a(Z)), obviously has a successful derivation:
If, however, we reorder the subgoals so as to ensure that before we select a negated subgoal, all its variables have been substituted, then the two answers are obtained as expected.
Fortunately, the syntactic restrictions on valid GDLs guarantee that subgoals can always be reordered in this manner. Specifically, every variable in a rule must occur in a positive literal in the body, not counting the special gameindependent predicate distinct. Hence you can always compute values for all variables in a clause before selecting a negative subgoal or an instance of distinct. The negationbyfailure principle is recursively applied in case of nested negation, that is, when negative subgoals arise during an attempted derivation for another negated literal. An illustrative example is given by the following excerpt from a fictitious game description.
Only player blue can be shown to have achieved the goal in the current state, as per the nested derivation below.
The attempt to derive goal(red,100) fails.
The same is true for goal(green,100). Exercises
