import java.util.*;

/** A SissyfightGame keeps track of players' health, 
    licks and tattles, etc; maintains overall game state
    and judges the results of turns.

    RSB 19990617

*/


public class SissyfightGame {
  
  // constants
  public static final int TURN_TIMEOUT		= 120,	// seconds after turn starts before it is
							// forced to end even if clients aren't ready
			  TURN_TIMEOUT_WARNING  = TURN_TIMEOUT-20;  // when all players act, advance timeout to 20 secs


  public static final int DAMAGE_LOLLY		= -2,
                          DAMAGE_GRAB		=  1,
                          DAMAGE_IDLE		=  1,
                          DAMAGE_SCRATCH	=  1,
 		          DAMAGE_GRAB_SCRATCH	=  2,
 		          DAMAGE_LOLLY_SCRATCH	=  2,
 		          DAMAGE_TATTLE_SUCC	=  3,
 		          DAMAGE_TATTLE_FAIL	=  3,
		// tease damage is 0 if 1 attacker, 
		// else ADD+MULT*(# of attackers)
			  DAMAGE_TEASE_ADD      =  0,
			  DAMAGE_TEASE_MULT     =  2,
			  DAMAGE_COWER_SCARED	=  1,	// for cowering unproductively twice in a row
       
		          MAX_HEALTH		= 10,
		          MAX_TATTLES		=  2,
		          MAX_LOLLIES		=  3;
  
  public static final int MAX_PLAYERS           =  6,
			  MIN_PLAYERS           =  3;

  public static final int BROWNIE_PTS_PLAY      = 10,   // pts for finishing a game
			  BROWNIE_PTS_DEFEAT    = 20,   // pts for each girl you defeated
			  BROWNIE_PTS_RUN_AWAY  = 0; 	// pts for not finishing a game

  // points for new ladder-like system
  public static final int LADDER_PTS_PLAY		= 10,	// pts for finishing a game
			  LADDER_PTS_DEFEAT_LOWER	= 20,	// pts for defeating someone with a lower score than you
			  LADDER_PTS_DEFEAT_HIGHER	= 30,	// pts for defeating someone with higher score
			  LADDER_PTS_RUN_AWAY		= 0;	// pts for not finishing a game


  public static final String[] HUMILIATION_TEXTS = {
	" was totally embarassed and lost all her self-esteem. She has to sit out for the rest of the game!",
	" couldn't take it any more and collapsed into sobs! With no self-esteem left, she's out of the game!",
	" broke down into a bawling crybaby from being humiliated so much! And the game went on..."
  };

  public static final Object[] ACTION_ORDER = {
    "cower",     new Integer(1),
    "grab",      new Integer(2),
    "lick",      new Integer(3),
    "scratch",   new Integer(4),
    "tease",     new Integer(4),
    "tattle",    new Integer(6),
    "idle",      new Integer(10),
    "leave",	 new Integer(12)
  };

  // globals
  Hashtable players = new Hashtable();
  Hashtable scores = new Hashtable();  // just maps nicknames to game's scores
  Hashtable pregame_scores;	// maps nicknames to previous score records
  Room room;
  String state;
  Vector announcements = new Vector();
  boolean eot_announced = false;
  long turn_time=0; // system time at which current turn started (for timeout)


  /** constructor: vector of nicknames of the (3-6) players,
      hash of names to their pre-game scores (or null of not registered),
      and also the Room so that game can send announcements.
  */

  public SissyfightGame(Vector playerNames, Hashtable pregame_scores, Room gameroom) {
    for (Iterator i=playerNames.iterator(); i.hasNext(); ) {
	String pn = (String)i.next();
	//if (pn == null) WordUtils.dbg("SissyfightGame constructor: someone handed me a vector containing a null string!");
	players.put(pn, new SissyfightGamePlayer(pn));
    }
    room = gameroom;
    this.pregame_scores = pregame_scores;

    /* CHANGE 19990727: game starts as soon as created (room keeps track of start votes)
    state = "before start";
    */
    readyToStart();
  }



  void readyToStart() {
    //  reset players' ready flags and go to state "await actions"
    resetReady(players);
    state = "await actions";	
    resetActed(players);

    announce("start_game","","");
    announce("status",player_status(players), "");
    eot_announced = false;
    start_turn();
  }

  void start_turn() {
    announce("start_of_turn","","");
    turn_time = (new Date()).getTime();
  }




  /** GameRoom calls leave if a player leaves before game is over 
  
      Returns true if there's nobody left in the game or game is over
  */
  public boolean leave(String name) {
    WordUtils.dbg("GAME: leave("+name+")");
    SissyfightGamePlayer p = (SissyfightGamePlayer)players.get(name);
    if (p == null) return false;
    p.zombie = true;
    p.action = "leave";


    // if everyone here is a zombie, might as well abandon the game.
    if (checkZombies(players)) return true;

    // otherwise, let the game continue 
    if (state.equals("await action")) {
      check_act();
      return false;
    }
    else if (state.equals("await eot")) {
      check_act();  // added experimentally.  IS THIS A GOOD IDEA?
      return check_end_of_turn();
    }

    return false;
  }




  /** call act when a player chooses an action (including idle = timeout)

      returns true if all players have acted.
  */
  public synchronized boolean act(String name, String action, String target) {
    if (!(state.equals("await actions")||state.equals("await eot"))) return false; 

    SissyfightGamePlayer p = (SissyfightGamePlayer)players.get(name);
    if (p == null) return false;
    if (action == null) return false;

    // there could be error checking before we announce
    // that the player has acted; but it's not that important --
    // surely the client can't send illegal actions, but if it 
    // does, they'll get caught at resolve time.
    if (! action.equals("idle")) {
      // idle isn't good enough to get rid of that "?" by your head!
      announce("acted", name, "");
    }
    p.action = action.toLowerCase();	// note lowercase is canonical!
    p.targetName = target;
    p.target = (SissyfightGamePlayer)players.get(target);

    return check_act();
  }

  public synchronized boolean check_act() {
    // if all players have acted, send the 10-second warning
    // (players have 10 seconds to change their minds)
    // all clients must respond with end_of_turn within 10 seconds.
    if (checkActed(players) && !eot_announced) {
      state = "await eot";
      announce("end_of_turn","","");
      eot_announced = true;

      // advance turn timeout timer too
      long now = new Date().getTime();
      turn_time = now - 1000*TURN_TIMEOUT_WARNING;
    }

    return true;
  }

  /** return true if too much time has elapsed since start of turn */
  public boolean check_timeout() {
    long now = new Date().getTime();
    WordUtils.dbg("CHECK TIMEOUT: " + ((now-turn_time)/1000)); 
    return (turn_time > 0 && (now - turn_time) > 1000*TURN_TIMEOUT);
  }


