/*
  A Free Software implementation of the Set card game
  Copyright 1998, 1999, 2002, 2003
  David M. Turner <novalis atsign gnu.org>
  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.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  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 

  Thanks to Gary Wong <gtw atsign gnu.org> for curves and Regyt <regyt
  atsign omniheurist.net> and many more for playtesting.  Cassia (too
  lazy to lool up email address) suggested single player mode.  Janet
  Casey suggested the configurator. I forget who suggested other
  things, but I thank them anyway.

  Version: 2

  Todo: 
  Variable stripe len
  Doco for config
  Right-click unselects

*/

import java.lang.System;
import java.util.Vector;
import java.util.ListIterator;
import java.util.TreeMap;
import java.awt. *;
import java.awt.event. *;
import java.applet.Applet;
import java.net.*;

public class Set extends Applet implements MouseListener, KeyListener {
    Panel center;

    Button players [];
    Button cheat, help, restart, config;

    Label lblscore [];
    Label lbltime [];

    public static final int NUM_PLAYERS = 5;

    int activePlayer = -1; //index of last player to click a 

    Vector deck;
    Vector onTable;

    int score [];
    long times [];

    SetCard card1, card2, card3;

    static final SetCard nullCard = new SetCard (-100, -100, -100, -100);

    long turnStart, pauseStart;
    Configurator configurator = null;

    Label status = new Label ("Select a player to begin. Released under the "+ 
			      "GNU General Public License version 2 or, " + 
			      "at your option, any later version. ");

    Label cardsLeft;

    DeselectTimer deselectTimer;

    boolean gameover = false;
    boolean buzzed = false;

    Statistics stats;

    public void init () {

	score = new int [NUM_PLAYERS];
	lblscore = new Label [NUM_PLAYERS];
	lbltime = new Label [NUM_PLAYERS];
	times = new long [NUM_PLAYERS];
	players = new Button [NUM_PLAYERS];

	stats = new Statistics ();

	initBoard ();
   
	addKeyListener (this);
	requestFocus ();

	String arg, base = getDocumentBase ().toString ();
	int qidx;
	if ((qidx = base.indexOf ('?')) != -1)
	    arg = base.substring (qidx + 1);
	else 
	    arg = "background=ffffff&selected=c0c0c0&color0=ff0000&color1=00bf00&color2=0000ff&single=N";
	try {
	    Config.main = new Config (arg);
	} catch (RuntimeException e) {
	    Config.main = new Config ("background=ffffff&selected=c0c0c0&color0=ff0000&color1=00bf00&color2=0000ff&single=N");
	}

	deselectTimer = new DeselectTimer (this);
	deselectTimer.start ();

	startGame ();
    }

    class DeselectTimer extends Thread {
	long startTime;
	Set game;

	//this feature protects against slow delayed class loading
	//causing timeouts between the final card click and the stat gathering 
	//and card removal
	boolean paused = false;

	DeselectTimer (Set g) {
	    click ();
	    game = g;
	}

	public void click () {
	    startTime = System.currentTimeMillis ();
	}

	public void run () {
	    while (true) {
		yield ();
		if (System.currentTimeMillis () - startTime > 5000) {
		    click ();
		    if (paused) continue;
		    for (int i = 0; i < center.getComponentCount (); i++) {
			SetCard card = (SetCard) center.getComponent (i);
			card.setSelected (false);
		    }
		    game.card1 = nullCard;
		    game.card2 = nullCard;
		    game.card3 = nullCard;
		}
	    }	  

	}
    }

    private void initBoard () {

	setLayout (new BorderLayout ());

	Panel top = new Panel (new GridLayout (0, NUM_PLAYERS));
	center = new Panel (new GridLayout (0, 6));
	GridBagLayout gridbag = new GridBagLayout ();
	Panel bottom = new Panel (gridbag);
	add ("North", top);
	add ("Center", center);
	add ("South", bottom);

	//player scores and buttons

	for (int i = 0; i < NUM_PLAYERS; i++) {
	    players [i] = new Button ("Player " + (i + 1));
	    top.add (players [i]);
	    players [i].addMouseListener (this);
	}

	//why yes, this does have to be two separate loops, b/c there's no
	//good way to place a component at a specific place in a
	//gridlayout.  I hate AWT.  I hate AWT.  I hate AWT.
	for (int i = 0; i < NUM_PLAYERS; i++) {
	    lblscore [i] = new Label ("0 sets");
	    top.add (lblscore [i]);    
	}
	for (int i = 0; i < NUM_PLAYERS; i++) {
	    lbltime [i] = new Label ("Average Time: 0");
	    top.add (lbltime [i]);
	}

	//bottom buttons

	GridBagConstraints gbc = new GridBagConstraints ();
	gbc.fill = GridBagConstraints.BOTH;
	gbc.weightx = 1;
	bottom.add (status, gbc);

	cheat = makeButton (bottom, "Cheat!");
	help = makeButton (bottom, "Help");
	restart = makeButton (bottom, "Restart");
	config = makeButton (bottom, "Configure");

	gbc.fill = GridBagConstraints.NONE;
	gbc.weightx = 0;    

	cardsLeft = new Label ("   27 sets left");
	bottom.add (cardsLeft, gbc);
    }

    Button makeButton (Container c, String text) {
	GridBagConstraints gbc = new GridBagConstraints ();
	gbc.fill = GridBagConstraints.NONE;
	gbc.weightx = 0;

	Button b = new Button (text);
	c.add (b, gbc);
	b.addMouseListener (this);

	return b;
    }

    //deals a new card off the deck and puts it on the table
    private void dealCard () {

	SetCard deal;
	deal = (SetCard) deck.elementAt (0);
	deck.removeElementAt (0);
	onTable.addElement (deal);
	center.add (deal);
	deal.addMouseListener (this);

    }

    //same, but puts it at a specific place on the table
    private void dealCard (int pos) {

	SetCard deal;
	deal = (SetCard) deck.elementAt (0);
	deck.removeElementAt (0);
	onTable.addElement (deal);
	center.add (deal, pos);
	deal.addMouseListener (this);

    }

    private void shuffle (Vector d) {

	int len = d.size ();
	for (int i = 0; i < len; i++) {
	    Object temp;
	    int j = (int) (Math.random () * len);
	    temp = d.elementAt (j);
	    d.setElementAt (d.elementAt (i), j);
	    d.setElementAt (temp, i);
	}
    } 


