import rita.RiText; RiText fragmentTexts[]; PFont font; float colWidth = 220; float colGutter = 34; float colBreak = 500; float topMargin = 34; float leftMargin = 20; Fragment caveRoot; int currentFragmentID; HashMap currentLinks; Path currentPath; ArrayList allVisitedPaths; HashSet visited; HashSet endings; int clickTotal = 0; boolean gameOver = false; boolean gameStarted = false; ArrayList allPaths; ArrayList longestPaths; ArrayList shortestPaths; int shortestFound = 0; int longestFound = 0; RiText pageNumber; RiText staticHistory; RiText staticScore; RiText scoreDisplay; RiText[] stackNumbers; PImage instructions; void setup() { // processing setup size(800, 600); frameRate(15); smooth(); font = loadFont("Baskerville-14.vlw"); instructions = loadImage("instructions.png"); // load transcript String[] lines = loadStrings("cot_transcript.txt"); // create root node and parse caveRoot = new Fragment(); caveRoot = caveRoot.parseToTree(lines); currentFragmentID = caveRoot.getID(); // initialize UI data currentLinks = new HashMap(); currentPath = new Path(); currentPath.append(caveRoot); visited = new HashSet(); endings = new HashSet(); allVisitedPaths = new ArrayList(); //caveRoot.dump(); //caveRoot.dumpPaths(); //caveRoot.printPaths(); //println("total fragments: " + caveRoot.getFragmentTotal()); //println("total endings: " + caveRoot.getTerminalTotal()); // generate list of shortest and longest paths shortestPaths = new ArrayList(); longestPaths = new ArrayList(); allPaths = caveRoot.getAllPaths(); // getAllPaths() returns a sorted result. Get from array until size changes to // collect all shortest paths // println("shortest paths"); // println("--------------"); int curval = ((Path)allPaths.get(0)).size(); for (int i = 0; i < allPaths.size(); i++) { Path p = (Path)allPaths.get(i); if (p.size() == curval) { shortestPaths.add(p); // println(p.toString()); } else { break; } } // same thing, but start at end for longest paths // println("longest paths"); // println("-------------"); curval = ((Path)allPaths.get(allPaths.size()-1)).size(); for (int i = allPaths.size() - 1; i >= 0; i--) { Path p = (Path)allPaths.get(i); if (p.size() == curval) { longestPaths.add(p); // println(p.toString()); } else { break; } } } void draw() { background(255); stroke(0); line(0, height - 70, width, height - 70); if (!gameStarted) { fill(255); rect(width/2 - instructions.width/2, height/2 - instructions.height/2, instructions.width, instructions.height); image(instructions, width/2 - instructions.width/2, height/2 - instructions.height/2); } } void keyPressed() { if (gameOver) return; if (!gameStarted) { gameStarted = true; updateFragment(); } if (keyCode == BACKSPACE) { if (currentPath.size() <= 1) { return; } currentPath.pop(); currentFragmentID = ((Fragment)currentPath.last()).getID(); updateFragment(); } } void mousePressed() { if (gameOver) return; if (!gameStarted) { gameStarted = true; updateFragment(); } RiText[] p = RiText.getPicked(mouseX, mouseY); if (p != null) { String linkText = p[0].getText(); // println(linkText); if (currentLinks.containsKey(linkText)) { Fragment nextFrag = (Fragment)currentLinks.get(linkText); currentFragmentID = nextFrag.getID(); currentPath.append(nextFrag); updateFragment(); clickTotal++; } } } int calculateScore() { int score = allVisitedPaths.size() * 5 + shortestFound * 5 + longestFound * 5; return score; } String scoreAsString() { int score = calculateScore(); int totalScore = (((ArrayList)caveRoot.getAllPaths()).size() * 5) + longestPaths.size() * 5 + shortestPaths.size() * 5; return score + "/" + totalScore; } void updateFragment() { RiText.deleteAll(fragmentTexts); Fragment drawMe = caveRoot.getFragmentByID(currentFragmentID); currentLinks = new HashMap(); if (drawMe != null) { visited.add(new Integer(drawMe.getID())); // player has visited this node! // println("found " + currentFragmentID + " with " + drawMe.getParas().size() + " lines"); ArrayList strings = wrappedParas(colWidth, drawMe.getParas(), font); strings.add(""); ArrayList drawLinks = drawMe.getLinks(); for (int i = 0; i < drawLinks.size(); i++) { Link li = (Link)drawLinks.get(i); // here is where all the scoring is done, for some reason! if (li == null) { if (!allVisitedPaths.contains(currentPath)) { RiText notification = new RiText(this, width - colGutter, height + 20); notification.align(RIGHT); notification.setColor(255f, 0f, 0f); notification.moveTo(width - colGutter, height - 90, 0.5f); notification.fadeOut(1.5f, 3f, true); if (longestPaths.contains(currentPath)) { notification.setText("+10! You found one of " + longestPaths.size() + " longest paths!"); longestFound++; } else if (shortestPaths.contains(currentPath)) { notification.setText("+10! You found one of " + shortestPaths.size() + " shortest paths!"); shortestFound++; } else { notification.setText("+5! You found a new path!"); } allVisitedPaths.add(currentPath); if (allVisitedPaths.size() == allPaths.size()) { RiText youwin = new RiText(this, width - colGutter, height + 20); youwin.align(RIGHT); youwin.setColor(0, 0, 255f); youwin.moveTo(width - colGutter, height - 120, 1.5f); youwin.setText("You win! You used " + clickTotal + " clicks."); gameOver = true; } currentPath = new Path(currentPath); } continue; } ArrayList linkLines = wrappedParas(colWidth, li.getLinkText(), font); for (int j = 0; j < linkLines.size(); j++) { String t = (String)linkLines.get(j); strings.add(t); currentLinks.put(t, li.getDest()); } } RiText.setDefaultFont("Baskerville-14.vlw"); fragmentTexts = new RiText[strings.size()]; float cy = topMargin; float cx = leftMargin; int colCount = 0; for (int i = 0; i < strings.size(); i++) { String current = (String)strings.get(i); if (cy == topMargin && current.equals("")) { continue; } fragmentTexts[i] = new RiText(this, current, cx, cy); cy += 18; if (cy > colBreak) { cx += colGutter + colWidth; cy = topMargin; } } } updateHud(); } void updateHud() { if (staticHistory == null) { staticHistory = new RiText(this, "History: ", width/4, height - 30); } if (staticScore == null) { staticScore = new RiText(this, "Score: ", (width/6)*5, height - 30); } if (pageNumber != null) { RiText.delete(pageNumber); } if (stackNumbers != null) { RiText.deleteAll(stackNumbers); } if (scoreDisplay != null) { RiText.delete(scoreDisplay); } pageNumber = new RiText(this, "Page " + currentFragmentID, 20, height - 30); stackNumbers = new RiText[currentPath.size()]; for (int i = 0; i < currentPath.size(); i++) { stackNumbers[i] = new RiText(this, String.valueOf(((Fragment)currentPath.get(i)).getID()), (width/4) + 50 + i*25, height - 30); } scoreDisplay = new RiText(this, scoreAsString(), (width/6)*5 + 50, height - 30); } ArrayList wrappedParas(float wrapWidth, String s, PFont font) { ArrayList tmp = new ArrayList(); tmp.add(s); return wrappedParas(wrapWidth, tmp, font); } ArrayList wrappedParas(float wrapWidth, ArrayList paras, PFont font) { ArrayList lines = new ArrayList(); textFont(font); for (int p = 0; p < paras.size(); p++) { String contents = (String)paras.get(p); int lastSpace = 0; int lastWrapIndex = 0; // guarantee that we won't wrap any narrower than the text's longest word String c = new String(contents); String[] words = c.split(" "); float minWidth = 0; for (int i = 0; i < words.length; i++) { if (textWidth(words[i]) > minWidth) { minWidth = textWidth(words[i]); } } if (wrapWidth < minWidth) { wrapWidth = minWidth; } while (true) { // get index of next space int nextSpace = contents.indexOf(" ", lastSpace); if (nextSpace == -1) { nextSpace = contents.length(); } // check to see if the chunk of text to the next space is longer than the maximum // width; if so, wrap to the last known space before the limit; otherwise, // move to the next space and try again. String sinceWrap = contents.substring(lastWrapIndex, nextSpace); if (textWidth(sinceWrap) > wrapWidth) { String wrapped = contents.substring(lastWrapIndex, lastSpace); lines.add(wrapped); lastWrapIndex = lastSpace; } else if (nextSpace == contents.length()) { lines.add(contents.substring(lastWrapIndex, contents.length())); break; } else { lastSpace = nextSpace+1; } } lines.add(""); } return lines; } class Fragment { ArrayList links; ArrayList paras; ArrayList paths; int id; int fragmentTotal; int terminalTotal; Fragment() { init(); } Fragment(int id_) { init(); id = id_; } void init() { links = new ArrayList(); paras = new ArrayList(); paths = new ArrayList(); } void setID(int id_) { id = id_; } int getID() { return id; } void addLink(Link link_) { links.add(link_); } ArrayList getLinks() { return links; } void addPara(String s) { paras.add(s); } ArrayList getParas() { return paras; } String getParasAsString() { StringBuffer cat = new StringBuffer(); for (int i = 0; i < paras.size() - 1; i++) { cat.append((String)paras.get(i)); cat.append("\n\n"); } cat.append((String)paras.get(paras.size()-1)); return cat.toString(); } Fragment getFragmentByID(int id_) { if (id == id_) { return this; } for (int i = 0; i < links.size(); i++) { Link li = (Link)links.get(i); if (li == null) continue; Fragment frag = li.getDest(); Fragment target = frag.getFragmentByID(id_); if (target != null) { return target; } } return null; } Fragment parseToTree(String[] infile) { int currentFragmentID = 0; Fragment root = null; Fragment currentFragment = null; int ftotal = 0; int ttotal = 0; ArrayList orphans = new ArrayList(); for (int i = 0; i < infile.length; i++) { String line = infile[i]; // fragment number if (line.startsWith("<")) { int fragNumEnds = 0; int comma = line.indexOf(','); if (comma == -1) { fragNumEnds = line.indexOf('>'); } else { fragNumEnds = comma; } currentFragmentID = int(line.substring(1, fragNumEnds)); // println(">>> now parsing fragment " + currentFragmentID); ftotal++; if (root == null) { root = new Fragment(currentFragmentID); currentFragment = root; } else { // check to see if we've already added a dummy fragment for this ID currentFragment = null; Fragment check = root.getFragmentByID(currentFragmentID); if (check == null) { // check orphan trees // println("checking orphan fragments for " + currentFragmentID); for (int j = 0; j < orphans.size(); j++) { Fragment frag = (Fragment)orphans.get(j); Fragment ocheck = frag.getFragmentByID(currentFragmentID); if (ocheck != null) { currentFragment = ocheck; break; } } if (currentFragment == null) { // println("adding orphan for " + currentFragmentID); currentFragment = new Fragment(currentFragmentID); orphans.add(currentFragment); } } else { // println("found existing in tree for " + currentFragmentID); currentFragment = check; } } } // link spec else if (line.startsWith("[")) { if (line.charAt(1) == '-') { currentFragment.addLink(null); // terminal ttotal++; continue; } int colon = line.indexOf(':'); int closingBracket = line.indexOf(']'); int destFragmentID = int(line.substring(1, colon)); // println("now parsing link from " + currentFragment.getID() + " to " + destFragmentID); String linkText = line.substring(colon+2, closingBracket); Fragment dest = null; // check orphans first for (int j = 0; j < orphans.size(); j++) { Fragment frag = (Fragment)orphans.get(j); if (frag.getID() == destFragmentID) { dest = frag; orphans.remove(j); // println("got top-level orphan fragment tree; linking and removing " + destFragmentID); break; } else { Fragment frag2 = frag.getFragmentByID(destFragmentID); if (frag2 != null && frag2.getID() == destFragmentID) { dest = frag2; // println("got fragment from orphan tree; linking " + // destFragmentID + " from orphan tree " + frag.getID()); break; } } } // if no orphan, check tree; if not in tree, create dummy node if (dest == null) { Fragment check = root.getFragmentByID(destFragmentID); if (check == null) { // println("creating dummy fragment for " + destFragmentID); dest = new Fragment(destFragmentID); } else { dest = check; } } Link li = new Link(currentFragment, dest, linkText); currentFragment.addLink(li); } else if (!line.equals("")) { // println("got line " + line); currentFragment.addPara(line); } } if (orphans.size() > 0) { print("warning: orphans remain: "); for (int i = 0; i < orphans.size(); i++) { Fragment frag = (Fragment)orphans.get(i); print(frag.getID() + " "); } println(); } root.setFragmentTotal(ftotal); root.setTerminalTotal(ttotal); root.findAllPaths(); return root; } int getFragmentTotal() { return fragmentTotal; } void setFragmentTotal(int ftotal) { fragmentTotal = ftotal; } int getTerminalTotal() { return terminalTotal; } void setTerminalTotal(int ttotal) { terminalTotal = ttotal; } void dump() { println("----"); println("fragment id " + id); println(paras.size() + " lines"); println("links to: "); for (int i = 0; i < links.size(); i++) { Link li = (Link)links.get(i); if (li == null) continue; println(li.getDest().getID()); } println("recursing..."); for (int i = 0; i < links.size(); i++) { Link li = (Link)links.get(i); if (li == null) { println("terminal"); continue; } Fragment dest = li.getDest(); if (dest != null) { dest.dump(); } } } void printPaths() { printPaths("", null); } void printPaths(String sofar, Link from) { // append all of our data if (from != null) { sofar += "[" + from.getLinkText() + "]\n\n"; } for (int i = 0; i < paras.size(); i++) { sofar += (String)paras.get(i); sofar += "\n\n"; } // recurse for (int i = 0; i < links.size(); i++) { Link li = (Link)links.get(i); if (li == null) { println("*****"); println(sofar); return; } else { if (li.getDest() != null) { li.getDest().printPaths(sofar, li); } } } } void dumpPaths() { println("all paths"); println("---------"); for (int i = 0; i < paths.size(); i++) { Path p = (Path)paths.get(i); println(p.toString()); } println(paths.size() + " total"); } void findAllPaths() { findAllPaths_internal(new Path(), paths); Collections.sort(paths); } void findAllPaths_internal(Path currentPath, ArrayList allPaths) { currentPath.append(this); for (int i = 0; i < links.size(); i++) { Link li = (Link)links.get(i); if (li == null) { allPaths.add(currentPath); // reached terminal return; } else { if (li.getDest() != null) { li.getDest().findAllPaths_internal(new Path(currentPath), allPaths); } } } } ArrayList getAllPaths() { return paths; } } class Link { Fragment dest; Fragment src; String linkText; Link(Fragment src_, Fragment dest_) { src = src_; dest = dest_; linkText = ""; } Link(Fragment src_, Fragment dest_, String linkText_) { src = src_; dest = dest_; linkText = linkText_; } void setDest(Fragment dest_) { dest = dest_; } void setLinkText(String linkText_) { linkText = linkText_; } String getLinkText() { return linkText; } Fragment getDest() { return dest; } } class Path implements Comparable { ArrayList fragments; Path() { fragments = new ArrayList(); } Path(Path copy_) { fragments = (ArrayList)copy_.getFragments().clone(); } void append(Fragment f) { fragments.add(f); } int size() { return fragments.size(); } Fragment get(int index) { return (Fragment)fragments.get(index); } ArrayList getFragments() { return fragments; } Fragment pop() { Fragment popped = last(); fragments.remove(size()-1); return popped; } Fragment last() { return (Fragment)fragments.get(size()-1); } int[] getIDs() { int[] ids = new int[fragments.size()]; for (int i = 0; i < fragments.size(); i++) { ids[i] = ((Fragment)fragments.get(i)).getID(); } return ids; } boolean equals(Object o) { Path other = (Path)o; if (other.size() != size()) { return false; } for (int i = 0; i < size(); i++) { Fragment ours = get(i); Fragment theirs = other.get(i); if (ours.getID() != theirs.getID()) { return false; } } return true; } String toString() { int[] ids = getIDs(); return join(nf(ids, 0), " => ") + " (" + size() + ")"; } int compareTo(Object o) { return size() - ((Path)o).size(); } }