  /** called every few seconds as clients send in their ping messages:
      use this opportunity to check for turn timeout. */
  public void ping() {
    /* deleted, trying another approach 
    if (state.equals("await actions") || state.equals("await eot")) {

      if (check_end_of_turn()) {
	WordUtils.dbg("*** TURN ENDED BY TIMEOUT ***");
      }

    }
    */
    if (check_timeout()) {
      WordUtils.dbg("*** TIMEOUT DETECTED ***");
      // anyone who hasn't registered an action gets an idle
      setActed(players);

      if (state.equals("await action")) {
        check_act();
      }
      else if (state.equals("await eot")) {
        check_end_of_turn();
      }

    }
  }


  /** call end_of_turn when client acknowledges turn end by sending end_of_turn
      (easy enough!)

      Returns true only if all clients ack'd, turn resolved, game over.
  */
  public synchronized boolean end_of_turn(String name) {
    if (!state.equals("await eot")) return false; 

    SissyfightGamePlayer p = (SissyfightGamePlayer)players.get(name);
    if (p == null) return false;

    p.ready = true;

    return check_end_of_turn();
  }

  public synchronized boolean check_end_of_turn() {
    // if all players have acknowledged end of turn, 
    // or the server turn timeout has expired,
    // it's time to resolve the turn.
    if (!checkReady(players) && !check_timeout()) return false;

    // resolveTurn returns true if game is over
    int end_state = resolveTurn(players); // 0 = not over, 1 = over no winners, 2 = over with winners
    if (end_state>0) {
      state = "end of game";
      announce("end_game",""+end_state,"");
    } 
    else {
      state = "await actions";
      eot_announced = false;
      start_turn();
    }

    resetReady(players);
    resetActed(players);

    return state.equals("end of game");
  }