    public static boolean isSet (SetCard a, SetCard b, SetCard c) {
	int cl, nb, sd, sp;

	if (a == b || a == c || b == c || a == nullCard || b == nullCard || c
	    == nullCard)
	    return false;

	cl = (a.color + b.color + c.color) % 3;
	nb = (a.number + b.number + c.number) % 3;
	sd = (a.shade + b.shade + c.shade) % 3;
	sp = (a.shape + b.shape + c.shape) % 3;
	if (cl == 0 && nb == 0 && sd == 0 && sp == 0)
	    return true;
	else
	    return false;
    }


    public static String whyNotSet (SetCard a, SetCard b, SetCard c) {
	String out = "there are ";
	boolean and = false;

	if ((a.color + b.color + c.color) % 3 != 0) {
	    and = true;
	    out += 	"two ";
	    if (a.color == b.color || a.color == c.color) {
		out += a.colorName ();
	    } else {
		out += b.colorName ();
	    }
	    out += "s";
	}

	if ((a.number + b.number + c.number) % 3 != 0) {
	    if (and) {
		out += " and ";
	    }
	    and = true;
	    out += 	"two ";
	    if (a.number == b.number || a.number == c.number) {
		out += a.numberName ();
	    } else {
		out += b.numberName ();
	    }
	    out += "s";
	}

	if ((a.shape + b.shape + c.shape) % 3 != 0) {
	    if (and) {
		out += " and ";
	    }
	    and = true;
	    out += 	"two ";
	    if (a.shape == b.shape || a.shape == c.shape) {
		out += a.shapeName ();
	    } else {
		out += b.shapeName ();
	    }
	    out += "s";
	}

	if ((a.shade + b.shade + c.shade) % 3 != 0) {
	    if (and) {
		out += " and ";
	    }
	    and = true;
	    out += 	"two ";
	    if (a.shade == b.shade || a.shade == c.shade) {
		out += a.shadeName ();
	    } else {
		out += b.shadeName ();
	    }
	}



	return out;
    }


    boolean isSetOnTable () {
	int i, j, k;
	for (i = 0; i < onTable.size () - 2; i++) {
	    for (j = i + 1; j < onTable.size () - 1; j++) {
		for (k = j + 1; k < onTable.size (); k++) {
		    if (isSet ((SetCard) onTable.elementAt (i), (SetCard)
			       onTable.elementAt (j), (SetCard)
			       onTable.elementAt (k))) {
			return true;
		    }
		}
	    }
	} 
	return false;
    }

    //Returns true if the board has at least one set on it and 12 or
    //more cards, or the deck is empty.
    public boolean liveboard () {

	if (deck.size () == 0)
	    return true;

	if (onTable.size () < 12)
	    return false;

	return isSetOnTable ();
    
    }

    private void clearcards (SetCard carda, SetCard cardb, SetCard cardc) {

	carda.setSelected (false);
	cardb.setSelected (false);
	cardc.setSelected (false);
    } 

    public void startGame () {

	cardsLeft.setText ("   27 sets left");
	if (Config.main.singlePlayerMode)
	    status.setText ("Select three cards");
	else
	    status.setText ("Select a player.");

	for (int i = 0; i < NUM_PLAYERS; i++) {
	    score [i] = 0;
	    times [i] = 0;
	    lbltime [i].setText ("Average Time: 0.0");
	} 
    
	deck = new Vector ();
	//create the deck
	for (int c = 0; c < 3; c++) {
	    for (int n = 0; n < 3; n++) {
		for (int sp = 0; sp < 3; sp++) {
		    for (int sd = 0; sd < 3; sd++) {
			deck.addElement (new SetCard (c, n, sp, sd));
		    }
		}
	    }
	} 

	center.removeAll ();

	onTable = new Vector ();
	shuffle (deck);

	for (int i = 0; i < 12; i++) {
	    dealCard ();
	}

	while (!liveboard ()) {
	    dealCard ();
	    dealCard ();
	    dealCard ();
	}

	for (int i = 0; i < NUM_PLAYERS; i++) {
	    lblscore [i].setText ("0 sets");
	    lbltime [i].setText ("Average Time: 0");
	}

	card1 = nullCard;
	card2 = nullCard;
	card3 = nullCard;

	validate ();
	gameover = false;
	turnStart = System.currentTimeMillis ();

    }

    private void cheat () {
	SetCard carda, cardb, cardc;

	clearcards (card1, card2, card3);
	card1 = nullCard;
	card2 = nullCard;
	card3 = nullCard;

	for (int i = 0; i < onTable.size () - 2; i++) {
	    for (int j = i + 1; j < onTable.size () - 1; j++) {
		for (int k = j + 1; k < onTable.size (); k++) {

		    carda = (SetCard) onTable.elementAt (i);
		    cardb = (SetCard) onTable.elementAt (j);
		    cardc = (SetCard) onTable.elementAt (k);

		    if (isSet (carda, cardb, cardc)) {
			carda.setSelected (true);
			cardb.setSelected (true);
			cardc.setSelected (true);

			return;
		    }
		}
	    }
	}
    }

    //for debugging
    private void removeSet () {
	SetCard carda, cardb, cardc;

	clearcards (card1, card2, card3);
	card1 = nullCard;
	card2 = nullCard;
	card3 = nullCard;

	for (int i = 0; i < onTable.size () - 2; i++) {
	    for (int j = i + 1; j < onTable.size () - 1; j++) {
		for (int k = j + 1; k < onTable.size (); k++) {

		    card1 = (SetCard) onTable.elementAt (i);
		    card2 = (SetCard) onTable.elementAt (j);
		    card3 = (SetCard) onTable.elementAt (k);

		    if (isSet (card1, card2, card3)) {
			if (activePlayer == -1) 
			    activePlayer = 0;
			gotSet ();
			return;
		    }
		}
	    }
	}
    }

    void selectPlayer (int i) {
	activePlayer = i;
	clearcards (card1, card2, card3);
	card1 = nullCard;
	card2 = nullCard;
	card3 = nullCard;
	status.setText ("Player " + (i + 1) + ", select three cards.");
    }

    public void doneConfig () {
	configurator.dispose ();
	configurator = null;
	if (Config.main.singlePlayerMode && activePlayer == -1)
	    activePlayer = 0;
	//unblank cards
	for (int i = 0; i < onTable.size (); i++) {
	    SetCard card = (SetCard) onTable.elementAt (i);
	    card.isBlank = false;
	    card.repaint ();
	}
	//unpause
	turnStart += System.currentTimeMillis () - pauseStart;
    }

