using System;
using System.Collections.Generic;
using STVrogue.GameLogic;
using STVrogue.Utils;
using static STVrogue.Utils.STVLogger ;
namespace STVrogue.TestInfrastructure
{
///
/// Representing three types of judgement of a temporal property.
///
public enum Judgement {
Valid, Inconclusive, Invalid
}
///
/// This is for PART-2.
///
/// A "temporal property" represents a correctness property that is judged over
/// an entire gameplay. An example of such a property is if we want to assert
/// that the player's hit point should never be negative. Another example is
/// if we want to assert that the player kill point should eventually be above
/// 0.
///
/// Note that "never be" and "eventually" are time-related modalities.
///
/// You can think a temporal property to be a predicate over
/// a sequence of states, which the states sampled during a gameplay. For
/// example a property "always p" (in other words, never (not p)) holds on
/// the sequence of p holds on every state in the sequence.
///
///
/// This class TemporalProperty is an abstract class intended to be the parent
/// of different types of temporal properties. It also defines a bunch of
/// factory-methods that allow you to construct/formulate a temporal properties with a
/// cleaner syntax.
/// Use them for constructing your temporal properties, rather than calling
/// their underlying constructors directly.
///
/// Several examples are shown below.
///
///
/// - The player's HP is never negative:
/// always(state => state.Player.Hp >= 0)
///
///
/// - The player's kill point never decreases:
/// always(state => state.Player.Kp, (before,now) => before <= now)
///
///
/// - Eventually the player's kill point will exceed 0:
/// eventually(state => state.Player.Kp > 0)
///
///
///
///
public abstract class TemporalProperty
{
///
/// Reset the evaluation-state of this temporal property.
///
public abstract void Reset();
///
/// Imagine that a gameplay is abstractly a sequence of states. At the start of
/// the play, this sequence is empty. When the play advances by one round/turn,
/// a new state is added to this sequence. Recall that a temporal property is
/// essentially a predicate over such a sequence of states. This method allows
/// the validity of the predicate to be checked incrementally by passing it
/// one state at a time. So, if the play advance by one turn, you would pass
/// the resulting game state to this predicate by calling this method EvaluateNextState,
/// and the method will return a judgement on whether the predicate, evaluated
/// on the sequence of states so far (including the new state you just passed) is
/// valid or invalid.
///
///
/// - Invalid : the so-far gameplay violates this temporal property.
///
/// - Valid: the property is satisfied by the gameplay so far (it holds on the
/// gameplay.
///
/// - Inconclusive : we cannot confirm whether the property is valid or
/// invalid on the gameplay so far.
///
///
///
abstract public Judgement EvaluateNextState(GameState state);
///
/// Create a copy of this temporal-property. The copy is a deep-clone, except for
/// the underlying state predicates.
///
public abstract TemporalProperty Copy();
///
/// this.And(psi) constructs a new temporal property representing a conjunction of
/// this temporal property and the given psi.
/// The conjunction xi of temporal properties phi and psi behaves as follows. Given
/// a gameplay s (a sequence of states s), evaluating xi on s results in:
///
/// - invalid, if either phi or psi evaluates to invalid on s
/// - valid if both phi and psi evaluate to valid on s.
/// - inconclusive neither (1) nor (2) above holds.
///
///
public TemporalProperty And(TemporalProperty psi)
{
return new And (this, psi);
}
///
/// A generalization of
/// that can take multiple conjuncts.
///
public static TemporalProperty And(params TemporalProperty[] phis)
{
var all = phis[0].Copy();
for (int k = 1; k < phis.Length; k++)
{
all = all.And(phis[k]);
}
return all;
}
///
/// this.Or(psi) constructs a new temporal property representing a disjunction of
/// this temporal property and the given psi. It is defined as not(And(not this, not psi)).
///
public TemporalProperty Or(TemporalProperty psi)
{
return Not(Not(this).And(Not(psi)));
}
///
/// A generalization of
/// that can take multiple disjuncts.
///
public static TemporalProperty Or(params TemporalProperty[] phis)
{
for (int k = 0; k < phis.Length; k++)
{
phis[k] = Not(phis[k]);
}
return Not(And(phis));
}
///
/// Construct a new temporal property representing the negation of
/// the given temporal property phi.
/// Given a gameplay s (sequence of states s), evaluating negation on s results in:
///
///
/// - invalid, if phi evaluates to valid on s.
/// - valid, if phi evaluates to invalid on s.
/// - inconclusive, if phi evaluates to inconclusive on s.
///
///
public static TemporalProperty Not(TemporalProperty phi)
{
return new Not(phi);
}
///
/// Construct a temporal property of the form always p. Such a property is valid on a gameplay
/// (sequence of states) s if p holds on every state in s. Else it is invalid.
///
public static TemporalProperty Always(Predicate p)
{
return new Always(p);
}
///
/// Construct an "always" type of property. Always(f,p) is valid on a gameplay (sequence of
/// states) s if for every state w in s, and its previous state v, p(f(v),f(w)) holds.
/// If the sequence contains less that two states, the property is inconclusive.
///
public static TemporalProperty Always(Func getValue, Func p)
{
return new Always(getValue, p);
}
///
/// Construct a temporal property of the form eventually p. Such a property is valid on a gameplay
/// (sequence of states) s if p holds on at least one state w in s.
///
public static TemporalProperty Eventually(Predicate p)
{
return Not(Always(st=> !p(st)));
}
///
/// Construct an "eventually" type of property. Eventually(f,p) is valid on a gameplay (sequence of
/// states) s if there is a state w in s, and its previous state v, such that p(f(v),f(w)) holds.
/// If the sequence contains less that two states, the property is inconclusive.
///
public static TemporalProperty Eventually(Func getValue, Func p)
{
return Not(Always(getValue, (prev,now)=> !p(prev,now)));
}
///
/// Run the given gameplays to check if they all satisfy this temporal property.
/// The method will stop at the first gameplay that violates this temporal property.
///
public bool SatisfiedByAll(params GamePlay[] testsuite)
{
foreach (var gameplay in testsuite)
{
this.Reset();
gameplay.ResetReplayState();
Judgement j = gameplay.Satisfies(this);
if (j == Judgement.Invalid)
{
STVLogger.Log($"## SatisfiedByAll violated by {gameplay.ReplayId()}");
return false;
}
}
return true;
}
///
/// Run the given gameplays to check if there is at least one that satisfies this temporal
/// property. The method stops at the first gameplay that satisfies the this
/// temporal property.
///
public bool SatisfiedBySome(params GamePlay[] testsuite)
{
foreach (var gameplay in testsuite)
{
this.Reset();
gameplay.ResetReplayState();
Judgement j = gameplay.Satisfies(this);
if (j == Judgement.Valid) return true;
}
return false;
}
///
/// This check if all gameplays in a certain group satisfy this temporal
/// property. The group is a subset of the given , consisting
/// of all gameplays in that satisfy the given .
/// If all gameplays in the group also satisfy this property, the method returns Valid.
/// If there is one gameplay in the group where this property is invalid, this method stops
/// and returns Invalid.
///
/// In other cases the method returns Inconclusive.
///
public Judgement SatisfiedByGroup(TemporalProperty condition, params GamePlay[] testsuite)
{
int numberOfWitness = 0;
foreach (var gameplay in testsuite)
{
condition.Reset();
gameplay.ResetReplayState();
Judgement cond = gameplay.Satisfies(condition);
if (cond == Judgement.Valid)
{
this.Reset();
gameplay.ResetReplayState();
Judgement j = gameplay.Satisfies(this);
if (j == Judgement.Invalid)
{
STVLogger.Log($"## SatisfiedByAll-with-condition violated by {gameplay.ReplayId()}");
return Judgement.Invalid ;
}
if (j == Judgement.Valid)
{
numberOfWitness++;
}
}
}
if (numberOfWitness > 0) return Judgement.Valid;
return Judgement.Inconclusive;
}
}
///
/// This is the underlying class representing a temporal property of the form "Always p".
/// A gameplay satisfies this property if the predicate p holds on every game state
/// through out the play.
///
public class Always : TemporalProperty
{
Predicate p;
Func getValue;
Func pWithPrev;
int? previousValue = null ;
public string desc = "";
Judgement judgementSoFar = Judgement.Inconclusive;
///
/// Construct a temporal property of the form Always(p) where p is a state predicate.
///
public Always(Predicate p) { this.p = p; }
///
/// Construct a temporal property of the form Always(f,p) where f is a function
/// that maps a state to an int and p is a predicate over integers. Given a sequence
/// of state s, the property is valid on s (satisfied by s) if for every state w
/// in s, and its previous state v, p(f(v),f(w)) holds.
///
/// Such a predicate p is called a "change predicate" as it expresses
/// how the current state is expected to change from the previous state.
///
///
///
/// Always(state => state.Player.Kp,
/// (previousValue,killPoint) => killPoint >= previousValue )
///
///
/// which states that it should always be the case that player's kill point
/// is greater or equal than its previous value. In other words, this property
/// says that the kill point of the player should never decrease.
///
///
public Always(Func getValue,Func p)
{
this.getValue = getValue;
pWithPrev = p;
}
public override void Reset()
{
judgementSoFar = Judgement.Inconclusive;
previousValue = null;
}
public override Judgement EvaluateNextState(GameState state)
{
if (judgementSoFar == Judgement.Invalid)
return Judgement.Invalid;
if (p != null)
{
judgementSoFar = p(state) ? Judgement.Valid : Judgement.Invalid;
}
else
{
int newValue = getValue(state);
if (previousValue != null)
{
//Console.WriteLine($">>> prev:{previousValue.Value}, now:{newValue}");
judgementSoFar = pWithPrev(previousValue.Value, newValue) ? Judgement.Valid : Judgement.Invalid;
}
previousValue = newValue;
}
if (judgementSoFar == Judgement.Invalid)
{
STVLogger.Log($"## An always property {desc} is violated. State = {state.ToString()}");
}
return judgementSoFar;
}
public override TemporalProperty Copy()
{
Always phi = new Always(this.p);
phi.getValue = this.getValue;
phi.pWithPrev = this.pWithPrev;
return phi;
}
}
///
/// This is the underlying class representing a temporal property of the form not phi,
/// where phi is another temporal property.
///
public class Not : TemporalProperty
{
private TemporalProperty phi;
public override void Reset()
{
phi.Reset();
}
Not() { }
public Not(TemporalProperty phi)
{
this.phi = phi.Copy();
}
public override Judgement EvaluateNextState(GameState state)
{
var j = phi.EvaluateNextState(state);
switch (j)
{
case Judgement.Valid : return Judgement.Invalid;
case Judgement.Invalid : return Judgement.Valid;
default: return Judgement.Inconclusive;
}
}
public override TemporalProperty Copy()
{
var psi = new Not();
psi.phi = this.phi.Copy();
return psi;
}
}
///
/// This is the underlying class representing a temporal property of the form
/// phi and psi, where phi and psi are temporal properties.
///
public class And : TemporalProperty
{
TemporalProperty phi1;
TemporalProperty phi2;
And() {}
public And(TemporalProperty phi, TemporalProperty psi)
{
phi1 = phi.Copy();
phi2 = psi.Copy();
}
public override void Reset()
{
phi1.Reset();
phi2.Reset();
}
public override Judgement EvaluateNextState(GameState state)
{
var j1 = phi1.EvaluateNextState(state);
var j2 = phi2.EvaluateNextState(state);
if (j1 == Judgement.Invalid || j2 == Judgement.Invalid)
return Judgement.Invalid;
if (j1 == Judgement.Valid && j2 == Judgement.Valid)
return Judgement.Valid;
return Judgement.Inconclusive;
}
public override TemporalProperty Copy()
{
var psi = new And();
psi.phi1 = this.phi1.Copy();
psi.phi2 = this.phi2.Copy();
return psi;
}
}
}