  /** resolveTurn is the tricky part.
      It figures out who lives and who dies,
      and announces the scenes that describe the fight.

      Returns 0 if game's not over, 1 if game ended with no winners, 2 if game ended with 1 or 2 winners.
  */
  int resolveTurn(Hashtable players) {
    int return_val = 0;
    Vector narrative = new Vector();

// FOR TESTING
WordUtils.dbg("RESOLVE TURN---------------------------------------");
for (Enumeration e = players.elements(); e.hasMoreElements(); ) {
  SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
  String st= "  " + n.name+"("+n.health+"): " + n.action + " ";
  if (n.target != null) st=st+n.target.name;
  WordUtils.dbg(st);
}
WordUtils.dbg("---------------------------------------------------");

    // STAGE 1: resolve the individual actions, in order.
    //  (also turn any ill-formed actions into "idle" actions)

    // a particular action is "resolved" if nothing more needs to
    // be done with that action structure when creating narrative.
    // (the narrative of another action will include this one)



    // Preparing to resolve actions:
    // . copy the non-dead players into Vector actions
    // .  (and while we're at it, turn any ill-formed actions into IDLE)
    // . shuffle the Vector (so simultaneous grabs will be resolved at random)
    // . and sort it by action order.

    
    Random rnd = new Random();
    Vector actions = new Vector();

    // copy non-dead players (and check that actions are OK)
    for (Enumeration e = players.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();

      if (n.health==0) continue;  // skip dead players
      n.resetTurn();	// clear turn data

      // defective transitive actions (missing target) get turned into idle
      if (n.action==null) n.action="idle";
      if (n.action.equals("grab")
	  || n.action.equals("scratch")
	  || n.action.equals("tease")) {
	if (n.target == null) n.action = "idle";
      }

      if (n.zombie && !n.action.equals("leave")) continue; // skip zombies, unless they just left this turn

      actions.add(n);
    }

    // now sort by action-order
    Collections.sort(actions, new SissyfightGamePlayer());


    // now we're ready for the actual resolution process
    for (Enumeration e = actions.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer act = (SissyfightGamePlayer)e.nextElement();
      // "act" refers to the Actor and to the Action!



      // COWER: target avoids 1 grab or scratch.
      if (act.action.equals("cower")) {
        act.cowering = true;
	// cower isn't resolved until someone attacks
      }

      // GRAB: target cannot take any action except cower or tattle.
      //  target receives 1 pt damage for each grab after first.
      //  In resolving grabs, the first resolved grab could 
      //    interrupt its target's grab.
      else if (act.action.equals("grab")) {
        // has subject been grabbed already?
	if (act.grabbed) {
	  // record that subject's grab was interrupted
	  act.interrupted = "grab " + act.target.name;
	  // this action is resolved
	  act.resolved = true;
	}

	else { // subject wasn't grabbed
	  // is target cowering? and cower isn't already resolved (used up)?
	  if (act.target.cowering && ! act.target.resolved) {
	    // log a scene 4: cowering from grab
	    narrative.add(new SissyfightGameNarration(
	      4,
	      act.name+" tried to grab "+act.target.name+" but she cowered away.",
	      "coweringFromGrab(s,"+LR()+",\""+act.target.name+"\",\""+act.name+"\")",
	      null
	    ));

	    // both the original cower and this grab have been resolved.
	    act.resolved = true;
	    act.target.resolved = true;
	  }

	  else { // target not cowering
	    // successful grab: note it.
	    act.target.grabbed = true;
	    act.target.grabbers.add(act);
	  }
	} // end subject not grabbed
      } // end GRAB




      // SCRATCH: target receives 1 damage, or 2 if grabbed.
      else if (act.action.equals("scratch")) {
        // has subject been grabbed already?
	if (act.grabbed) {
	  // record that subject's scratch was interrupted
	  act.interrupted = "scratch " + act.target.name;
	  // this action is resolved
	  act.resolved = true;
	}
	
	else { // subject wasn't grabbed
	  // is target cowering? and cower isn't already resolved (used up)?
	  if (act.target.cowering && ! act.target.resolved) {
	    // log a scene 5: cowering from scratch
	    narrative.add(new SissyfightGameNarration(
	      5,
	      act.name+" tried to scratch "+act.target.name+" but she cowered away.",
	      "coweringFromScratch(s,"+LR()+",\""+act.target.name+"\",\""+act.name+"\")",
	      null
	    ));

	    // both the original cower and this scratch have been resolved.
	    act.resolved = true;
	    act.target.resolved = true;
	  }

	  else { // target not cowering
	    // successful scratch: note it.
	    act.target.scratched = true;
	    act.target.scratchers.add(act);
	  } 

	} // end subject not grabbed

      } // end SCRATCH



      // TEASE: target receives damage according to # of teasers
      //  if only 1 teaser, tease fails; no damage.
      else if (act.action.equals("tease")) {
        // has subject been grabbed already?
	if (act.grabbed) {
	  // record that subject's tease was interrupted
	  act.interrupted = "tease " + act.target.name;
	  // this action is resolved
	  act.resolved = true;
	}

	// otherwise tease occurs
	else {
          act.target.teasers.add(act);
	}

      } // end TEASE


      // LICK LOLLY: if subject is scratched, lick fails+double damage
      //   successful licker has damage reduced by DAMAGE_LOLLY
      //   successful licker is immune to tattles
      //   Each player may lick only MAX_LOLLIES times per game
      //      (enforced by client as well)
      // don't check for interrupted by grab here; will resolve it
      //   during grab scene.
      else if (act.action.equals("lick")) {
        // in case of cheating client, check if lollies already used up
	if (act.lollies == 0) {
	  act.action = "idle";	// if illegal lolly, change action to idle
	  act.idling = true;
	}
	else {
	  act.lollies--;
	  act.licking = true;
	}
      } // end LICK


      // TATTLE: if 1 player tattles, all other non-licking players 
      //   get 2 damage.
      //   If more than 1 player tattles, all tattlers get 3 damage.
      //   Each player may tattle only MAX_TATTLES times per game
      else if (act.action.equals("tattle")) {
	// in case of cheating client, check if tattles used up
	if (act.tattles == 0) {
	  act.action = "idle";
	  act.idling = true;
	}
	else {
	  act.tattles--;
	  act.tattling = true;
	}
      } // end TATTLE


    } // end of iteration through actions
    // end of stage 1


    // STAGE 2
    // resolve unresolved actions into narrative scenes
    // (only scenes 4 and 5 have already been found during stage 1)



    // scene 1, cowering from nothing, no longer grouped 2 at a time
    {
      // need to know if there was a successful tattle now to decide if cower was productive
      // compute # of tattlers
      int tattlesize = 0;
      for (Iterator i=actions.iterator(); i.hasNext(); ) {
	SissyfightGamePlayer p = (SissyfightGamePlayer)i.next();
	if (p.action.equals("tattle")) tattlesize++;
      }
      boolean successfulTattle = (tattlesize==1);

      Stack cowerers = new Stack();
      // make the list
      for (Enumeration e = actions.elements(); e.hasMoreElements(); ) {
        SissyfightGamePlayer act = (SissyfightGamePlayer)e.nextElement();

	if (act.cowering && ! act.resolved) {
	  cowerers.add(act);
	} else {
	  // anyone who didn't fall in to cowerers category is definitely not SCARED.
	  act.scared = false; 
	}
      }

      // make scenes -- we no longer have 2-person cower scenes

      // cowerers contains only those who were not grabbed or scratched, so to resolve SCARED 
      //   we only need to take successfulTattle into account.

      while (cowerers.size() > 0) {
        SissyfightGamePlayer cow = (SissyfightGamePlayer)cowerers.pop();
	cow.resolved = true;
	if (successfulTattle) { // case 3 or 7: prod. cower due to tattle ('case' refers to eric's notes and changelog)
	  cow.scared = false;
	  narrative.add(new SissyfightGameNarration(
	    1,
	    cow.name+" cowered and looked innocent while the other girls fought.",
	    "coweringFromNothing(s,\""+cow.name+"\")",
	    null
	  ));
	}
	else if (!cow.scared) { // case 2: 1st unprod. cower
	  cow.scared = true;
	  narrative.add(new SissyfightGameNarration(
	    1,
	    cow.name+" cowered for no reason like a scaredy-cat. If she does that again next turn, she's gonna be sorry!",
	    "coweringFromNothing(s,\""+cow.name+"\")",
	    null
	  ));
	}
	else { // case 5: 2nd unprod. cower
	  cow.scared = false;
	  Hashtable damage = new Hashtable();
	  damage.put(cow, new Integer(DAMAGE_COWER_SCARED));
	  narrative.add(new SissyfightGameNarration(
	    1,
	    cow.name+" cowered again for no reason! She feels like a little wimp and loses self-esteem.",
	    "coweringFromNothing(s,\""+cow.name+"\")",
	    damage
	  ));
	}
      } // end while
    } // end scene 1


    // scene 26, mutual scratch
    // look for a scratch b, b scratch a, no lollies (must be true if a<->b)
    // and nobody else scratching or grabbing either one
    for (Enumeration e = actions.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer act = (SissyfightGamePlayer)e.nextElement();
      if (act.action.equals("scratch")   		// a scratch b
	  && !act.resolved  
	  && act.target.action != null
	  && act.target.action.equals("scratch") 	// b scratch a
	  && act.target.target == act			
	  && !act.target.resolved
	  && act.scratchers.size() == 1			// nobody else 
	  && act.target.scratchers.size() == 1    	//   scratch
	  && act.grabbers.size() == 0			//   or grab
	  && act.target.grabbers.size() == 0 		//   either one
	 ) {

	// if all that's satisfied, we have a mutual scratch.
	act.resolved = true;
	act.target.resolved = true;

	// make a scene
	Hashtable damage = new Hashtable();
	damage.put(act, new Integer(DAMAGE_SCRATCH));
	damage.put(act.target, new Integer(DAMAGE_SCRATCH));
	narrative.add(new SissyfightGameNarration(
          26,
	  act.name+" and "+act.target.name+" scratched each other.",
	  "mutual(s,\"scratch\",\""+act.name+"\",\""+act.target.name+"\")",
	  damage 
	));
      } // end if
    } // end for -- end scene 26


    // scenes 2,3,6,7,9,10,11,12,13 -- scratch and/or grab with
    // optional lolly
    // 
    // locate players who have been scratched or grabbed
    for (Enumeration e = actions.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer pl = (SissyfightGamePlayer)e.nextElement();
      if (pl.scratchers.size()>0 || pl.grabbers.size()>0) {
	// no other prior scene finders would have resolved
	// the scratch/grab actions that are listed in scratchers/grabbers
	// so there's no more checking to do.

	// resolve all the scratchers & grabbers 
	// and remove any that are already resolved
	// (probably can't happen, but to be safe...)
	for (Iterator s = pl.scratchers.iterator(); s.hasNext(); ) {
	  SissyfightGamePlayer t=(SissyfightGamePlayer)s.next();
	  if (t.resolved) s.remove();
	  else t.resolved = true;
	}
	for (Iterator g = pl.grabbers.iterator(); g.hasNext(); ) {
	  SissyfightGamePlayer t=(SissyfightGamePlayer)g.next();
	  if (t.resolved) g.remove();
	  else t.resolved = true;
	}
	// just in case all the grabbers and scratchers got deleted:
	if (pl.grabbers.size()==0 && pl.scratchers.size()==0) {
	  continue;
	}

	// if the player had a lolly, it's gone now
	pl.lostlolly = true;

	// divide scenes up based on scratch/grab/lick
	if (pl.licking) {
	  // lolly cases 
	  // being scratched/grabbed caused me to lose my lolly

	  // Lolly cases don't need to worry about the pl.interrupted
	  // field, since we know it was a lick that was interrupted.
	  // Stage 1 lick doesn't get resolved on grab/scratch so resolve here:
	  pl.resolved = true;

	  if (pl.grabbers.size()==0) {
	    // lolly+scratch case
	    int scene;
	    if (pl.scratchers.size()>1) scene = 12;
	    else scene = 11;
	    Hashtable damage = new Hashtable();
	    damage.put(pl, new Integer(DAMAGE_LOLLY_SCRATCH * pl.scratchers.size()));
	    narrative.add(new SissyfightGameNarration(
              scene,
	      join(" and ", pl.scratchers) + " scratched " + pl.name
		+ " and she choked on her lollipop, suffering extra humiliation.",
	      "multiscratchgrab(s,"+LR()+",\"scratchlolly\",\""+pl.name+"\","
		+ "\"" + join("\",\"", pl.scratchers) + "\")",
	      damage 
	    ));
	  }
	  else if (pl.scratchers.size()==0) {
	    // lolly+grab case
	    int scene;
	    if (pl.grabbers.size()>1) scene = 7;
	    else scene = 6;
	    Hashtable damage = new Hashtable();
	    damage.put(pl, new Integer(DAMAGE_GRAB * (pl.grabbers.size()-1)));
	    narrative.add(new SissyfightGameNarration(
              scene,
	      join(" and ", pl.grabbers) + " grabbed " + pl.name
		+ " and she couldn't lick her lollipop.",
	      "multiscratchgrab(s,"+LR()+",\"grablolly\",\""+pl.name+"\","
		+ "\"" + join("\",\"", pl.grabbers) + "\")",
	      damage 
	    ));
	  }
	  else {
	    // lolly+scratch+grab case
	    int scene;
	    scene = 13;
	    Hashtable damage = new Hashtable();
	    damage.put(pl, new Integer(
	      DAMAGE_GRAB_SCRATCH * pl.scratchers.size() 
	      + DAMAGE_GRAB * (pl.grabbers.size()-1)));
	    narrative.add(new SissyfightGameNarration(
              scene,
	      join(" and ", pl.grabbers) + " grabbed " + pl.name
		+ " and " + join(" and ", pl.scratchers) + " scratched her."
		+ " She never got to lick her lollipop.",
	      "scratchAndGrab(s,"+LR()+",\"lolly\",\""+pl.name+"\","
		+ "\"scratch\",\"" + join("\",\"", pl.scratchers) + "\","
		+ "\"grab\",\"" + join("\",\"", pl.grabbers) + "\")",
	      damage
	    ));
	  }
	}
	else {
	  // non-lolly cases

	  if (pl.grabbers.size()==0) {
	    // scratch case
	    int scene;
	    if (pl.scratchers.size()>1) scene = 9;
	    else scene = 8; // not that scene # actually matters!
	    Hashtable damage = new Hashtable();
	    damage.put(pl, new Integer(DAMAGE_SCRATCH * pl.scratchers.size()));
	    narrative.add(new SissyfightGameNarration(
              scene,
	      join(" and ", pl.scratchers) + " scratched " + pl.name + ".",
	      "multiscratchgrab(s,"+LR()+",\"scratch\",\""+pl.name+"\","
		+ "\"" + join("\",\"", pl.scratchers) + "\")",
	      damage 
	    ));
	  }
	  else if (pl.scratchers.size()==0) {
	    // grab case
	    int scene;
	    if (pl.grabbers.size()>1) scene = 3;
	    else scene = 2;
	    Hashtable damage = new Hashtable();
	    damage.put(pl, new Integer(DAMAGE_GRAB * (pl.grabbers.size()-1)));
	    String caption;
	    if (pl.interrupted == null) {
	      caption = join(" and ", pl.grabbers) + " grabbed " + pl.name+".";
	    } else {
	      caption = pl.name+" was going to "+pl.interrupted+" but "
		+ join(" and ", pl.grabbers) + " grabbed " + pl.name+".";
	    }
	    narrative.add(new SissyfightGameNarration(
              scene,
	      caption,
	      "multiscratchgrab(s,"+LR()+",\"grab\",\""+pl.name+"\","
		+ "\"" + join("\",\"", pl.grabbers) + "\")",
	      damage 
	    ));
	  }
	  else {
	    // scratch+grab case
	    int scene;
	    scene = 10;
	    Hashtable damage = new Hashtable();
	    damage.put(pl, new Integer(
	      DAMAGE_GRAB_SCRATCH * pl.scratchers.size() 
	      + DAMAGE_GRAB * (pl.grabbers.size()-1)));
	    String caption;
	    if (pl.interrupted == null) {
	      caption = join(" and ", pl.grabbers) + " grabbed " + pl.name
		+ " and " + join(" and ", pl.scratchers) + " scratched her, causing extra humiliation.";
	    } else {
	      caption = pl.name+" was going to "+pl.interrupted+" but "
	        + join(" and ", pl.grabbers) + " grabbed " + pl.name
		+ " and " + join(" and ", pl.scratchers) + " scratched her.";
	    }
	    narrative.add(new SissyfightGameNarration(
              scene,
	      caption,
	      "scratchAndGrab(s,"+LR()+",0,\""+pl.name+"\","
		+ "\"scratch\",\"" + join("\",\"", pl.scratchers) + "\","
		+ "\"grab\",\"" + join("\",\"", pl.grabbers) + "\")",
	      damage
	    ));
	  }
	}

      }
      
    } // end for -- end scene 2-13


    // scene 14, mutual (failed) tease
    // -- a tease b and b tease a with no other teases on a or b.
    for (Enumeration e = actions.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer pl = (SissyfightGamePlayer)e.nextElement();
    
      // check that conditions hold
      if (!pl.action.equals("tease")) continue;
      if (pl.teasers.size() != 1) continue;
      if (pl.resolved) continue;
      SissyfightGamePlayer pl2 = pl.target;
      if (pl2.target!=pl) continue;
      if (!pl2.action.equals("tease")) continue;
      if (pl2.teasers.size() != 1) continue;
      if (pl2.resolved) continue;

      // ok, resolve both the participants
      pl.resolved = true;
      pl2.resolved = true;

      narrative.add(new SissyfightGameNarration(
	14,
	pl.name+" and "+pl2.name+" teased each other. But nobody else joined in, so it didn't work.",
	"mutual(s,\"tease\",\""+pl.name+"\",\""+pl2.name+"\")",
	null // no damage done
      ));

    } // end for -- end scene 14




    // scene 15, failed tease (only one teaser)
    // scene 16, successful tease (more than one teaser)
    for (Enumeration e = actions.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer pl = (SissyfightGamePlayer)e.nextElement();

      // check that conditions hold: skip if player not teased
      if (pl.teasers.size() < 1) continue;
      // remove any teases that are already resolved -- end if none left
      // (and resolve the others while you're at it)
      for (Iterator i = pl.teasers.iterator(); i.hasNext(); ) {
	SissyfightGamePlayer t = (SissyfightGamePlayer)i.next();
        if (t.resolved) i.remove();
	else t.resolved = true;
      }
      if (pl.teasers.size()==0) continue;

      String caption;
      int scene;
      Hashtable damage = null;

      caption = join(" and ", pl.teasers);
      caption = caption + " teased " + pl.name + ".";

      if (pl.teasers.size()==1) {
	// 1 teaser: tease failed
        scene = 15;
	caption = caption + " But nobody else joined in, so it didn't work.";
      }

      else {
        // > 1 teaser: tease suceeded.
	scene = 16;
	damage = new Hashtable();
	damage.put(pl,
	  new Integer(DAMAGE_TEASE_ADD+DAMAGE_TEASE_MULT*pl.teasers.size())
	);
      }

      narrative.add(new SissyfightGameNarration(
	scene,
	caption,
	"multiscratchgrab(s,"+LR()+",\"tease\",\""+pl.name+"\","
	  + "\"" + join("\",\"", pl.teasers) + "\")",
	damage
      ));

    } // end for -- end scene 15/16



    // scene 17 successful tattle (1 tattler)
    // scene 18 failed tattle (>1 tattlers)
    {
      Vector tattlers, sufferers, lickers, cowerers;
      Hashtable damage;
      String text, code;
      tattlers = new Vector(); //tattlers vector now passed on from Cower handler
      sufferers = new Vector();
      lickers = new Vector();
      cowerers = new Vector();
      damage = new Hashtable();
      // not checking resolved because nothing stops a tattle

      for (Iterator i=actions.iterator(); i.hasNext(); ) {
	SissyfightGamePlayer p = (SissyfightGamePlayer)i.next();
	if (p.action.equals("tattle")) tattlers.add(p);
	else if (p.action.equals("lick") && !p.lostlolly) lickers.add(p);
	else if (p.action.equals("cower")) cowerers.add(p);
	else sufferers.add(p);
      }
      if (tattlers.size() == 1) {
	// scene 17 successful tattle

        for (Iterator i=sufferers.iterator(); i.hasNext(); ) {
	  SissyfightGamePlayer p = (SissyfightGamePlayer)i.next();
	  damage.put(p,new Integer(DAMAGE_TATTLE_SUCC));
	}
	text = join("",tattlers)+" tattled! ";
	code = "successfulTattle(s,"+LR()+","+joinq("",tattlers);
	if (sufferers.size()>0){
	  text = text + join(" and ",sufferers) + " got in trouble. ";
	  code = code + ","+joinq(",",sufferers);
	}

	Vector lick_cowerers = new Vector(lickers);
	lick_cowerers.addAll(cowerers);
	if (lick_cowerers.size()>0) {
	  text = text + join(" and ", lick_cowerers) + " looked innocent.";
	}
	if (lickers.size()>0) {
	  code = code + ",\"lolly\"," + joinq(",", lickers);
	}
	if (cowerers.size()>0) {
	  code = code + ",\"cower\"," + joinq(",", cowerers);
	}
	code = code + ")";

	narrative.add(new SissyfightGameNarration(
	  17,
	  text,
	  code,
	  damage
	));
      }

      else if (tattlers.size() > 1) {
	// failed tattle - the tattlers suffer
	text = join(" and ",tattlers)+" all tattled and got themselves in trouble!";
	code = "multipleTattle(s,"+LR()+","+joinq(",",tattlers)+")";
        for (Iterator i=tattlers.iterator(); i.hasNext(); ) {
	  SissyfightGamePlayer p = (SissyfightGamePlayer)i.next();
	  damage.put(p,new Integer(DAMAGE_TATTLE_FAIL));
	}

	narrative.add(new SissyfightGameNarration(
	  18,
	  text,
	  code,
	  damage
	));
      }
    } // end scene 17/18


    // scene 19 lick lolly
    {
      Vector lickers = new Vector();
      Hashtable damage = new Hashtable();
      // make a list o' lickers
      for (Iterator i=actions.iterator(); i.hasNext(); ) {
	SissyfightGamePlayer p = (SissyfightGamePlayer)i.next();
	if (p.action.equals("lick") && !p.resolved && !p.lostlolly) {
	  lickers.add(p);
	  damage.put(p,new Integer(DAMAGE_LOLLY));
	}
      }
      if (lickers.size()>0) {
        String text,code;
        if (lickers.size()==1) {
          text = join("",lickers)+" licked her lollipop and felt better.";
        } else {
          text = join(" and ",lickers)+" all licked their lollipops and felt better.";
        }
        code = "lollyLick(s,"+LR()+","+joinq(",",lickers)+")";
        narrative.add(new SissyfightGameNarration(
          19,
          text,
          code,
          damage
        ));
      }

    } // end scene 19


    // scene 22 idle, 27 leave -- one scene per idler/ leaver
    for (Iterator i=actions.iterator(); i.hasNext(); ) {
      SissyfightGamePlayer p = (SissyfightGamePlayer)i.next();

      if (p.action.equals("idle")) {

        Hashtable damage = new Hashtable();
        damage.put(p,new Integer(DAMAGE_IDLE));

        narrative.add(new SissyfightGameNarration(
	  22,
	  p.name+" couldn't make up her mind what to do.  How humiliating!",
	  "doingNothing(s," +LR()+"," + Q(p.name) + ")",
	  damage
        ));
      
      }
      else if (p.action.equals("leave")) {
	narrative.add(new SissyfightGameNarration(
	  27,
	  p.name+" ran away like a big loser.  No brownie points for "+p.name+"!",
	  "leaveGame(s,"+LR()+","+Q(p.name)+")",
	  null
	));
      }

    } // end for -- end scene 22






    // Count up total damage and find out who's out of the game 
    // scene 20: humiliation

    {
      // make temporary copy of narrative so adding new scenes to end
      // doesn't mess us up
      Vector temp_nar = new Vector(narrative);
      for (Iterator i=temp_nar.iterator(); i.hasNext(); ) {
        SissyfightGameNarration sgn = (SissyfightGameNarration)i.next();
  
        if (sgn.damage != null) {
	  for (Iterator j=sgn.damage.keySet().iterator(); j.hasNext(); ) {
	    SissyfightGamePlayer p = (SissyfightGamePlayer)j.next();
	    // no need to go on if player's already humiliated
	    if (p.health==0) continue;
	    int d = ((Integer)sgn.damage.get(p)).intValue();
    
	    p.health -= d;
	    if (p.health<0) p.health=0;
	    if (p.health>MAX_HEALTH) p.health=MAX_HEALTH;
    
	    // if newly humiliated, create the scene
	    Hashtable damage = new Hashtable();
	    damage.put(p,new Integer(0)); // this is really just to encode who died
	    if (p.health==0) {
	      // choose a random humiliation text
    	      Random rtext = new Random();
    	      String humtext = HUMILIATION_TEXTS[Math.abs(rtext.nextInt()) % HUMILIATION_TEXTS.length]; 
              narrative.add(new SissyfightGameNarration(
	        20,
	        p.name+humtext,
	        "humiliated(s,"+LR()+","+Q(p.name)+")",
		damage
	      ));
	    }
    
          } // end for each player in damage
	} // end if damage not null
      }
    } // end detect humiliation/scene 20



    // end of turn/end of game
    {
      String text,code;
      Vector survivors = new Vector();
      Vector losers = new Vector();
      Vector all = new Vector();

      // remove humiliated players from the survivors vector and add them to losers vector
      for (Enumeration i=players.elements(); i.hasMoreElements(); ) {
        SissyfightGamePlayer p = (SissyfightGamePlayer)i.nextElement();

	if (p.zombie || p.health == 0) { // #### zombie check appropriate here?
	  losers.add(p);
	}
	else {
	  survivors.add(p);
	}
	all.add(p);
      }

      text = join(" and ", survivors);
      code = "endOfTurn(s,"+LR()+",";

      if (survivors.size() >= MIN_PLAYERS) {
        // game's not over yet
	text = text + " thought about what to do next.";

	// to show status of ALL players, use the original players hash
        for (Iterator i=players.values().iterator(); i.hasNext(); ) {
          SissyfightGamePlayer p = (SissyfightGamePlayer)i.next();
	  code = code + Q(p.name)+",";
	  if (p.zombie) code = code + Q("zombie");
	  else if (p.health>0) code = code + Q("ok");
	  else code = code + Q("humiliated");
	  if (i.hasNext()) code = code+",";
	}
	code = code+")";

	narrative.add(new SissyfightGameNarration(
	  21,
	  text,
	  code,
	  null
	));
      }

      else {
	// < MIN_PLAYERS: game's over
	int scene;

	// to show status of ALL players, use the original players hash
        for (Iterator i=players.values().iterator(); i.hasNext(); ) {
          SissyfightGamePlayer p = (SissyfightGamePlayer)i.next();
	  code = code + Q(p.name)+",";
	  if (p.health>0) code = code + Q("winner");
	  else if (p.zombie) code = code + Q("zombie");
	  else code = code + Q("humiliated");
	  if (i.hasNext()) code = code+",";
	}
	code = code+")";

	// start computing scores
	Vector nonZom = nonZombies(all);
	Vector zom = zombies(all);


	// start new scoring midnight sunday morning may 28
	Calendar today = Calendar.getInstance();
	Calendar satnite = Calendar.getInstance();
	satnite.clear();
	satnite.set(2000,Calendar.MAY,28,0,0);
	boolean ladder_point_system = today.after(satnite); //#### TEMP
	//ladder_point_system = true;

	// old point system or new?
	if (ladder_point_system) {
	  // new system
	  /* sample text from Eric:
	  Abbie and Betty became best friends and won the game!  Abbie earned 45
	  points and Betty earned 35 points for defeating 2 girls.  (You get more
	  points if you defeat girls with higher rankings.)  Cattie and Dolly
	  earned 5 points just for playing. 
  
	  [we might include the parenthetical only if there were 2 winners getting
	  different amounts]
	  */

	  // first, the easy stuff:
	  addPoints(zom, LADDER_PTS_RUN_AWAY); 	// those who left
	  addPoints(nonZom, LADDER_PTS_PLAY);	// those who stayed, whether won or lost

	  // now, for each winner, sum up the points per loser:

	  // no survivors = no problem
	  if (survivors.size()==0) {
            text = "Everybody was humiliated and nobody won!\r";
	    scene = 23;
	    return_val = 1;
	    if (nonZom.size()>0) {
	      text = text + join(" and ", nonZom) + " got "+LADDER_PTS_PLAY+" points just for playing, though.\r" ;
	    }
	  }
	  else if (survivors.size()==1) {
	    int winPts = computeLadderPoints((SissyfightGamePlayer)survivors.get(0), losers);
	    addPoints((SissyfightGamePlayer)survivors.get(0), winPts);
	    text = text + " won the game all by herself!\r"
	         + "She earned " + winPts + " brownie points for defeating " + losers.size() + " girls.\r\r";
	    scene = 24;
	    return_val = 2;
	    if (nonZom.size()>0) {
	      text = text + join(" and ", nonZom) + " got "+LADDER_PTS_PLAY+" points just for playing.\r" ;
	    }
	  } else { //survivors.size()==2
	    int winPts0 = computeLadderPoints((SissyfightGamePlayer)survivors.get(0), losers);
	    int winPts1 = computeLadderPoints((SissyfightGamePlayer)survivors.get(1), losers);
	    addPoints((SissyfightGamePlayer)survivors.get(0), winPts0);
	    addPoints((SissyfightGamePlayer)survivors.get(1), winPts1);

            text = text + " became best friends and won the game! ";
	    if (winPts0 == winPts1) {
	      text=text+"They each earned " + winPts1 + " points for defeating " + losers.size();
	      if (losers.size() > 1) text = text + " girls.\r\r";
	      else text = text + " girl.\r\r";
	    } else {
	      text=text+((SissyfightGamePlayer)survivors.get(0)).name;
	      text=text+" earned " + winPts0 + " points and ";
	      text=text+((SissyfightGamePlayer)survivors.get(1)).name;
	      text=text+", " + winPts1 + " for defeating " + losers.size();
	      //text=text+" earned " + winPts1 + " points for defeating " + losers.size();
	      if (losers.size() > 1) text = text + " girls. ";
	      else text = text + " girl. ";
	      //text=text+"(You get more points when you defeat girls with higher rankings than you.) ";
		text=text+"\r\r";
	    }
  
	    scene = 25;
	    return_val = 2;
	    if (nonZom.size()>0) {
	      text = text + join(" and ", nonZom) + " got "+LADDER_PTS_PLAY+" points just for playing.\r" ;
	    }
	  } // end survivors.size()==2
	}
	else {
	  // old point system
	 
	  // how many points did winners receive? (not counting the 10 for playing)
	  int winPts = losers.size()*BROWNIE_PTS_DEFEAT;

	  addPoints(zom, BROWNIE_PTS_RUN_AWAY);
	  addPoints(nonZom, BROWNIE_PTS_PLAY);
	  addPoints(survivors, losers.size()*BROWNIE_PTS_DEFEAT);

	  if (survivors.size()==0) {
            text = "Everybody was humiliated and nobody won!\r";
	    scene = 23;
	    return_val = 1;
	    if (nonZom.size()>0) {
	      text = text + join(" and ", nonZom) + " got "+BROWNIE_PTS_PLAY+" points just for playing, though.\r" ;
	    }
	  }
	  else if (survivors.size()==1) {
	    text = text + " won the game all by herself!\r"
	         + "She earned " + winPts + " brownie points for defeating " + losers.size() + " girls.\r\r";
	    scene = 24;
	    return_val = 2;
	    if (nonZom.size()>0) {
	      text = text + join(" and ", nonZom) + " got "+BROWNIE_PTS_PLAY+" points just for playing.\r" ;
	    }
	  } else {
            text = text + " won the game!\r"
		        + "They became best friends and earned " + winPts + " brownie points each for defeating " + losers.size();
	    if (losers.size() > 1) text = text + " girls.\r\r";
	    else text = text + " girl.\r\r";
  
	    scene = 25;
	    return_val = 2;
	    if (nonZom.size()>0) {
	      text = text + join(" and ", nonZom) + " got "+BROWNIE_PTS_PLAY+" points just for playing.\r" ;
	    }
	  }
  
  
	} // end if ladder_point_system

	narrative.add(new SissyfightGameNarration(
	  scene,
	  text,
	  code,
	  null
	));
      }

    } // end end of turn/end of game




    // TEMPORARY dump narrative
    for (Iterator i=narrative.iterator(); i.hasNext(); ) {
      SissyfightGameNarration sgn = (SissyfightGameNarration)i.next();
      
      if (sgn.scene==20) { // humilation
	// the humiliated character is the first(only) one in the damage hash
	if (sgn.damage!=null) {
	  Iterator z = sgn.damage.keySet().iterator();
	  if (z.hasNext()) {
	    announce("died",((SissyfightGamePlayer)z.next()).name,"");
	  }
	}
      }

      // end of turn/end of game use different messages
      // to set status of background player images
      if (sgn.scene==21) {
	announce("eot_results",sgn.caption,sgn.code);
      }
      else if (sgn.scene==23 || sgn.scene==24 || sgn.scene==25) {
	announce("eog_results",sgn.caption,sgn.code);
      }
      else {
        announce("results",sgn.caption+((char)13)+player_damage(sgn.damage),sgn.code);
      }
    }


    announce("status",player_status(players), "");

    return return_val;



  } // end resolveTurn