    public void keyPressed (KeyEvent e) {}
    public void keyReleased (KeyEvent e) {}
    public void keyTyped (KeyEvent e) {

	if (configurator != null) return;

	char key = e.getKeyChar ();

	if (key > '0' && key < '9' && !gameover) {
	    selectPlayer (key - '1');
	}
	if (key == 'c')
	    cheat ();
	if (key == 'r')
	    removeSet ();

	if (buzzed) return;

	switch (key) {
	case '`':
	    selectPlayer (0);
	    break;
	case '=':
	    selectPlayer (1);
	    break;
	case 'z':
	    selectPlayer (2);
	    break;
	case '/':
	    selectPlayer (3);
	    break;
	case 'b':
	    selectPlayer (4);
	    break;
	}
	buzzed = true;
    }

    public void mouseClicked (MouseEvent e) {};
    public void mouseReleased (MouseEvent e) {};
    public void mouseExited (MouseEvent e) {};
    public void mouseEntered (MouseEvent e) {};

    public void mousePressed (MouseEvent e) {

	if (configurator != null) return;

	deselectTimer.click ();
	requestFocus ();

	Object source = e.getSource ();

	if (source == config) {
	    //pause timer
	    pauseStart = System.currentTimeMillis ();

	    //blank cards
	    for (int i = 0; i < onTable.size (); i++) {
		SetCard card = (SetCard) onTable.elementAt (i);
		card.isBlank = true;
		card.repaint ();
	    }

	    //show configurator
	    configurator = new Configurator (this);
	    configurator.show ();
	    return;
	} else if (source == restart) {
	    startGame ();
	    return;
	} else if (source == help) {
	    SetHelpFrame.instance ().show ();
	    return;
	}

	if (gameover) return; //all of the other buttons are disabled
			      //when the game is over.

	//Was the click on one of the player buttons?
	for (int i = 0; i < NUM_PLAYERS; i++) {
	    if (source == players [i]) {
		selectPlayer (i);
		return;
	    }
	}

	//Was the click on the cheat button?
	if (source == cheat) {
	    cheat ();
	    return;
	}

	//None of the above: It must have been a card click
	if (activePlayer != -1) {
	    if (source == card1 || source == card2 || source == card3) return;
	    deselectTimer.paused = true;

	    //cycle the selected cards
	    card3.setSelected (false);
	    card3 = card2;
	    card2 = card1;
	    card1 = (SetCard) source;
	    card1.setSelected (true);

	    //do we have a set?
	    if (isSet (card1, card2, card3)) {
		if (Config.main.singlePlayerMode)
		    status.setText ("Select three cards");
		else
		    status.setText ("Select a player.");
		gotSet ();
	    } else {
		if (card1 != nullCard && card2 != nullCard && card3 != nullCard) 
		    status.setText ("Not a set because " + 
				    Set.whyNotSet (card1, card2, card3) + ".");
	    }
	    deselectTimer.paused = false;
	}
    }

    private void gotSet () {

	long now = System.currentTimeMillis ();

	times [activePlayer] += now - turnStart;

	//record the stats
	stats.gotSet (card1, card2, activePlayer, (int) (now - turnStart));

	turnStart = now;

	onTable.removeElement (card1);
	onTable.removeElement (card2);
	onTable.removeElement (card3);

	buzzed = false;

	if (liveboard () && onTable.size () >= 12) {

	    //there were > 12, and the board is still live (most likely,
	    //there are now 12).  Move cards from end into positions of
	    //removed cards (like real players do).

	    for (int i = center.getComponentCount () - 1; i >= 0; i--) {
		Component card = center.getComponent (i);
		if (card == card1 || card == card2 || card == card3) {
		    if (i < 12) {
			//we need to move in a card from the end, #12
			Component moved = center.getComponent (12);
			//remove card from end
			center.remove (12);
			//and put it in the current pos
			center.add (moved, i);
		    }
		    //and remove the current card
		    center.remove (card);
		} 
	    }
	} else {
	    if (deck.size () > 0) {

		//this whole song and dance has the goal of replacing the
		//removed cards, rather than deleting them and adding the new
		//cards at the end.
		for (int i = 0; i < center.getComponentCount (); i++) {
		    Component card = center.getComponent (i);
		    if (card == card1 || card == card2 || card == card3) {
			dealCard (i);
			center.remove (card);
		    }
		}
		//of course, extra cards *should* go at the end
		while (!liveboard ()) {
		    dealCard ();
		    dealCard ();
		    dealCard ();
		}
	    } else {
		center.remove (card1);
		center.remove (card2);
		center.remove (card3);
	    }
	}

	if (deck.size () == 0 && !isSetOnTable ()) {
	    //calculate winner
	    int winner = -1;
	    int best = 0;
	    for (int player = 0; player < NUM_PLAYERS; player ++) {
		if (score [player] > best) {
		    best = score [player];
		    winner = player;
		}
	    }

	    gameover = true;
	    String text;
	    if (Config.main.singlePlayerMode) {
		text = "Game over.  Press restart to play again.";
	    } else {
		text = "Game over.  Player " + (winner + 1) + " wins. Press restart to play again.";
	    }
	    GameOver go = new GameOver (this, text, stats);
	    go.show ();
	    go = null;
	    status.setText (text);
	}

	//deselect all cards
	for (int i = 0; i < center.getComponentCount (); i++) {
	    SetCard card = (SetCard) center.getComponent (i);
	    card.setSelected (false);
	}

	int left = deck.size () / 3;
	cardsLeft.setText ("   " + left + pluralize (left, " set") + " left");

	//force a repaint (works around bug in kaffe)
	validate ();

	score [activePlayer]++;

	int sc = score [activePlayer];
	lblscore [activePlayer].setText (sc + pluralize (sc, " set"));

	//round to the nearest 1/10th
	int secs = Math.round ((times [activePlayer] / sc) / 100.0f);
	int tenths = secs % 10;
	lbltime [activePlayer].setText ("Average Time: " + secs / 10 + "." + tenths);

	card1 = nullCard;
	card2 = nullCard;
	card3 = nullCard;
	if (!Config.main.singlePlayerMode)
	    activePlayer = -1;
    }

    public static String pluralize (int i, String str) {
	if (i == 1)
	    return str;
	else
	    if (str.endsWith ("s") || str.endsWith ("x"))
		return str + "es";
	    else
		return str + "s";	
    }

}



/*
This collects per-player stats on types of sets collected
stats are: how many and avg time by: differences, similarities & num diffs
So, one can see what the average time for cards differing in color is
*/

class Statistics extends Panel {
 
