Updated boolean expressions to really, actually, seriously work this time.

This commit is contained in:
Krazy
2014-09-16 00:49:44 +00:00
parent 1c9975bf2a
commit 37c1cc9c54
5 changed files with 244 additions and 235 deletions

2
.gitattributes vendored
View File

@@ -16711,6 +16711,7 @@ forge-gui/src/main/java/forge/interfaces/IGuiBase.java -text
forge-gui/src/main/java/forge/interfaces/IProgressBar.java -text
forge-gui/src/main/java/forge/interfaces/ITextField.java -text
forge-gui/src/main/java/forge/interfaces/IWinLoseView.java -text
forge-gui/src/main/java/forge/itemmanager/BooleanExpression.java -text
forge-gui/src/main/java/forge/itemmanager/ColumnDef.java -text
forge-gui/src/main/java/forge/itemmanager/GroupDef.java -text
forge-gui/src/main/java/forge/itemmanager/IItemManager.java -text
@@ -16721,6 +16722,7 @@ forge-gui/src/main/java/forge/itemmanager/ItemManagerModel.java -text
forge-gui/src/main/java/forge/itemmanager/SColumnUtil.java -text
forge-gui/src/main/java/forge/itemmanager/SFilterUtil.java -text
forge-gui/src/main/java/forge/itemmanager/SItemManagerUtil.java -text
forge-gui/src/main/java/forge/itemmanager/StringTokenizer.java -text
forge-gui/src/main/java/forge/itemmanager/package-info.java -text
forge-gui/src/main/java/forge/limited/BoosterDeckBuilder.java -text
forge-gui/src/main/java/forge/limited/BoosterDraft.java svneol=native#text/plain

View File