  /** add points to players' score in scores hashtable */
  void addPoints(Vector who, int howmuch) {
    for (Enumeration e = who.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer p = (SissyfightGamePlayer)e.nextElement();

      addPoints(p, howmuch);
    }
  }

  void addPoints(SissyfightGamePlayer p, int howmuch) {
    Integer s;
    if ((s=(Integer)scores.get(p.name)) == null) {
      scores.put(p.name, new Integer(howmuch));
    }
    else {
      scores.put(p.name, new Integer(howmuch+s.intValue()));
    }
  }

  /** compare the pregame scores of winner and loser; return 1 if loser has higher score,
      -1 if loser has lower score, and 0 if loser has same score as winner. */
  int comparePoints(SissyfightGamePlayer winner, SissyfightGamePlayer loser) {
    int ws, ls;
    Integer wi, li;

    wi = (Integer)pregame_scores.get(winner.name);
    if (wi==null) ws = 0;
    else ws = wi.intValue();

    li = (Integer)pregame_scores.get(loser.name);
    if (li==null) ls = 0;
    else ls = li.intValue();

    if (ls > ws) return 1;
    if (ls < ws) return -1;
    return 0;
  }

  /** total up ladder-system points for given player who defeated given vector of players */
  int computeLadderPoints(SissyfightGamePlayer winner, Vector losers) {
    int points = 0;

    for (Enumeration e=losers.elements(); e.hasMoreElements();) {
      SissyfightGamePlayer loser = (SissyfightGamePlayer)e.nextElement();
      if (comparePoints(winner, loser) == -1) {
	points += LADDER_PTS_DEFEAT_LOWER;
      } else {
	points += LADDER_PTS_DEFEAT_HIGHER;
      }
    }

    return points;
  }