    public void display () {

	removeAll ();
	int i, j;
	//figure out how many players actually played
	//Assume that if player n did not play, player n + 1 also did not
	for (i = 0; i < Set.NUM_PLAYERS; i++) {
	    for (j = 0; j < 4; j ++) {
		if (countByNDiff [i][j] != 0) {
		    //this player has sets
		    break;
		}
	    }
	    if (j == 4) {
		//no sets for this player
		break;
	    }
	}
	int numPlayers = i;

	setLayout (new GridLayout (0, numPlayers * 2 + 1));

	addLabel (" ");
	for (i = 0; i < numPlayers; i++) {
	    addLabel ("Player " + (i + 1) + " Ct");
	    addLabel ("Average time");
	}

	for (int attr = 0; attr < 4; attr ++) {
	    addLabel ("Differing in " + SetCard.attributes [attr]);
	    for (i = 0; i < numPlayers; i++) {
		int count = countByDiff [i][attr];
		addLabel ("" + count, true);
		if (count == 0) {
		    addLabel ("N/A", false);
		} else {
		    int ms = totalTimeByDiff [i][attr] / count;
		    addLabel ("" + ms, false);
		}
	    }
	}

	addLabel ("--");
	for (i = 0; i < numPlayers; i++) {
	    addLabel ("--");
	    addLabel ("--");
	}
	for (int attr = 0; attr < 4; attr ++) {
	    addLabel ("Same in " + SetCard.attributes [attr]);
	    for (i = 0; i < numPlayers; i++) {
		int count = countBySim [i][attr];
		addLabel ("" + count, true);
		if (count == 0) {
		    addLabel ("N/A", false);
		} else {
		    int ms = totalTimeBySim [i][attr] / count;
		    addLabel ("" + ms, false);
		}
	    }
	}
	addLabel ("--");
	for (i = 0; i < numPlayers; i++) {
	    addLabel ("--");
	    addLabel ("--");
	}
	for (int attr = 0; attr < 4; attr ++) {
	    addLabel ((attr + 1) + Set.pluralize (attr + 1, " difference"));
	    for (i = 0; i < numPlayers; i++) {
		int count = countByNDiff [i][attr];
		addLabel ("" + count, true);
		if (count == 0) {
		    addLabel ("N/A", false);
		} else {
		    int ms = totalTimeByNDiff [i][attr] / count;
		    addLabel ("" + ms, false);
		}
	    }
	}
    }

    void addLabel (String s) {
	add (new Label (s));
    }

    void addLabel (String s, boolean b) {
	Label label = new Label (s);
	Color fore = b ? Color.white : Color.black;
	Color back = !b ? Color.white : Color.black;

	label.setForeground (fore);
	label.setBackground (back);
	add (label);
    }

    public void gotSet (SetCard card1, SetCard card2, int player, int msecs) {

	//find differences
	int diffCount = -1; //note that this array is indexed 0-3 for vals 1-4
	for (int i = 0; i < 4; i++) {
	    if (card1.attr (i) != card2.attr (i)) {
		diffCount ++;
		countByDiff [player][i] ++;
		totalTimeByDiff [player][i] += msecs;
	    } else {
		countBySim [player][i] ++;
		totalTimeBySim [player][i] += msecs;
	    }
	}

	countByNDiff [player][diffCount] ++;
	totalTimeByNDiff [player][diffCount] += msecs;
    }

    int totalTimeByDiff [][] = new int [Set.NUM_PLAYERS][4];
    int totalTimeBySim [][] = new int [Set.NUM_PLAYERS][4];
    int totalTimeByNDiff [][] = new int [Set.NUM_PLAYERS][4];
    
    int countByDiff [][] = new int [Set.NUM_PLAYERS][4];
    int countBySim [][] = new int [Set.NUM_PLAYERS][4];
    int countByNDiff [][] = new int [Set.NUM_PLAYERS][4];

    public Statistics () {

	for (int i = 0; i < Set.NUM_PLAYERS; i ++) {
	    for (int j = 0; j < 4; j ++) {
		
		totalTimeByDiff [i][j] = 0;
		totalTimeBySim [i][j] = 0;
		totalTimeByNDiff [i][j] = 0;
		countByDiff [i][j] = 0;
		countBySim [i][j] = 0;
		countByNDiff [i][j] = 0;
	    }
	}
    }
}

class SetCard extends Canvas {

    int color, number, shade, shape;

    public static final String colorNames [] = {"red", "green", "blue"};
    public static final String numberNames [] = {"one", "two", "three"};
    public static final String shapeNames [] = {"diamond", "oval", "squiggle"};
    public static final String shadeNames [] = {"hollow", "shaded", "solid"};
    public static final String attributeNames [][] = {colorNames, numberNames, 
						      shapeNames, shadeNames};

    public static final String attributes [] = {"color", "number", 
						"shape", "shade"};

    boolean selected;

    public SetCard (int c, int n, int sp, int sd) {
	color = c;
	number = n;
	shape = sp;
	shade = sd;
	selected = false;
    }

    public int attr (int i) {
	switch (i) {
	case 0:
	    return color;
	case 1:
	    return number;
	case 2:
	    return shape;
	case 3:
	    return shade;
	default:
	    return -1;
	}
    }
    
    public void setAttributes (int c, int n, int sp, int sd) {
	color = c;
	number = n;
	shape = sp;
	shade = sd;
	repaint ();
    }
    
    public void set (SetCard a) {
	color = a.color;
	number = a.number;
	shape = a.shape;
	shade = a.shade;
	repaint ();
    }
    
    public void setSelected (boolean sel) {
	if (selected != sel) {
	    selected = sel;
	    repaint ();
	}
    }
    
    public String colorName () {
	return colorNames [color];
    }
    public String numberName () {
	return numberNames [number];
    }
    public String shapeName () {
	return shapeNames [shape];
    }
    public String shadeName () {
	return shadeNames [shade];
    }

    public static SetCard third (SetCard a, SetCard b) {
	return new SetCard (2 - (a.color + b.color + 2) % 3, 
			    2 - (a.number + b.number + 2) % 3, 
			    2 - (a.shape + b.shape + 2) % 3, 
			    2 - (a.shade + b.shade + 2) % 3);

    }
    
    final double squiggle_x [] = { 0.6, 0.62, 0.52, 0.27,
				   0.7, 0.7 };
    final double squiggle_y [] = { 0.43431457505076198048, 0.4, 0.42,
				   0.18, 
				   -0.28284271247461900977,
				   -0.65147186257614297071 };
    final double squiggle_r [] = { 0.56568542494923801952, 0.6, 0.5,
				   0.15857864376269049511,
				   0.46568542494923801954,
				   0.83431457505076198048 };
    
    final int squiggle_start [] = { 60, 90, 180, 220, 85, 60 };
    final int squiggle_end [] = { 90, 180, 225, 315, 136, 90 };


    public boolean isBlank = false;
    
