Solving with TLA in Star Wars KOTOR 2
Posted on
TLDR
These are just my notes on an exercise in TLA+. I think it's a good excuse to play with sequence manipulation in TLA+ on an entirely impractical application. In this case, to solve a puzzle from a Star Wars game. Someday, I will write a story about a Jedi learning TLA+.
Wait, whats TLA+? Whats this for?
TLA+ is a modeling language. You can use it to write specifications. In TLA+, a specification is a set of formulas that could be true or false. Then you pass the specification to a model checker, which tries lots of things and tells you when and how your invariants or liveness properties fail.
It's been successfully applied to distributed systems and protocols to identify when they break. I recommend the TLA+ Video Course to learn more. Or Hillel Wayne's writing. Or Pron's writing.
Problem
In Star Wars: Knights of the Old Republic II you encounter a computer that must be repaired. After putting in a great deal of work, you encounter one final problem. There is one equation whose operators are missing: 6_2_9_8_1=13. Can you write a specification in TLA+ to identify the operators that satisfy it?
Approach 1: Predetermined Operators and Brute Force
This is essentially brute force.
- Choose a set of operators.
- Upon evaluation, does it equal 13? If so, you're done.
--------------------------------- MODULE dxun ----------------------------------
EXTENDS Integers, Sequences
VARIABLES pass, position, operator_decisions, operands, operand_position
vars == <<pass, position, operator_decisions, operands, operand_position>>
Operator == {"*", "/", "+", "-"}
PassTypeOK == pass \in 1..2
PositionTypeOK == position \in Nat
OperandPositionTypeOK == operand_position \in Nat
OperandTypeOK == \A i \in DOMAIN operands: operands[i] \in Int
OperatorDecisionsTypeOK == \A i \in DOMAIN operator_decisions:
operator_decisions[i] \in Operator
TypeOK == /\ PassTypeOK
/\ PositionTypeOK
/\ OperandPositionTypeOK
/\ OperandTypeOK
/\ OperatorDecisionsTypeOK
PrecedenceMap(operator) ==
CASE operator = "*" -> 1
[] operator = "/" -> 1
[] operator = "+" -> 2
[] operator = "-" -> 2
PerformOperation(operation, lhs, rhs) ==
CASE operation = "*" -> lhs * rhs
[] operation = "+" -> lhs + rhs
[] operation = "-" -> lhs - rhs
[] operation = "/" -> lhs \div rhs
Reduce(operation, seq, index) ==
LET i == index
j == index + 1
lhs == seq[i]
rhs == seq[j]
val == PerformOperation(operation, lhs, rhs)
IN
SubSeq(operands, 1, i - 1) \o <<val>>
\o SubSeq(operands, j+1, Len(operands))
Init == /\ pass = 1
/\ position = 1
/\ operands = <<6, 2, 8, 9, 1>>
/\ operator_decisions \in [1..Len(operands)-1 -> Operator]
/\ operand_position = 1
AdvanceOperatorPos ==
LET op == operator_decisions[position] IN
IF PrecedenceMap(op) > pass
THEN
/\ operand_position' = operand_position + 1
ELSE
UNCHANGED operand_position
Evaluate(index) ==
LET op == operator_decisions[position] IN
IF PrecedenceMap(op) = pass
THEN
/\ operands' = Reduce(op, operands, operand_position)
/\ UNCHANGED <<operand_position, operator_decisions>>
ELSE
/\ AdvanceOperatorPos
/\ UNCHANGED <<operator_decisions, operands>>
HandleFirstPass == /\ pass = 1
/\ position \in 1..(Len(operator_decisions))
/\ Evaluate(position)
/\ position' = position + 1
/\ UNCHANGED <<pass>>
TransitionToSecondPass == /\ pass = 1
/\ position = Len(operator_decisions)+1
/\ pass' = 2
/\ position' = 1
/\ operand_position' = 1
/\ UNCHANGED <<operator_decisions, operands>>
HandleSecondPass == /\ pass = 2
/\ position \in 1..(Len(operator_decisions))
/\ Evaluate(position)
/\ position' = position + 1
/\ UNCHANGED <<pass>>
Next == \/ HandleFirstPass
\/ TransitionToSecondPass
\/ HandleSecondPass
Spec == Init /\ [][Next]_vars /\ WF_vars(Next)
AlwaysCompletes == <>[](Len(operands) = 1)
NeverEquals13 == Len(operands) = 1 => operands[1] # 13
================================================================================
And, the accompanying configuration.
SPECIFICATION
Spec
INVARIANTS
NeverEquals13
TypeOK
CHECK_DEADLOCK
FALSE
PROPERTIES
AlwaysCompletes
Notes
AlwaysCompletes
verifies that all evaluations eventually end with exactly one value.
NeverEquals13
is an invariant that fails when the expression evaluates to 13, giving
us a satisfying set of operators.
Questions
- What happens when there is a division by zero?
- Why is
WF_vars
necessary to ensure theAlwaysCompletes
property? - What is the depth of the complete state graph seach?
- What is the outdegree of the complete state graph?
- Is there a simpler way to express the reduction operation? That is, do the arithmetic?