  /** return scores hashtable which maps nicknames to scores */
  public Hashtable getScores() {
    return scores;
  }



  String player_status(Hashtable players) {
    String scrapStatus = "";

    for (Enumeration e = players.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      scrapStatus = scrapStatus + n.name+","+n.health+","+n.lollies+","+n.tattles;
      if (e.hasMoreElements()) scrapStatus = scrapStatus + ",";
    }
    return scrapStatus;

  }

  String player_damage(Hashtable damage) {
    String scrapDamage = "";

    if (damage == null) return "";

    for (Enumeration e = damage.keys(); e.hasMoreElements(); ) {
      SissyfightGamePlayer p = (SissyfightGamePlayer)e.nextElement();
      String name = p.name;
      int hurt = ((Integer)damage.get(p)).intValue();
      if (hurt!=0) scrapDamage = scrapDamage + name+","+hurt+",";
    }
    if (scrapDamage.length() > 0) 
      return scrapDamage.substring(0,scrapDamage.length() - 1);
    else
      return "";
      
  }





  // UTILITY FUNCTIONS


  /** randomly return L or R */
  String LR() {
    Random rnd = new Random();
    if (rnd.nextInt()%2==1) {
      return "\"R\"";  
    }
    else {
      return "\"L\"";
    }
  }