@@ -23,6 +23,36 @@ Now, when you place first or second, you will get to pick a rare in the drafted
Additionally, when you spend draft tokens, you no longer receive a random draft. Instead, you get to pick which block the new draft will be (but you still have to pay for it!), completely eliminating the game of chance.
- Boolean Expressions Finished -
Yes, again! They say the third time's the charm, and, well, it is! Boolean expressions have undergone a complete rewrite again with a slightly different method of parsing that works much better.
Parenthesis work correctly, order is preserved, and NOT operators are applied in the correct places (and also in the correct order). This means that what you expect is what you really get!
There's one big change to note: The operators have been shortened to one character. They are now: & | ! \
The only conflicts the AND operator has is a few Unhinged/Unglued cards (which aren't even in Forge), but you can use the escape operator (\) to tell it you really meant to use the & (and that's assuming these cards ever get added to Forge).
There's a couple cards with exclamation points in their names, but again, you can escape that character to search for them.
A few examples of possible expressions:
> Search by type: All warriors or cats that aren't zombies, vampires, orcs, or skeletons.
(warrior | cat) & !(zombie | vampire | orc | skeleton)
> Search by mana cost: All non-red cards.
!r
> Search by mana cost: All non-red, non-black green cards that are also either blue or white, but not only blue or white.
!(r|b) & (g & (u | w))
> Search by mana cost: All red or black cards that are not also green and blue or green and white.
(r|b) & !(g & (u | w))
> Search by mana cost: All cards that cost 4 colorless mana.
!(w | r | u | g | b) & 4
> Search by text: All cards that have both Lifelink and Vigilance printed in the rules text.
lifelink & vigilance
These examples should get you started with this powerful-now-that-it's-finished feature. Enjoy!
-------------------------------
New Commander 2014 branch Cards
-------------------------------

View File

@@ -0,0 +1,170 @@
package forge.itemmanager;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import forge.card.CardRules;
import forge.card.CardRulesPredicates;
import forge.util.PredicateString.StringOp;
import java.util.*;
public class BooleanExpression {
private Stack<Operator> operators = new Stack<>();
private Stack<Predicate<CardRules>> operands = new Stack<>();
private StringTokenizer expression;
private boolean inName, inType, inText, inCost;
private static enum Operator {
AND("&", 0), OR("|", 0), NOT("!", 1), OPEN_PAREN("(", 2), CLOSE_PAREN(")", 2), ESCAPE("\\", -1);
private final String token;
private final int precedence;
private Operator(final String token, final int precedence) {
this.token = token;
this.precedence = precedence;
}
}
public BooleanExpression(final String expression, final boolean inName, final boolean inType, final boolean inText, final boolean inCost) {
this.expression = new StringTokenizer(expression);
this.inName = inName;
this.inType = inType;
this.inText = inText;
this.inCost = inCost;
}
public Predicate<CardRules> evaluate() {
String currentValue = "";
boolean escapeNext = false;
while (expression.hasNext()) {
String token = expression.next();
Operator operator = null;
if (token.equals(Operator.AND.token)) {
operator = Operator.AND;
} else if (token.equals(Operator.OR.token)) {
operator = Operator.OR;
} else if (token.equals(Operator.OPEN_PAREN.token)) {
operator = Operator.OPEN_PAREN;
} else if (token.equals(Operator.CLOSE_PAREN.token)) {
operator = Operator.CLOSE_PAREN;
} else if (token.equals(Operator.NOT.token)) {
operator = Operator.NOT;
} else if (token.equals(Operator.ESCAPE.token)) {
escapeNext = true;
continue;
}
if (operator == null) {
currentValue += token;
} else {
if (escapeNext) {
escapeNext = false;
currentValue += token;
continue;
}
if (!currentValue.trim().isEmpty()) {
operands.push(valueOf(currentValue.trim()));
}
currentValue = "";
if (!operators.isEmpty() && operator.precedence < operators.peek().precedence) {
resolve(true);
} else if (!operators.isEmpty() && operator == Operator.CLOSE_PAREN) {
while (!operators.isEmpty() && operators.peek() != Operator.OPEN_PAREN) {
resolve(true);
}
}
operators.push(operator);
}
}
if (!currentValue.trim().isEmpty()) {
operands.push(valueOf(currentValue.trim()));
}
while (operators.size() > 0) {
resolve(true);
}
return operands.get(0);
}
private void resolve(final boolean alwaysPopOperator) {
Predicate<CardRules> right;
Predicate<CardRules> left;
switch (operators.peek()) {
case AND:
operators.pop();
right = operands.pop();
left = operands.pop();
operands.push(Predicates.and(left, right));
break;
case OR:
operators.pop();
right = operands.pop();
left = operands.pop();
operands.push(Predicates.or(left, right));
break;
case NOT:
operators.pop();
left = operands.pop();
operands.push(Predicates.not(left));
break;
default:
if (alwaysPopOperator) {
operators.pop();
}
break;
}
}
private Predicate<CardRules> valueOf(final String value) {
List<Predicate<CardRules>> predicates = new ArrayList<>();
if (inName) {
predicates.add(CardRulesPredicates.name(StringOp.CONTAINS_IC, value));
}
if (inType) {
predicates.add(CardRulesPredicates.joinedType(StringOp.CONTAINS_IC, value));
}
if (inText) {
predicates.add(CardRulesPredicates.rules(StringOp.CONTAINS_IC, value));
}
if (inCost) {
predicates.add(CardRulesPredicates.cost(StringOp.CONTAINS_IC, value));
}
if (predicates.size() > 0) {
return Predicates.or(predicates);
}
return Predicates.alwaysTrue();
}
public static boolean isExpression(final String string) {
return string.contains(Operator.AND.token) || string.contains(Operator.OR.token) || string.trim().startsWith(Operator.NOT.token);
}
}

View File

@@ -15,7 +15,10 @@ import forge.itemmanager.SItemManagerUtil.StatTypes;
import forge.util.BinaryUtil;
import forge.util.PredicateString.StringOp;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Static factory; holds blocks of form elements and predicates
@@ -38,10 +41,15 @@ public class SFilterUtil {
if (BooleanExpression.isExpression(text)) {
BooleanExpression expression = new BooleanExpression(text, inName, inType, inText, inCost);
Predicate<CardRules> filter = expression.evaluate();
if (filter != null) {
return Predicates.compose(invert ? Predicates.not(filter) : filter, PaperCard.FN_GET_RULES);
try {
Predicate<CardRules> filter = expression.evaluate();
if (filter != null) {
return Predicates.compose(invert ? Predicates.not(filter) : filter, PaperCard.FN_GET_RULES);
}
} catch (Exception ignored) {
ignored.printStackTrace();
//Continue with standard filtering if the expression is not valid.
}
}
@@ -64,236 +72,6 @@ public class SFilterUtil {
return Predicates.compose(textFilter, PaperCard.FN_GET_RULES);
}
private static class Tokenizer implements Iterator<String> {
private String string;
private int index = 0;
private Tokenizer(String string) {
this.string = string;
}
@Override
public boolean hasNext() {
return index < string.length();
}
@Override
public String next() {
return string.charAt(index++) + "";
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
public String lookAhead() {
if (hasNext()) {
return string.charAt(index) + "";
}
else {
return "";
}
}
}
private static class BooleanExpression {
private static enum Operation {
AND("&&"), OR("||"), NOT("!"), OPEN_PAREN("("), CLOSE_PAREN(")"), ESCAPE("\\");
private final String token;
private Operation(String token) {
this.token = token;
}
}
private String text;
private Stack<String> stack = new Stack<>();
private String currentValue = "";
private boolean inName = false;
private boolean inType = false;
private boolean inText = false;
private boolean inCost = false;
private BooleanExpression(String text, boolean inName, boolean inType, boolean inText, boolean inCost) {
this.text = text;
this.inName = inName;
this.inType = inType;
this.inText = inText;
this.inCost = inCost;
parse();
}
private static boolean isExpression(String text) {
return text.contains(Operation.AND.token) || text.contains(Operation.OR.token);
}
private void parse() {
Tokenizer tokenizer = new Tokenizer(text);
String currentChar;
boolean escapeNext = false;
while (tokenizer.hasNext()) {
currentChar = tokenizer.next();
if (escapeNext) {
currentValue += currentChar;
escapeNext = false;
}
else if (currentChar.equals(Operation.ESCAPE.token)) {
escapeNext = true;
}
else {
if ((currentChar + tokenizer.lookAhead()).equals(Operation.AND.token)) {
tokenizer.next();
pushTokenToStack(Operation.AND.token);
}
else if ((currentChar + tokenizer.lookAhead()).equals(Operation.OR.token)) {
tokenizer.next();
pushTokenToStack(Operation.OR.token);
}
else if (currentChar.equals(Operation.OPEN_PAREN.token)) {
pushTokenToStack(Operation.OPEN_PAREN.token);
}
else if (currentChar.equals(Operation.CLOSE_PAREN.token)) {
pushTokenToStack(Operation.CLOSE_PAREN.token);
}
else if (currentChar.equals(Operation.NOT.token)) {
pushTokenToStack(Operation.NOT.token);
}
else {
currentValue += currentChar;
}
}
}
if (!currentValue.trim().isEmpty()) {
stack.push(currentValue.trim());
}
}
private void pushTokenToStack(String token) {
currentValue = currentValue.trim();
if (!currentValue.isEmpty()) {
stack.push(currentValue);
currentValue = "";
}
stack.push(token);
}
private Predicate<CardRules> evaluate() {
//Collections.reverse(stack); //Reverse the stack so we're popping off the start
Predicate<CardRules> rules = null;
Stack<String> evaluationStack = new Stack<>();
while (stack.size() > 0) {
String stackItem = stack.pop();
if (stackItem.equals(Operation.CLOSE_PAREN.token)) {
rules = evaluateUntilToken(evaluationStack, rules, Operation.OPEN_PAREN.token);
}
else {
evaluationStack.push(stackItem);
}
}
return evaluateUntilToken(evaluationStack, rules, "");
}
private Predicate<CardRules> evaluateUntilToken(Stack<String> evaluationStack, Predicate<CardRules> rules, String token) {
Predicate<CardRules> outputRules = rules;
Operation currentOperation = null;
while (!evaluationStack.isEmpty()) {
String stackItem = evaluationStack.pop();
if (!token.isEmpty() && token.equals(stackItem)) {
break;
}
if (isOperation(stackItem)) {
if (stackItem.equals(Operation.AND.token)) {
currentOperation = Operation.AND;
}
else if (stackItem.equals(Operation.OR.token)) {
currentOperation = Operation.OR;
}
else if (stackItem.equals(Operation.NOT.token)) {
if (outputRules == null) {
return null;
}
outputRules = Predicates.not(outputRules);
}
}
else {
if (currentOperation == null) {
if (outputRules == null) {
outputRules = evaluateValue(stackItem);
}
}
else {
if (outputRules == null) {
return null;
}
switch (currentOperation) {
case AND:
outputRules = Predicates.and(outputRules, evaluateValue(stackItem));
break;
case OR:
outputRules = Predicates.or(outputRules, evaluateValue(stackItem));
break;
default:
break;
}
currentOperation = null;
}
}
}
return outputRules;
}
private Predicate<CardRules> evaluateValue(String value) {
List<Predicate<CardRules>> predicates = new ArrayList<>();
if (inName) {
predicates.add(CardRulesPredicates.name(StringOp.CONTAINS_IC, value));
}
if (inType) {
predicates.add(CardRulesPredicates.joinedType(StringOp.CONTAINS_IC, value));
}
if (inText) {
predicates.add(CardRulesPredicates.rules(StringOp.CONTAINS_IC, value));
}
if (inCost) {
predicates.add(CardRulesPredicates.cost(StringOp.CONTAINS_IC, value));
}
if (predicates.size() > 0) {
return Predicates.or(predicates);
}
return Predicates.alwaysTrue();
}
private boolean isOperation(String token) {
for (Operation o : Operation.values()) {
if (token.equals(o.token)) {
return true;
}
}
return false;
}
}
public static <T extends InventoryItem> Predicate<T> buildItemTextFilter(String text) {
if (text.trim().isEmpty()) {
return Predicates.alwaysTrue();

View File

@@ -0,0 +1,29 @@
package forge.itemmanager;
import java.util.Iterator;
public class StringTokenizer implements Iterator<String> {
private String string;
private int index = 0;
public StringTokenizer(String string) {
this.string = string;
}
@Override
public boolean hasNext() {
return index < string.length();
}
@Override
public String next() {
return string.charAt(index++) + "";
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
}