    public void paint (Graphics g) {
	
	int xpoints [] = new int [4];
	int ypoints [] = new int [4];
	
	Color bordercolor, fillcolor, backcolor;
	
	Dimension d = getSize ();
	int w = d.width - 1;
	int h = d.height - 1;

	if (isBlank) {
	    //fill
	    g.setColor (Config.main.background);
	    g.fillRoundRect (2, 2, w - 4, h - 4, w / 10, h / 10);
	
	    //border
	    g.setColor (Color.black);
	    g.drawRoundRect (1, 1, w - 2, h - 2, w / 10, h / 10);
	    g.drawRoundRect (1, 1, w - 3, h - 2, w / 10, h / 10);
	    g.drawRoundRect (1, 1, w - 2, h - 3, w / 10, h / 10);

	    return;
	}
	
	fillcolor = Config.main.cardcolors [color];
	bordercolor = fillcolor;

	if (selected)
	    backcolor = Config.main.selected;
	else
	    backcolor = Config.main.background;
	
	if (shade == 0) {
	    fillcolor = backcolor;
	}
	
	g.setColor (backcolor);
	g.fillRoundRect (2, 2, w - 4, h - 4, w / 10, h / 10);
	
	//border
	g.setColor (Color.black);
	g.drawRoundRect (1, 1, w - 2, h - 2, w / 10, h / 10);
	g.drawRoundRect (1, 1, w - 3, h - 2, w / 10, h / 10);
	g.drawRoundRect (1, 1, w - 2, h - 3, w / 10, h / 10);
	

	int count = number + 1;
	
	for (int i = 0; i < count; i++) {
	    switch (shape) {
	    case 0:
		//diamond
		xpoints[0] = (int) (w * 0.2);
		xpoints[1] = (int) (w * 0.5);
		xpoints[2] = (int) (w * 0.8);
		xpoints[3] = (int) (w * 0.5);
		
		ypoints[0] = (int) (h * (i + .5) / count);
		ypoints[1] = (int) (h * (i + .5) / count - h / 10);
		ypoints[2] = (int) (h * (i + .5) / count);
		ypoints[3] = (int) (h * (i + .5) / count + h / 10);
		g.setColor (fillcolor);
		g.fillPolygon (xpoints, ypoints, 4);

		//shaded
		if (shade == 1) {
		    //Erase:
		    g.setColor (backcolor);
		    for (int x = (int) (w * 0.2); x < w * 0.8; x += Config.main.stripeWidth * 2) {
			for (int j = 0; j < Config.main.stripeWidth; j ++)
			    g.drawLine (x + j, 
					(int) (h * (i + .5) / count - h / 10), 
					x + j, 
					(int) (h * (i + .5) / count + h / 10));
		    }
		}

		//border
		g.setColor (bordercolor);
		for (int k = 0; k < 3; k++) {
		    g.drawPolygon (xpoints, ypoints, 4);
		    for (int j = 0; j < 4; j++) {
			ypoints[j] ++;
		    }
		}
		
		break;
		
	    case 1:
		g.setColor (fillcolor);
		g.fillOval ((int) (w * .2), (int) (h * (i + .5) / count - h / 9),
			    (int) (w * .6), (int) (h / 6));

		//shaded
		if (shade == 1) {
		    //Erase:
		    g.setColor (backcolor);
		    for (int x = (int) (w * 0.19); x < w * 0.8; x += Config.main.stripeWidth * 2) {
			for (int j = 0; j < Config.main.stripeWidth; j ++) 
			    g.drawLine (x + j, 
					(int) (h * (i + .5) / count - h / 10), 
					x + j, 
					(int) (h * (i + .5) / count + h / 10));
		    }
		}

		//border

		g.setColor (bordercolor);
		for (int k = 0; k < 3; k++) {
		    g.drawOval ((int) (w * .2), 
				(int) (h * (i + .5) / count - h / 9) + k,
				(int) (w * .6), (int) (h / 6));
		}
		break;
	    case 2:
		//squiggle
		//Gary Wong <gtw at gnu.org> wrote most of the code in this case
		int base_x = (int) (w * .17);
		int base_y = (int) (h * (i + .5) / count + h / 10);

		g.clipRect (3, 3, w - 6, h - 6);

		g.setColor( fillcolor);

		///fill in the center of the squiggle
		g.fillRect ((int) (base_x + w * .07), (int) (base_y - h * .12),
			    (int) (w * .45), (int) (h * .11));
	
		g.fillRect ((int) (base_x + w * .16), (int) (base_y - h * .185),
			    (int) (w * .43), (int) (h * .08));

		//convex arcs
		for( int j = 0; j < 4; j++) {
		    g.fillArc( (int) (base_x + w * ( squiggle_x [j] -
						     squiggle_r [j]) / 3),
			       (int) (base_y - h * ( squiggle_y [j] + 
						     squiggle_r [j]) / 5),
			       (int) (squiggle_r [j] * w * 2 / 3),
			       (int) (squiggle_r [j] * h * 2 / 5),
			       squiggle_start [j], 
			       squiggle_end [j]  - squiggle_start [j]);
		    g.fillArc( (int) (base_x + w * ( 2 - squiggle_x [j] - 
						     squiggle_r [j]) / 3),
			       (int) (base_y - h * ( 1 - squiggle_y [j] + 
						     squiggle_r [j]) / 5),
			       (int) (squiggle_r [j] * w * 2 / 3),
			       (int) (squiggle_r [j] * h * 2 / 5),
			       180 + squiggle_start [j], 
			       squiggle_end [j]  - squiggle_start [j]);
		}      

		//Concave arcs
		g.setColor( backcolor);
		for( int j = 4; j < 6; j++) {
		    //bottom
		    g.fillArc( (int) (base_x + w * ( squiggle_x [j] - 
						     squiggle_r [j]) / 3),
			       (int) (base_y - h * ( squiggle_y [j] + 
						     squiggle_r [j]) / 5),
			       (int) (squiggle_r [j] * w * 2 / 3),
			       (int) (squiggle_r [j] * h * 2 / 5),
			       squiggle_start [j], 
			       squiggle_end [j]  - squiggle_start [j]);

		    //top
		    g.fillArc( (int) (base_x + w * ( 2 - squiggle_x [j] - 
						     squiggle_r [j]) / 3),
			       (int) (base_y - h * ( 1 - squiggle_y [j] + 
						     squiggle_r [j]) / 5),
			       (int) (squiggle_r [j] * w * 2 / 3),
			       (int) (squiggle_r [j] * h * 2 / 5),
			       180 + squiggle_start [j], 
			       squiggle_end [j]  - squiggle_start [j]);
		}      

		//shaded
		if (shade == 1) {
		    //Erase:
		    g.setColor (backcolor);
		    for (int x = (int) (w * 0.15); x < w * 0.85; x += Config.main.stripeWidth * 2) {
			for (int j = 0; j < Config.main.stripeWidth; j ++) 
			    g.drawLine (x + j, 
					(int) (h * (i + .5) / count - h / 10), 
					x + j, 
					(int) (h * (i + .5) / count + h / 10));

		    }
		}

		//outline
		g.setColor (bordercolor);
		for( int j = 0; j < 6; j++) 
		    for( int k = 0; k < 2; k++) {
			g.drawArc( (int) (base_x + w * ( squiggle_x [j] - 
							 squiggle_r [j]) / 3),
				   (int) (base_y - h * ( squiggle_y [j] + 
							 squiggle_r [j]) / 5) + k,
				   (int) (squiggle_r [j] * w * 2 / 3),
				   (int) (squiggle_r [j] * h * 2 / 5),
				   squiggle_start [j], 
				   squiggle_end [j]  - squiggle_start [j]);
			g.drawArc( (int) (base_x + w * ( 2 - squiggle_x [j] - 
							 squiggle_r [j]) / 3),
				   (int) (base_y - h * ( 1 - squiggle_y [j] + 
							 squiggle_r [j]) / 5) + k,
				   (int) (squiggle_r [j] * w * 2 / 3),
				   (int) (squiggle_r [j] * h * 2 / 5),
				   180 + squiggle_start [j], 
				   squiggle_end [j]  - squiggle_start [j]);
		    }

		break;
	    }
	}
    }
    public Dimension getPreferredSize() {
	return new Dimension (100, 200);
    }
    