  /** similar to perl's join(), for player names */
  String join(String con, Vector players) {
    String ret = "";
    for (Enumeration e = players.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      ret = ret + n.name;
      if (e.hasMoreElements()) {
	ret = ret + con;
      }
    }
    return ret;
  }

  /** as above, but puts quotes around player names */
  String joinq(String con, Vector players) {
    String ret = "";
    for (Enumeration e = players.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      ret = ret + Q(n.name);
      if (e.hasMoreElements()) {
	ret = ret + con;
      }
    }
    return ret;
  }

  /** Q() - put quotes around a string */
  String Q(String a) {
    return "\""+a+"\"";
  }



  /** true if all live players in given hash are ready */
  boolean checkReady(Hashtable v) {
    for (Enumeration e = v.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      if (n.health > 0 && !n.zombie && !n.ready) return false;
    }
    return true;
  }

  /** set all ready flags false in given hash of players */
  void resetReady(Hashtable v) {
    for (Enumeration e = v.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      n.ready = false;
    }
  }


  /** true if all live players in given hash have acted */
  boolean checkActed(Hashtable v) {
    for (Enumeration e = v.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      if (n.health > 0 && !n.zombie && n.action == null) return false;
    }
    return true;
  }

  /** set all action vars false in given hash of players */
  void resetActed(Hashtable v) {
    for (Enumeration e = v.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      n.action = null;
    }
  }

