// ArgumentChoice.java // // handles user-selected argument tree, judges validity of dialogue moves // ---------------------------------------------------------------------- // part of ArgumentGame (simulation of a rational argument) // game written for/designed by Prof. Ron Loui, loui@ai.cs.wustl.edu // based on the paper "An Argument Game" by Ronald Loui and Wiliam Chen // based on the game "LMNOP" // (see Loui-Norman-Stiefvater-Merrill-Olson-Costello [92]) // code by Nina Kang, nkang@husc.harvard.edu, 8/96 import java.awt.*; import java.util.*; import java.util.Vector; public class ArgumentChoice extends Panel { ArgumentGame game; CaseNode Vroot; // Vladimir is the DECLARER and has the burden of proof CaseNode Eroot; // Estragon is the DEFENDER and has no burden of proof. // graphics-oriented globals ArgStack theStack; // keeps track of argument as it is being drawn String errorMsg; // messages from thrown exceptions FlexMsg judgeMsg; // the verdict of the judgment module // pixel manipulation info static int xinset; static int yinset; int depth; int margin; // judgment state static final int NONCOMPARABLE = 0; static final int MORE_SPECIFIC = 1; static final int LESS_SPECIFIC = 2; ArgumentChoice (ArgumentGame g) { game = g; theStack = new ArgStack(); } // ArgumentGame section 2.5 on argument trees: // // A _tree_of_potential_argument_ for a card (henceforth, an _argument_) // is a collection of cases that can be organized into a tree by: // * Taking the card to be root, // * Taking the unique case for the card to define the children of the root // by using any (including possibly all) of its admissible facts // as children of the root; // * Checking that no card and its opposite both appear; // * Checking that the leaves of this tree are each admissible. // constructs the tree of argument public CaseNode makeRoot (char[][] argument, String player) { CaseNode root = new CaseNode(); int[] hist; char ch; int x; boolean firstcase; // first case listed must be the claim CaseNode curr_node; // Read through each line of the argument for (int i = 0; i < argument.length; i++) { curr_node = root; firstcase = true; hist = new int[13]; for (int j = 0; j < hist.length; j++) hist[j] = -1; // Parse the characters in each line for (int j = 0; j < argument[i].length; j++) { ch = argument[i][j]; x = -1; if (ch == '/') { // following text is comment j = argument[i].length; // ignore rest of line // valid characters found } else { if ((ch >= 'A') && (ch <= 'W')) x = ch-'A'; if ((ch >= 'a') && (ch <= 'w')) x = ch-'a'; if (x >= 0) { if (firstcase) { checkContradicts (x-3, player); checkNoDuplication (x-3, root); firstcase = false; } curr_node = curr_node.addChild ((char)x); checkNode (curr_node, hist); } } } hist = null; } // check for blank tree if (root.children == null) { throw new IllegalArgumentException ("Tree is empty"); } // make sure that one of the arguments is the claim if (player.equals (game.contract.Vladimir())) { Vroot = moveClaimToFront (root, player); return (Vroot); } else { Eroot = moveClaimToFront (root, player); return (Eroot); } } // makes sure there is no contradiction within this argument void checkNode (CaseNode curr_node, int[] hist) { int n = hist[CardDeck.getNumber(curr_node.myCase.decision)]; if ((n != -1) && (n != CardDeck.getColor(curr_node.myCase.decision))) throw new IllegalArgumentException ("Card's negation used" + " elsewhere in argument"); else hist[CardDeck.getNumber (curr_node.myCase.decision)] = CardDeck.getColor(curr_node.myCase.decision); } // must prove that the case in question is a counterargument public void checkContradicts (int i, String player) { CaseNode argument; if (player.equals (game.contract.Vladimir())) { if (CardDeck.equivalent(Case.cases[i].decision, CardDeck.getOpposite (game.contract.establish))) return; argument = Eroot; } else { if (CardDeck.equivalent (Case.cases[i].decision, game.contract.establish)) return; argument = Vroot; } if (argument == null) return; if (!argument.searchTree(CardDeck.getOpposite(Case.cases[i].decision))) throw new IllegalArgumentException ("Card "+CardDeck.textCard(Case.cases[i].decision)+ " does not contradict\nopponent's argument."); } // must prove that the case in question is not a duplicate argument. public void checkNoDuplication (int i, CaseNode root) { if (root.children == null) return; int decision = Case.cases[i].decision; for (int j = 0; j < root.children.length; j++) { if ((root.children[j].myCase.index != i) && (CardDeck.equivalent (root.children[j].myCase.decision, decision))) throw new IllegalArgumentException ("Cannot use two arguments for the same case."); } } // make sure the claim or !claim is in the argument, and in position [0] public CaseNode moveClaimToFront (CaseNode root, String player) { CaseNode claimNode = null; int claim; if (player.equals (game.contract.Vladimir())) claim = game.contract.establish; else claim = CardDeck.getOpposite (game.contract.establish); boolean stop = false; int i; for (i = 0; (i < root.children.length) && (!stop); i++) { if (CardDeck.equivalent (root.children[i].myCase.decision, claim)) { claimNode = root.children[i]; stop = true; } } if (claimNode == null) throw new IllegalArgumentException ("Claim must be present in argument"); CaseNode[] tmp = new CaseNode[root.children.length]; System.arraycopy (root.children, 0, tmp, 1, i-1); tmp[0] = claimNode; root.children = null; root.children = tmp; return (root); } // --------------------- Arbitrating functions ------------------------- // decide whether player in question has made a "sufficient response" // section 2.5, challenging // A challengeable card is any leaf in an opponent's held argument // that is not evidence. boolean challengeIsValid (int card, int lastMove) { // node cannot be evidence if (Support.NewSupport(card).evidence) return false; // node must be in argument tree if (lastMove == game.flow.brain.E_CHALLENGE) { return (Vroot.searchTree (card)); } else if (lastMove == game.flow.brain.V_CHALLENGE) { return (Eroot.searchTree (card)); } else throw new IllegalArgumentException(); } // Gauging the sufficiency of Vladimir's and Estragon's arguments: // * the opposite of the argument cannot be cited to have potential // argument for the opposing player, i.e. // * the argument must have appropriate strength: // (in weighing Vladimir's case) Vladimir's argument must be // MORE specific than Estragon's. // (in weighing Estragon's case) Estragon's argument cannot be // less specific than Vladimir's. boolean sufficientDeclarerResponse () { // Vladimir must be MORE specific if (Vroot == null) throw new IllegalArgumentException ("Uninitialized argument tree for player "+game.contract.Vladimir()); judgeMsg = new FlexMsg(); if (Eroot == null) { judgeMsg.add (game.contract.Estragon()+" has no argument."); judgeMsg.add (game.contract.Vladimir()+" wins by default."); return true; } Vroot.unDefeat(); Eroot.unDefeat(); if (Vroot.children.length > 1) { judgeMsg.add ("Examining "+game.contract.Estragon()+"'s subsidiary arguments:"); compareSecondaryArguments (Vroot, Eroot, MORE_SPECIFIC); } if (Eroot.children.length > 1) { judgeMsg.add ("Examining "+game.contract.Vladimir()+"'s subsidiary arguments:"); compareSecondaryArguments (Eroot, Vroot, NONCOMPARABLE); } int verdict; boolean answer; verdict = checkSpecificity (Vroot.children[0], Eroot.children[0]); if (verdict == MORE_SPECIFIC) { answer = true; judgeMsg.add (game.contract.Vladimir()+"'s argument is more specific."); } else { answer = Vroot.live(); judgeMsg.add (game.contract.Vladimir()+"'s argument is not more specific."); if (answer) judgeMsg.add ("But its decisions have not all been defeated."); else judgeMsg.add ("And its decisions have all been defeated."); } return (answer); } boolean sufficientDefenderResponse () { // Estragon must be at least if (Eroot == null) throw new IllegalArgumentException ("Uninitialized argument tree for player "+game.contract.Estragon()); if (Vroot == null) throw new IllegalArgumentException ("Uninitialized argument tree for player "+game.contract.Vladimir()); Vroot.unDefeat(); Eroot.unDefeat(); judgeMsg = new FlexMsg(); if (Vroot.children.length > 1) { judgeMsg.add ("Examining "+game.contract.Vladimir()+"'s subsidiary arguments:"); compareSecondaryArguments (Vroot, Eroot, MORE_SPECIFIC); } if (Eroot.children.length > 1) { judgeMsg.add ("Examining "+game.contract.Estragon()+"'s subsidiary arguments:"); compareSecondaryArguments (Eroot, Vroot, NONCOMPARABLE); } int verdict; boolean answer; verdict = checkSpecificity (Eroot.children[0], Vroot.children[0]); if (verdict != LESS_SPECIFIC) { answer = true; judgeMsg.add (game.contract.Estragon()+"'s arguments are not less specific."); } else { answer = false; judgeMsg.add (game.contract.Estragon()+"'s arguments are less specific."); } return (answer); } // check the subsidiary arguments for defeat void compareSecondaryArguments (CaseNode attacker, CaseNode responder, int necessaryCondition) { CaseNode attack; CaseNode[] response; int verdict; for (int i = 1; i < attacker.children.length; i++) { attack = attacker.children[i]; response = responder.getOpposition (attack.myCase.decision); if (response != null) for (int j = 0; j < response.length; j++) { verdict = checkSpecificity (response[j], attack); if (verdict == MORE_SPECIFIC) response[j].markDefeated(); else if ((verdict == NONCOMPARABLE) && (necessaryCondition == NONCOMPARABLE)) response[j].markDefeated(); } } } // check whether two argument trees activate each other int checkSpecificity (CaseNode a, CaseNode b) { if (!CardDeck.opposite (a.myCase.decision, b.myCase.decision)) throw new IllegalArgumentException (CardDeck.textCard(a.myCase.decision) + " !opposite " + CardDeck.textCard(b.myCase.decision)); if ((a.children == null) || (b.children == null)) return (NONCOMPARABLE); CaseNode[] a_activators; CaseNode[] b_activators; boolean astat; boolean bstat; a_activators = new CaseNode[a.children.length]; b_activators = new CaseNode[b.children.length]; System.arraycopy (a.children, 0, a_activators, 0, a.children.length); System.arraycopy (b.children, 0, b_activators, 0, b.children.length); printDebug ("Original activators", a.children, b.children); a_activators = removeDefeated (a_activators); b_activators = removeDefeated (b_activators); printDebug ("RemoveDefeated", a_activators, b_activators); if ((a_activators == null) && (b_activators == null)) return (NONCOMPARABLE); if (a_activators == null) return (LESS_SPECIFIC); if (b_activators == null) return (MORE_SPECIFIC); // add ordered evidence, defeasible specs IN ORDER if (game.contract.OrderedEvidence) { a_activators = addOrderedEvidence (a_activators); b_activators = addOrderedEvidence (b_activators); printDebug ("OrderedEvidence", a_activators, b_activators); } if (game.contract.DefeasibleSpecificity) { a_activators = addDefeasibleSpecificity (a_activators); b_activators = addDefeasibleSpecificity (b_activators); printDebug ("DefeasibleSpecificity", a_activators, b_activators); } astat = a.activatedBy (b_activators); bstat = b.activatedBy (a_activators); if (astat) System.out.println ("A.activated"); if (bstat) System.out.println ("B.activated"); if (astat && bstat) return (NONCOMPARABLE); if (astat) return (LESS_SPECIFIC); if (bstat) return (MORE_SPECIFIC); return (NONCOMPARABLE); } CaseNode[] removeDefeated (CaseNode[] activators) { boolean[] ok = new boolean[activators.length]; int numOK = 0; for (int i = 0; i < activators.length; i++) { ok[i] = !activators[i].defeated; if (ok[i]) numOK++; } if (numOK == 0) return null; CaseNode[] newActivators = new CaseNode[numOK]; int index = 0; for (int i = 0; i < activators.length; i++) { if (ok[i]) { newActivators[index] = activators[i]; index++; } } activators = null; return (newActivators); } CaseNode[] addOrderedEvidence (CaseNode[] activators) { CaseNode[] otherEvidence = null; CaseNode[] tmp1; CaseNode[] tmp2; for (int i = 0; i < activators.length; i++) { if ((activators[i].myCase.optional == null) // if case is evidence && (activators[i].myCase.index < 2)) { // one of the first 2 tmp1 = new CaseNode[2 - activators[i].myCase.index]; for (int j = 0; j < tmp1.length; j++) { tmp1[j] = new CaseNode (Case.evidence[2 - j]); } if (otherEvidence == null) { otherEvidence = tmp1; } else { tmp2 = otherEvidence; otherEvidence = new CaseNode[tmp1.length+tmp2.length]; System.arraycopy (tmp1, 0, otherEvidence, 0, tmp1.length); System.arraycopy (tmp2, 0, otherEvidence, tmp1.length, tmp2.length); } } } if (otherEvidence != null) { tmp2 = activators; activators = new CaseNode[tmp2.length+otherEvidence.length]; System.arraycopy (otherEvidence, 0, activators, 0, otherEvidence.length); System.arraycopy (tmp2, 0, activators, otherEvidence.length, tmp2.length); } return (activators); } CaseNode[] addDefeasibleSpecificity (CaseNode[] activators) { int numNew; boolean[] casesIncluded = new boolean[Case.cases.length]; CaseNode[] newActivators = null; CaseNode[] tmp; Case c; CaseNode a; // initialize casesIncluded for (int i = 0; i < casesIncluded.length; i++) casesIncluded[i] = false; for (int i = 0; i < activators.length; i++) { if (activators[i].myCase.optional.length > 0) casesIncluded[activators[i].myCase.index] = true; } // add new cases numNew = activators.length; while ((numNew > 0) && (activators.length < Case.cases.length)) { for (int i = 0; i < Case.cases.length; i++) { if (!casesIncluded[i]) { // make sure not already present c = Case.cases[i]; for (int j = 0; j < numNew; j++) { // only look through the new ones a = activators[j]; if (c.isEvidenceOfCase (a.myCase.decision)) { if (newActivators == null) { newActivators = new CaseNode[1]; } else { tmp = newActivators; newActivators = new CaseNode[tmp.length+1]; System.arraycopy (tmp, 0, newActivators, 1, tmp.length); tmp = null; } newActivators[0] = new CaseNode (c); } } } } if (newActivators == null) numNew = 0; else { numNew = newActivators.length; tmp = new CaseNode[activators.length+numNew]; System.arraycopy (newActivators, 0, tmp, 0, numNew); System.arraycopy (activators, 0, tmp, numNew, activators.length); activators = tmp; tmp = null; newActivators = null; } } return (activators); } void printDebug (String s, CaseNode[] a, CaseNode[] b) { System.out.println (""); System.out.println (s); System.out.println ("A:"); for (int i = 0; i < a.length; i++) { System.out.println (" "+CardDeck.textCard(a[i].myCase.decision)); } System.out.println ("B:"); for (int i = 0; i < b.length; i++) { System.out.println (" "+CardDeck.textCard(b[i].myCase.decision)); } } // ----------------------- Graphics Functions -------------------------- // PAINT public void paint (Graphics g) { g.setFont (game.panels.fonts[0]); xinset = g.getFontMetrics().stringWidth("10"); yinset = g.getFontMetrics().getHeight(); depth = 1; // print out the judgment message if (judgeMsg != null) { judgeMsg.print(g, xinset, yinset, depth); depth += judgeMsg.length(); } // paint Vroot margin = 0; theStack.clear(); if (judgeMsg != null) depth = judgeMsg.length() + 2; else depth = 2; if (Vroot == null) { g.drawString ("No argument presented yet.", xinset, depth*yinset); if (errorMsg != null) g.drawString (errorMsg, xinset, (depth+1)*yinset); return; } g.drawString ("Drawing " + game.contract.Vladimir() + "\'s:", xinset, depth*yinset); depth++; paintNode (0, Vroot, g); // paint Eroot margin = (int)(size().height/2); theStack.clear(); if (judgeMsg != null) depth = judgeMsg.length() + 2; else depth = 2; g.setColor (Color.black); if (Eroot == null) { g.drawString ("No argument for "+game.contract.Estragon(), margin+xinset, depth*yinset); } else { g.drawString ("Drawing "+game.contract.Estragon()+"\'s:", margin+xinset, depth*yinset); depth++; paintNode (0, Eroot, g); } } // PAINT NODE -- recursive tree-painting function public void paintNode (int level, CaseNode currNode, Graphics g) { depth++; int x = margin + xinset * (level + 1); int y = yinset * depth; if (currNode.myCase != null) { // make sure that case does not appear earlier in stack if (theStack.hasCase (Support.NewSupport (currNode.myCase.decision))) { g.setColor (CardDeck.color (currNode.myCase.decision)); g.drawString ("*" + CardDeck.toString (currNode.myCase.decision) + "* used earlier", x, y); return; } // draw symbol g.setColor (CardDeck.color (currNode.myCase.decision)); if (Support.isEvidence(currNode.myCase.decision)) { // case is evidence String factStr = CardDeck.toString (currNode.myCase.decision)+"!"; g.drawString (factStr, x, y); // if ordered evidence, add the implied evidence if (game.contract.OrderedEvidence) { for (int i = currNode.myCase.index+1; i < game.contract.evidence.length; i++) { x += g.getFontMetrics().stringWidth(factStr); factStr = ","+CardDeck.toString(game.contract.evidence[i]); g.setColor (CardDeck.color (game.contract.evidence[i])); g.drawString (factStr, x, y); } x = margin + xinset * (level + 1); } } else { // plainVanilla case g.drawString (CardDeck.toString (currNode.myCase.decision), x, y); } { // draw line connecting to parents if (currNode.defeated) g.setColor (Color.red); else g.setColor (Color.darkGray); ArgLevel al = theStack.peek(); if ((al != null) && (al.c != null)) { g.drawLine (al.x, al.y, al.x, y-yinset/2); g.drawLine (al.x, y-yinset/2, x-3, y-yinset/2); } } } // paint the rest of the children if (currNode.children != null) { theStack.push (x, y, currNode.myCase); for (int i = 0; i < currNode.children.length; i++) { paintNode (level+1, currNode.children[i], g); } theStack.pop (); // if there aren't any, see if there should be } else if ((currNode.myCase != null) && (currNode.myCase.opened)) { g.setColor (Color.black); g.drawString (" (needs support)", x, y); } } } // CLASS: -------------------- CASE NODE ------------------------------- // a node in the argument tree // class CaseNode { Case myCase; CaseNode[] children; boolean defeated; // CONSTRUCTOR for root of tree CaseNode () { myCase = null; children = null; defeated = false; } // CONSTRUCTOR for nodes CaseNode (Case theCase) { myCase = theCase; children = null; defeated = false; } // ADD CHILD // called by ArgumentChoice.makeVroot() and ArgumentChoice.makeEroot // CaseNode addChild (char oldchar) { char ch = (char) (oldchar + 'a'); System.out.println ("Adding child:" + String.valueOf(ch)); Case newCase; // find out what case the new child is if (oldchar < 3) { // child is evidence newCase = Case.evidence[oldchar]; } else if (oldchar - 3 < Case.cases.length) { // child is a case newCase = Case.cases[oldchar - 3]; } else { throw new IllegalArgumentException ("Bad char " + String.valueOf(ch)); } // see if this new child is already in the list of children if (children != null) for (int i = 0; i < children.length; i++) if (children[i].myCase == newCase) { return children[i]; } // if not, then make sure it's live... if (!newCase.live) { throw new IllegalArgumentException("Case not live: "+String.valueOf(ch)); } // ... and one of the optionals if (myCase != null) { if (myCase.optional == null) { throw new IllegalArgumentException ("Evidence " + String.valueOf(ch) + " needs no grounding"); } boolean isOptional = false; for (int i = 0; (i < myCase.numOptional) && (!isOptional); i++) { if (CardDeck.equivalent (myCase.optional[i].decision, newCase.decision)) { isOptional = true; } } if (!isOptional) { throw new IllegalArgumentException ("Case "+String.valueOf(ch)+" does not follow from this argument"); } } // add new child if (children != null) { CaseNode[] oldChildren = children; children = new CaseNode[oldChildren.length + 1]; System.arraycopy (oldChildren, 0, children, 0, oldChildren.length); } else { children = new CaseNode[1]; } children[children.length-1] = new CaseNode (newCase); return (children[children.length-1]); } // returns true if decision occurs in tree boolean searchTree (int decision) { if ((myCase != null) && (CardDeck.equivalent (myCase.decision, decision))) { return true; } if (children != null) { for (int i = 0; i < children.length; i++) { if (children[i].searchTree (decision)) return true; } } return false; } void unDefeat() { defeated = false; if (children != null) { for (int i = 0; i < children.length; i++) { if (children[i] == null) { String name; if (myCase == null) name = "null"; else name = CardDeck.textCard (myCase.decision); throw new IllegalArgumentException ("Null child in tree at node "+ name); } children[i].unDefeat(); } } } void markDefeated () { defeated = true; if (children != null) for (int i = 0; i < children.length; i++) { children[i].markDefeated(); } } boolean live() { if (defeated) return (false); if ((children == null) && (myCase.live)) return (true); for (int i = 0; i < children.length; i++) { if (children[i].live()) return (true); } return (false); } boolean activatedBy (CaseNode[] activators) { if (activators == null) return (false); // if activators actually activate decision, must concede for (int i = 0; i < activators.length; i++) { if ((activators[i].myCase == myCase) || (CardDeck.equivalent (activators[i].myCase.decision, myCase.decision))) return (true); } if (children == null) return (false); // otherwise, the only way for activators to win is // to activate all the children. boolean notCompletelyGone = false; for (int i = 0; i < children.length; i++) { if (children[i].activatedBy(activators) == false) notCompletelyGone = true; } return (!notCompletelyGone); } CaseNode[] getOpposition (int claim) { claim = CardDeck.getOpposite (claim); CaseNode[] answers = null; if ((myCase != null) && (CardDeck.equivalent (myCase.decision, claim))) { answers = new CaseNode[1]; answers[0] = this; } else if (children != null) { CaseNode[] tmp1; CaseNode[] tmp2; for (int i = 0; i < children.length; i++) { tmp1 = children[i].getOpposition(claim); if (tmp1 != null) { if (answers != null) { tmp2 = answers; answers = new CaseNode[tmp1.length+tmp2.length]; System.arraycopy (tmp1, 0, answers, 0, tmp1.length); System.arraycopy (tmp2, 0, answers, tmp1.length, tmp2.length); } else { answers = tmp1; } } } } if (answers != null) return (answers); else return (null); } } class FlexMsg { String[] msg; int len; FlexMsg () { msg = new String[2]; len = 0; } public void add (String s) { if (msg.length <= len) { String[] tmp = msg; msg = new String[msg.length*2]; System.arraycopy (tmp, 0, msg, 0, tmp.length); } msg[len] = s; len++; } public void print (Graphics g, int x, int y, int depth) { for (int i = 0; i < len; i++) { g.drawString (msg[i], x, y*(depth+i)); } } public int length () { return (len); } }