    public Dimension getMinimumSize() {
	return new Dimension (50, 100);
    }

    public Dimension getMaximumSize() {
	return new Dimension (150, 200);
    }


    public String toString () {
	return "SetCard (" + color + ", " + number + ", " + shape + ", " + shade + ")";
    }

 
}

class GameOver extends Frame implements ActionListener {

    Set game;

    public GameOver (Set g, String text, Statistics stats) {
	super (null);
	setTitle ("Game Over");
	game = g;

	BorderLayout bl = new BorderLayout ();
	setLayout (bl);

	add (new Label (text), "North");
	Button b = new Button ("Restart");
	b.addActionListener (this);
	add (b, "South");
	stats.display ();
	add (stats, "Center");
	pack ();

    }
    public void actionPerformed (ActionEvent e) {
	game.startGame ();
	hide ();
	dispose ();
    }

}

class SetHelpFrame extends Frame implements WindowListener {
    Vector helpBits;

    static SetHelpFrame _instance = null;

    public static SetHelpFrame instance () {
	if (_instance == null)
	    _instance = new SetHelpFrame ();
	return _instance;
    }

    private SetHelpFrame () {
	super (null);
	setTitle ("Set Help");

	helpBits = new Vector ();

	BorderLayout bl = new BorderLayout ();
	setLayout (bl);

	GridBagLayout gridbag = new GridBagLayout ();
	Panel center = new Panel (gridbag);

	initHelpBits (gridbag);

	for (ListIterator i = helpBits.listIterator (); i.hasNext ();) {
	    center.add ((Component) i.next ());
	}

	ScrollPane sp = new ScrollPane ();
	sp.add (center);
	sp.setSize (new Dimension (320, 500));
	add (sp, "Center");

	addWindowListener (this);

	pack ();
    }

    public void windowActivated (WindowEvent e) {}
    public void windowClosed (WindowEvent e) {}
    public void windowOpened (WindowEvent e) {}
    public void windowDeactivated (WindowEvent e) {}
    public void windowDeiconified (WindowEvent e) {}
    public void windowIconified (WindowEvent e) {}

    public void windowClosing (WindowEvent e) {
	dispose ();
    }

    void initHelpBits (GridBagLayout gridbag) {
	GridBagConstraints gbc = new GridBagConstraints ();
	gbc.gridx = 0;
	gbc.gridy = GridBagConstraints.RELATIVE;
	gbc.weightx = 1;
	gbc.weighty = 0;

	//inital explanation

	Panel demoPanel1 = new Panel ();
	demoPanel1.setLayout (new GridLayout (1, 3));
	demoPanel1.add (new SetCard (0, 2, 0, 2));
	demoPanel1.add (new SetCard (0, 2, 1, 1));
	demoPanel1.add (new SetCard (0, 2, 2, 0));
	gbc.fill = GridBagConstraints.NONE;
	gridbag.setConstraints (demoPanel1, gbc);
	helpBits.add (demoPanel1);

	gbc.fill = GridBagConstraints.BOTH;
	WrappedLabel wl = new WrappedLabel (help [0]);
	gridbag.setConstraints (wl, gbc);
	helpBits.add (wl);

	//bad cards
	Panel demoPanel2 = new Panel ();
	demoPanel2.setLayout (new GridLayout (1, 3));
	demoPanel2.add (new SetCard (1, 2, 0, 0));
	demoPanel2.add (new SetCard (2, 2, 1, 0));
	demoPanel2.add (new SetCard (2, 2, 2, 0));
	gridbag.setConstraints (demoPanel2, gbc);
	gbc.fill = GridBagConstraints.NONE;
	helpBits.add (demoPanel2);

	gbc.fill = GridBagConstraints.BOTH;
	wl = new WrappedLabel (help [1]);
	gridbag.setConstraints (wl, gbc);
	helpBits.add (wl);

	//user cards
	Component stp = new SetTwiddlingPanel ();
	gridbag.setConstraints (stp, gbc);
	helpBits.add (stp);

	wl = new WrappedLabel (help [2]);
	gridbag.setConstraints (wl, gbc);	
	helpBits.add (wl);

	//how to use this applet
	wl = new WrappedLabel (help [3]);
	gridbag.setConstraints (wl, gbc);
	helpBits.add (wl);


    }

    String help [] = {
//string1:
	"A Set deck contains 81 Set cards.  Each card has four attributes: " +
"color, number, shape, and shading.  Each attribute has three possible " +
"settings.  Three attributes match if they're all the same or all " +
"different.  A set consists of three Set cards for which all four " +
"attributes match.  For example, the above set is the same in " +
"number and color, but different in shape and shading.",

//string2:
"The cards above are not a set, because their colors are neither " +
"all the same nor all different.  When two cards have the same " +
"attribute, but one has a different attribute, the cards do not " +
"make a set.",

//string3:
"You can use these selectors to change the cards displayed above. " +
"The text below the cards will tell you if they are a set, and if not, " +
"why not.",

//string4:
"To play Set with this applet, assign one of the numbered player " +
"buttons to each player.  When a player calls a set, he or she " + 
"should click on their player button (or press the corresponding " +
"number key on the keyboard).  Alternately, the keys ', =, z, /, and b " +
"can be used like buzzers on a game show -- once a player buzzes in, " +
"the other buzzers are deactivated (but the number keys and buttons " +
"still work).  After a player is selected, he or she should click the " +
"three cards that make up his or her set.  If the cards are actually " +
"a set, they will be removed.  Otherwise, the bottom of the window " +
"will explain why they are not a set.  To select three different " +
"cards, just click them."
};

}