  /** set all action vars true in given hash of players */
  void setActed(Hashtable v) {
    for (Enumeration e = v.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      if (n.action == null) n.action="idle";
    }
  }

  /** true if all players in given hash are zombies */
  boolean checkZombies(Hashtable v) {
    for (Enumeration e = v.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      if (!n.zombie) return false;
    }
    return true;
  }

  /** return hash of given players who aren't zombies */
  Hashtable nonZombies(Hashtable v) {
    Hashtable nonzom = new Hashtable();
    for (Enumeration e = v.keys(); e.hasMoreElements(); ) {
      String name = (String)e.nextElement();
      SissyfightGamePlayer n = (SissyfightGamePlayer)v.get(name);
      if (n != null && !n.zombie) nonzom.put(name, n);
    }
    return nonzom;
  }
  Vector nonZombies(Vector v) {
    Vector nonzom = new Vector();
    for (Enumeration e = v.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      if (n != null && !n.zombie) nonzom.add(n);
    }
    return nonzom;
  }
  /** return vector of only the zombies */
  Vector zombies(Vector v) {
    Vector zom = new Vector();
    for (Enumeration e = v.elements(); e.hasMoreElements(); ) {
      SissyfightGamePlayer n = (SissyfightGamePlayer)e.nextElement();
      if (n != null && n.zombie) zom.add(n);
    }
    return zom;
  }


