/* * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.aitools.programd.processor.aiml; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.aitools.programd.Core; import org.aitools.programd.CoreSettings; import org.aitools.programd.parser.TemplateParser; import org.aitools.programd.processor.ProcessorException; import org.aitools.util.math.MersenneTwisterFast; import org.apache.commons.collections.map.LRUMap; import org.jdom.Element; /** * <p> * Handles a <code><a href="http://aitools.org/aiml/TR/2001/WD-aiml/#section-random">random</a></code> element. * </p> * <p> * To achieve the kind of randomness expected by users and AIML authors, the following requirements exist: * </p> * <ul> * <li>Each <code>random</code> element should have its own "space". This means that, for example, if * <code>random</code> element <code>A</code> contains five <code>li</code> children, and <code>random</code> element * <code>B</code> contains seven <code>li</code> children, the probability that any given <code>li</code> of * <code>A</code> will be chosen when <code>A</code> is activated should con- sistently be 1:5, and the probability that * any given <code>li</code> of <code>B</code> will be chosen when <code>B</code> is chosen should consistently be 1:7. * Essentially, each <code>random</code> must have its own unique series of random numbers.</li> * <li>A "unique space" requirement exists as well on a per-user basis: each user should have an equivalent * experience of randomness for each <code>random</code> element, independent of any other users.</li> * <li>The individual bot also has a uniqueness requirement, multiplying the previous two. In effect, if there are * <code>m</code> bots, <code>n</code> users, and <code>p</code> random elements, there are (potentially) * <code>m * n * n</code> independent random number series.</li> * </ul> * <p> * As an alternative to the first point, it is possible (since 4.7) to set Program D to process <code>random</code> * elements in a kind of stack-based fashion, so no list item will be repeated (within the same per-user, per-bot space) * until all others have been chosen. * </p> * * @author <a href="mailto:noel@aitools.org">Noel Bush</a> * @author Jon Baer * @author Thomas Ringate, Pedro Colla * @author Jay Myers */ public class RandomProcessor extends AIMLProcessor { /** The label (as required by the registration scheme). */ public static final String label = "random"; /** The tag name for a listitem element. */ public static final String LI = "li"; @SuppressWarnings("boxing") private static List<Integer> makeIncrementingList(int size) { List<Integer> result = new ArrayList<Integer>(); for (int index = 0; index < size; index++) { result.add(index); } return result; } /** * The map in which MersenneTwisterFast random number generators will be stored for each unique botid + userid + * random element. */ private LRUMap generators = new LRUMap(100); /** * The map in which indices not-yet-used listitems will be stored if non-repeating random choosing is enabled. (We * store indices rather than references to the listitems themselves, because the DOM Element doesn't appear to * implement equals() in a way that makes List operations like remove() work. */ private Map<String, List<Integer>> availableIndices = new HashMap<String, List<Integer>>(); /** * Creates a new RandomProcessor using the given Core. * * @param core the Core object to use */ public RandomProcessor(Core core) { super(core); this.availableIndices = core.getStoredObject(RandomProcessor.class.getName(), "availableIndices", this.availableIndices); this.generators = core.getStoredObject(RandomProcessor.class.getName(), "generators", this.generators); } /** * @see AIMLProcessor#process(Element, TemplateParser) */ @SuppressWarnings({ "boxing", "unchecked" }) @Override public String process(Element element, TemplateParser parser) throws ProcessorException { // Construct the identifying string (botid + userid + element // contents). String userid = parser.getUserID(); String identifier = parser.getBotID() + userid + element.hashCode(); // Does the generators map already contain this one? MersenneTwisterFast generator = (MersenneTwisterFast) this.generators.get(identifier); if (generator == null) { generator = new MersenneTwisterFast(System.currentTimeMillis()); this.generators.put(identifier, generator); } List<Element> listitems = element.getChildren(); int nodeCount = listitems.size(); // Only one <li></li> child means we don't have to pick anything. if (nodeCount == 1) { return parser.evaluate(listitems.get(0).getChildren()); } // Otherwise, select a random element of the listitem (if strategy is pure-random). if (this._core.getSettings().getRandomStrategy() == CoreSettings.RandomStrategy.PURE_RANDOM) { return parser.evaluate(listitems.get(generator.nextInt(nodeCount)).getContent()); } // If we get here, then the no-repeat strategy is wanted. List<Integer> indices; Integer choice = null; // Check whether this random + userid + botid has been selected before. if (this.availableIndices.containsKey(identifier)) { // If it has, get the remaining available sets. indices = this.availableIndices.get(identifier); // Note that, because of the logic below, this set will never get to size 0. assert indices.size() > 0 : "Random strategy logic failed."; /* * If it is complete, then we've been through all before, and the last index in the list was the last one chosen * (see below), so make sure this first choice does not repeat the last one. */ if (indices.size() == nodeCount) { choice = indices.get(generator.nextInt(indices.size() - 1)); } else { // Otherwise just make a random choice from the indices. choice = indices.get(generator.nextInt(indices.size())); } } else { // If it has not (been selected before), create a new set containing an index for each listitem. indices = makeIncrementingList(nodeCount); this.availableIndices.put(identifier, indices); // Make a random choice from the indices. choice = indices.get(generator.nextInt(indices.size())); } // Remove the chosen index. indices.remove(choice); // If this has reduced the size to zero, if (indices.size() == 0) { // Reconstitute the list, indices.addAll(makeIncrementingList(nodeCount)); // but remove the last choice, indices.remove(choice); // and place it last, so we can avoid repeating it next go-round (see above). indices.add(choice); } // Evaluate the node corresponding to the chosen index. return parser.evaluate(listitems.get(choice).getContent()); } }