class SetTwiddlingPanel extends Panel implements ItemListener, ActionListener {

    Choice controllers [][];
    SetCard cards [];
    Button third;
    WrappedLabel status;

    public void itemStateChanged (ItemEvent e) {
	Object source = e.getSource ();

	for (int card = 0; card < 3; card++) { 
	    for (int ctrl = 0; ctrl < 4; ctrl++) {
		Choice [] controller = controllers [card];
		if (source == controller [ctrl]) {
		    //do something
		    cards [card].setAttributes 
			(controller [0].getSelectedIndex (),
			 controller [1].getSelectedIndex (),
			 controller [2].getSelectedIndex (),
			 controller [3].getSelectedIndex ());
		}
	    }
	}
	if (Set.isSet (cards [0], cards [1], cards [2]))
	    status.setText ("These cards are a set");	
	else 
	    status.setText ("Not a set because " + 
			    Set.whyNotSet (cards [0], cards [1], cards [2]));
	validate ();
    }

    public void actionPerformed (ActionEvent e) {
	//must be third button
	status.setText ("These cards are a set");
	validate ();
	cards [2].set (SetCard.third (cards [0], cards [1]));
	controllers[2][0].select (cards [2].color);
	controllers[2][1].select (cards [2].number);
	controllers[2][2].select (cards [2].shape);
	controllers[2][3].select (cards [2].shade);

    }

    public SetTwiddlingPanel () {

	GridBagLayout gridBag = new GridBagLayout ();
	setLayout (gridBag);
	GridBagConstraints gbc = new GridBagConstraints ();

	controllers = new Choice [3][];

	cards = new SetCard [3];

	//set up 3 cards and 4 controllers per card
	for (int card = 0; card < 3; card++) { 

	    cards [card] = new SetCard (card, 0, 0, 0);
	    gbc.gridx = card;
	    gbc.gridy = 0;
	    add (cards [card], gbc);

	    controllers [card] = new Choice [4];
	    for (int ctrl = 0; ctrl < 4; ctrl++) {
		gbc.gridx = card;
		gbc.gridy = ctrl + 1;
		Choice list = new Choice ();
		for (int i = 0; i < 3; i++) {
		    list.add (SetCard.attributeNames [ctrl][i]);
		}
		add (list, gbc);
		controllers [card][ctrl] = list;
		list.addItemListener (this);
	    }
	    //make the selection match the displayed card
	    controllers [card][0].select (card);
	}

	gbc.gridx = 1;
	gbc.gridy = 5;
	third = new Button ("Show third");
	add (third, gbc);
	third.addActionListener (this);

	gbc.gridx = 0;
	gbc.gridy = 6;
	gbc.gridwidth = 3;
	status = new WrappedLabel ("These cards are a set");
	status.setSize (getSize ().width, status.lineHeight * 3);
	add (status, gbc);
    }
}

//a non-editable text area with wrapping.
class WrappedLabel extends Canvas {

    String [] contents;
    Vector wrappedContents = new Vector ();

    static final int leftMargin = 5;
    int lineHeight;

    public WrappedLabel (String c) {
	this (new String [] {c});
    }
    public WrappedLabel (String [] c) {
	super ();
	contents = c;
	Font f = getFont ();  
	if (f == null) {
	    f = new Font ("Times", Font.PLAIN, 12);
	    setFont (f);
	}
	lineHeight = getFontMetrics (f).getHeight ();
	setSize (300, 100);
	wrap ();
    }


    public void setText (String c) {
	contents = new String [] {c};
	forceWrap ();
	repaint ();
    }

    Dimension saved_size = null;

    private void forceWrap () {
	Dimension d = getSize ();

	wrappedContents.clear ();

	FontMetrics fm = getFontMetrics (getFont ());
	//for each "paragraph"
	for (int para = 0; para < contents.length; para++) {
	    int i = 0;
	    contents [para] += ' ';
	    char [] chars = contents [para].toCharArray ();
	    //add characters until we overflow.
	    do {
		int start = i;
		int lastspace = i;
		int width = leftMargin;
		while (width <= d.width && i < chars.length) {
		    if (chars [i] == ' ')
			lastspace = i;
		    width += fm.charWidth (chars [i++]);
		}

		String row = new String (chars, start, lastspace - start);
		wrappedContents.add (row);
		i = lastspace + 1; //start over at last space

	    } while (i != chars.length);
	    wrappedContents.add ("");
	    wrappedContents.add ("");
	}
    }

    private void wrap () {
	Dimension d = getSize ();
	if (! d.equals (saved_size)) {
	    forceWrap ();
	    saved_size = d;
	}
    }

    public Dimension getPreferredSize() {
	int w = saved_size.width;
	if (w < 200) w = 200;
	if (w > 400) w = 400;
	FontMetrics fm = getFontMetrics (getFont ());
	return new Dimension (w, wrappedContents.size () * fm.getHeight ());
    }

    public Dimension getMinimumSize() {
	return new Dimension (200, 10);
    }

    public void paint (Graphics g) {

	int y = 10;
	FontMetrics fm = getFontMetrics (getFont ());

	wrap ();

	for (ListIterator i = wrappedContents.listIterator (); i.hasNext ();) {
	    g.drawString ((String) i.next (), leftMargin, y);
	    y += lineHeight;
	}
    }
}

class Config {

    public boolean singlePlayerMode;
    public Color background, selected, cardcolors [] = new Color [3];

    public int stripeWidth = 3;

    static Config main = new Config ();

    public Config () {
	singlePlayerMode = false;
	background = Color.white;
	selected = Color.lightGray;
	cardcolors [0] = Color.red;
	cardcolors [1] = new Color (0f, .75f, 0f);
	cardcolors [2] = Color.blue;
    }