  /** queue message for broadcast */
  void announce(String s0, String s1, String s2) {
    /*
    String announcement[] = new String[3];
    announcement[0] = s0;
    announcement[1] = s1;
    announcement[2] = s2;
    announcements.add(announcement);
    */
    // #### TEMP DANGER NASTY check for threadsafety; probably better to queue
    room.broadcast(s0, s1, s2);
String s1dump = s1.replace((char)13, ':');
WordUtils.dbg("Just announced: "+s0+"///"+s1dump+"///"+s2); // #####
  }



}



/** A SissyfightGamePlayer is just the data structure for
    a single player in a game, keeping track of name,
    health, tattles, lollies, actions, etc.

    Implements Comparator so that players can be sorted by
    Action in the official action priority order.  
    (Not "consistent with equals" since two different players
    will compare equal if they are doing the same type of action.)
*/
class SissyfightGamePlayer implements Comparator {
  public String name;
  public int health;
  public int lollies, tattles;
  public String action, targetName;
  public SissyfightGamePlayer target;
  public boolean ready;
  public boolean zombie; // true if player left a game in progress
  public boolean scared; // true if player cowered last turn and was not grabbed, scratched, or tattled on.

  // turn-specific variables (reset at start of each turn)
  public boolean cowering, scratched, grabbed, idling, licking, lostlolly, tattling, resolved;
  public Vector scratchers, grabbers, teasers;
  public String interrupted;

  /**
    Constructor initializes default health=10, lollies=3, tattles=2
  */
  public SissyfightGamePlayer(String myName) {
    name = myName;
    health = SissyfightGame.MAX_HEALTH;
    lollies = SissyfightGame.MAX_LOLLIES;
    tattles = SissyfightGame.MAX_TATTLES;
    ready = false;
    zombie = false;
    scared = false;
  }
  /**
    Constructor for use as a comparator
  */
  public SissyfightGamePlayer() {
    initActionOrderHash();
  }

  /** clear all the turn-specific variables */
  public void resetTurn() {
    cowering = false;
    scratched = false;
    grabbed = false;
    idling = false;
    licking = false;
    lostlolly = false;
    tattling = false;
    resolved = false;
    scratchers = new Vector();
    grabbers = new Vector();
    teasers = new Vector();
    interrupted = null;

    // SCARED is not reset automatically, because it persists across turns.
  }

  // COMPARATOR functionality
  /** static hashtable actionOrderHash caches a hashtable mapping action names to integers */
  static Hashtable actionOrderHash = null;

  /** compare() implements the Comparator interface for sorting players
    by their action type. */
  public int compare(Object o1, Object o2) {
    SissyfightGamePlayer p1, p2;
    Integer i1, i2;

    p1 = (SissyfightGamePlayer)o1;
    p2 = (SissyfightGamePlayer)o2;

    // get the integer sort values for each action
    if (p1.action == null || actionOrderHash.get(p1.action) == null)
      i1 = new Integer(Integer.MAX_VALUE);
    else
      i1 = (Integer)actionOrderHash.get(p1.action);

    if (p2.action == null || actionOrderHash.get(p2.action) == null)
      i2 = new Integer(Integer.MAX_VALUE);
    else
      i2 = (Integer)actionOrderHash.get(p2.action);

    // compare using the Integers' Natural Order
    return i1.compareTo(i2);
  }


  /** initialize the action sorting hash */
  static synchronized void initActionOrderHash() {
    // synchronized so that if two threads try to init at the same
    // time, at least they won't clobber each other.  
    int i;
    if (actionOrderHash != null) return;
    actionOrderHash = new Hashtable();
    for (i=0; i<SissyfightGame.ACTION_ORDER.length-1; i+=2) {
      actionOrderHash.put(SissyfightGame.ACTION_ORDER[i], SissyfightGame.ACTION_ORDER[i+1]); 
    }
  }
}





/** A SissyfightGameNarration is a single entry in the narration
    table for a turn: for each scene, contains a scene number (not used),
    a caption, lingo code to build the scene, and a hashtable 
    mapping players to the amount of damage they took in that scene.
    (Damage hash may be null if no damage was done.)
    */
class SissyfightGameNarration {
  public int scene;
  public String caption;
  public String code;
  public Hashtable damage;

  public SissyfightGameNarration (int sce, String cap, String cod, Hashtable dam) {
    scene = sce;
    caption = cap;
    code = cod;
    damage = dam;
  }
}