    public Config (String str) {

	ArgParser args = new ArgParser (str);

	background = Color.decode ("0x" + args.get ("background", "ffffff"));
	selected = Color.decode ("0x" + args.get ("selected", "c0c0c0"));
	
	cardcolors [0] = Color.decode ("0x" + args.get ("color0", "ff0000"));
	cardcolors [1] = Color.decode ("0x" + args.get ("color1", "00bf00"));
	cardcolors [2] = Color.decode ("0x" + args.get ("color2", "0000ff"));

	stripeWidth = Integer.parseInt (args.get ("stripeWidth", "3"));

	singlePlayerMode = args.get ("single", "N") == "Y";
    }

    public String toString () {
	Color colors [] = {background, selected, cardcolors [0], 
			   cardcolors [1], cardcolors [2]};
	String colorNames [] = {"background", "selected", "color0", "color1",
				"color2"};
	String s = "";
	s += "single=" + (singlePlayerMode ? "Y" : "N");
	for (int i = 0; i < 5; i ++) {
	    s += "&" + colorNames [i] + "=" + colorToHex (colors [i]);
	}
	s += "&stripeWidth=" + stripeWidth;
	return s;
    }

    public Config (Config c) {
	background = c.background;
	selected = c.selected;
	for (int i = 0; i < 3; i ++) {
	    cardcolors [i] = c.cardcolors [i];
	}

	singlePlayerMode = c.singlePlayerMode;
	stripeWidth = c.stripeWidth;
    }
    public static String colorToHex (Color c) {
	int r = c.getRed ();
	int g = c.getGreen ();
	int b = c.getBlue ();
	return paddedHex (r) + paddedHex (g) + paddedHex (b);
    }

    public static String paddedHex (int i) {
	String s = Integer.toHexString (i);
	if (s.length () == 1) s = "0" + s;
	return s;
    }

}

class Configurator extends Frame implements ActionListener, WindowListener {
    Config origConfig;

    Button okButton, cancelButton, resetButton, genBookmarkButton;

    TextField stripeWidth, linkField, colors [] = new TextField [5];

    Checkbox singlePlayerMode;

    String base;
    Set set;

    public Configurator (Set s) {
	super ("Configurator");
	set = s;
	base = set.getDocumentBase ().toString ();
	int qidx;
	if ((qidx = base.indexOf ('?')) != -1)
	    base = base.substring (0, qidx);
	origConfig = new Config (Config.main);
	addWindowListener (this);
	setup ();
	init (origConfig);
    }

    void setColor (TextComponent f, Color c) {
	f.setText (Config.colorToHex (c));
    }

    void init (Config c) {
	setColor (colors [0], c.background);
	setColor (colors [1], c.selected);
	for (int i = 0; i < 3; i ++) {
	    setColor (colors [i + 2], c.cardcolors [i]);
	}
	singlePlayerMode.setState (c.singlePlayerMode);
	stripeWidth.setText ("" + c.stripeWidth);
    }

    Config makeConfig () {
	Config c = new Config ();
	c.background = Color.decode ("0x" + colors [0].getText ());
	c.selected = Color.decode ("0x" + colors [1].getText ());
	for (int i = 0; i < 3; i ++) {
	    c.cardcolors [i] = Color.decode ("0x" + colors [i + 2].getText ());
	}
	c.singlePlayerMode = singlePlayerMode.getState ();
	c.stripeWidth = Integer.parseInt (stripeWidth.getText ());
	return c;
    }

    public void actionPerformed (ActionEvent e) {
	Object source = e.getSource ();
	if (source == okButton) {
	    Config.main = makeConfig ();
	    close ();
	} else if (source == resetButton) {
	    init (origConfig);
	} else if (source == cancelButton) {
	    close ();
	} else if (source == genBookmarkButton) {
	    Config c = makeConfig ();
	    String link = base + "?" + c.toString ();
	    linkField.setText (link);
	}
    }

    void close () {
	hide ();
	set.doneConfig ();
    }

    void setup () {
	setLayout (new BorderLayout ());

	Panel colorPanel = new Panel ();
	colorPanel.setLayout (new GridLayout (0, 2));

	String text [] = {"Card background", "Selected card background", 
			  "Card color 1 (\"red\")", "Card color 2 (\"green\")", 
			  "Card color 3 (\"blue\")"};

	for (int i = 0; i < 5; i++) {
	    colorPanel.add (new Label (text [i]));
	    colors [i] = new TextField ();
	    colorPanel.add (colors [i]); 
	}

	singlePlayerMode = new Checkbox ();
	colorPanel.add (new Label ("Single Player Mode"));
	colorPanel.add (singlePlayerMode);

	colorPanel.add (new Label ("Stripe width for shaded shapes"));
	colorPanel.add (stripeWidth = new TextField ());

	//for displaying the link
	colorPanel.add (new Label ("Link"));
	linkField = new TextField ();
	linkField.setEditable (false);
	colorPanel.add (linkField);

	add (colorPanel, "Center");

	Panel buttonPanel = new Panel ();
	buttonPanel.setLayout (new FlowLayout ());
	
	//loadButton = makeButton (buttonPanel, "Load from cookie");	
	//saveButton = makeButton (buttonPanel, "Save to cookie");
	genBookmarkButton = makeButton (buttonPanel, "Generate bookmark");
	resetButton = makeButton (buttonPanel, "Reset");
	cancelButton = makeButton (buttonPanel, "Cancel");
	okButton = makeButton (buttonPanel, "OK");

	add (buttonPanel, "South");
	pack ();
    }

    Button makeButton (Container c, String text) {
	Button b = new Button (text);
	c.add (b);
	b.addActionListener (this);

	return b;
    }


    public void windowActivated (WindowEvent e) {}
    public void windowClosed (WindowEvent e) {}
    public void windowOpened (WindowEvent e) {}
    public void windowDeactivated (WindowEvent e) {}
    public void windowDeiconified (WindowEvent e) {}
    public void windowIconified (WindowEvent e) {}

    public void windowClosing (WindowEvent e) {
	close ();
    }
}

class ArgParser {

    TreeMap args;

    ArgParser (String str) {
	System.out.println ("parsing " + str);
	args = new TreeMap ();

	int start = 0;
	while (true) {
	    int eidx = str.indexOf ('=', start);
	    if (eidx == -1) break;
	    String var = str.substring (start, eidx);
	    int aidx = str.indexOf ('&', eidx);
	    if (aidx == -1) aidx = str.length ();
	    String val = str.substring (eidx + 1, aidx);
	    System.out.println ("parsed " + var + " = " + val + ".");
	    args.put (var, val);
	    start = aidx + 1;
	}
    }

    String get (String var) {
	return (String) args.get (var);
    }
    String get (String var, String def) {
	Object o = args.get (var);
	if (o == null) 
	    return def;
	else 
	    return (String) o;
    }

}
