Webset specifications

Informal description

Webset is a computer version of the game of Set. Unlike most computer versions, this one is multi-user.

Users are presented with a game window, where they can fill in their names, create and delete games, or join games that have not yet begun. When a game is joined, the user can talk to the other users that have joined the game. Once a game is started, the cards being played with are displayed. The game proceeds as follows: 12 cards are laid out from the deck on the table. Now, the players have to search for combinations of three cards that satisfy certain criteria; such a combination is called a `set' (a more detailed description). The first one to see a set presses a `set!' button, and then has a couple of seconds to point out the three cards. These are removed and replaced with cards from the deck. The game continues until the deck is exhausted. The player with most sets wins. When no player can find any sets, extra cards may be laid out. The game also has the option to point out sets, pausing the game, and enabling all players to see the sets on the table, after which the game may be continued with fresh cards.

Diagrams generated from VDC specification


ERD


STD of user


STD of game

Code

Data constraints: webset.vdc

entity user(username) {
	value game_event, setcalled/0;
	event speak_event, new_game/0,more_cards/0,show_solutions/0,call_set/0;
	set player(~game,~joined) : participant(self,~game,~joined); 
	set game_event(~msg) : game_event(self,~msg);
	statemachine {
		Free -> Watching   {} { player.joined==0 }
		Watching -> Playing   {} { player.joined==1 }
		         -> Free      {} { not exists(player) }
		Playing -> Free     {} { not exists(player) }
		        -> Watching {} { player.joined==0 }
	}
	assert Free { not game_event and not setcalled }
	assert Watching { not game_event and not setcalled }
}

entity game(name,in_progress) {
	event scoring, game_over/0;
	set new_game(~player) : new_game(~player) participant(~player,self,1);
	set setcalled(~player,~joined) :
		participant(~player,self,~joined) setcalled(~player);
	set solutions_shown(~cd1,~cd2,~cd3) : solution(~cd1,~cd2,~cd3)
		setcard(~cd1,self,_,_,_)
		setcard(~cd2,self,_,_,_)
		setcard(~cd3,self,_,_,_);
	set userselected(~player,~card) :
		selected(~player,~card) participant(~player,self,_);
	statemachine {
		Initialising -> BeginTurn { new_game }
		BeginTurn -> SetCalled      {} { setcalled }
		          -> SolutionsShown {} { solutions_shown }
		SetCalled -> BeginTurn    {} { not setcalled }
				  -> BeginTurn    { new_game }
		SolutionsShown -> BeginTurn {} { not solutions_shown }
				       -> BeginTurn { new_game }
	}
	assert Initialising { not userselected }
	assert BeginTurn { not userselected }
	assert SolutionsShown { not userselected and not setcalled }
	assert { size(setcalled) <= 1 and not exists(setcalled.joined==0)}
}

entity setcard(game:*,picture,xpos,ypos) {}

relation participant(user:*,game:0..1,joined) {}
relation selected(user:*,setcard:0..3) {}
relation solution(setcard:0..1,setcard:0..1,setcard:0..1) {}

set solutions(~game1,~cd1,~game2,~cd2,~game3,~cd3) : solution(~cd1,~cd2,~cd3)
	setcard(~cd1,~game1,_,_,_)
	setcard(~cd2,~game2,_,_,_)
	setcard(~cd3,~game3,_,_,_);

assert{ not exists(
	solutions.game1 != solutions.game2 or solutions.cd1 == solutions.cd2 or
	solutions.game1 != solutions.game3 or solutions.cd1 == solutions.cd3 or
	solutions.game2 != solutions.game3 or solutions.cd2 == solutions.cd3) }

User window: webset_user.html

<HTML><HEAD>
 <TITLE>WebSet</TITLE>
</HEAD><BODY>

Your name
<applet code="TextFieldAgent.class" width=200 height=40 name="useragent"
align="center">
<param name="columns" value=25>
<param name="trailer" value="">
<param name="init_s" value='
	@update + user(self,"Anonymous");
'>
<param name="text_out_s" value='
	@update + user(self, *text);
'>
</applet>
<BR>

<table> <tr><td> 

<applet code="ButtonAgent" width=90 height=30>
<param name="text" value="Create game">
<param name="pressed_s" value='
	@execute("vs","webset_game.vs");
'>
</applet>
<br>
<applet code="ButtonAgent" width=90 height=30>
<param name="text" value="Join game">
<param name="pressed_s" value = '
	print useragent;
	@updateq + participant(useragent,~GAME,*1) : participant(useragent,~GAME,0);
'>;
</applet>
<applet code="ButtonAgent" width=90 height=30>
<param name="text" value="Delete game">
<param name="pressed_s" value='
	@updateq -game(~GAME,_,_) : participant(useragent, ~GAME, _);
'>
</applet>

</td>

<td>
Select a game:
<applet code="ListboxAgent" width=120 height=130>
<param name="rows" value=8>
<param name="init_s" value = '
	@addquery item_list ~N,~G : game(~G, ~N, 0);
	@addquery item_list ~N+" (running)",~G : game(~G, ~N, 1);
'>
<param name="selected_s" value = '
	@update + participant(useragent, *item, *0);
'>;
</applet>

</td><td>

Users in game:
<applet code="ListboxAgent" width=120 height=130>
<param name="rows" value=8>
<param name="init_s" value = '
	@addquery item_list ~N,~U :
		participant(useragent,~G,_) participant(~U,~G,0) user(~U,~N);
	@addquery item_list ~N+" (joined)",~U :
		participant(useragent,~G,_) participant(~U,~G,1) user(~U,~N);
	@addquery item_list ~N+" (ready)",~U :
		participant(useragent,~G,_) participant(~U,~G,2) user(~U,~N);
'>;
</applet>

</td><td>

<applet code="TextAreaAgent" width=300 height=220>
<param name="rows" value=15>
<param name="init_s" value = '
	@addquery append ~NAME+": "+~TEXT :
		participant(useragent,~GAME,_) participant(~USER,~GAME,_)
		user(~USER, ~NAME) speak_event(~USER,~TEXT);
	@addquery append ~NAME+" joins the game." :
		participant(useragent,~GAME,_) participant(~USER,~GAME,1)
		user(~USER, ~NAME);
	@addquery append ~NAME+" requests game start." :
		participant(useragent,~GAME,_) participant(~USER,~GAME,2)
		user(~USER, ~NAME);
	@addquery append ~NAME+" calls ---S-E-T---." :
		participant(useragent,~GAME,_) participant(~USER,~GAME,_)
		user(~USER, ~NAME) setcalled(~USER);
	@addquery append "Game event: "+~NAME+" "+~MSG :
		participant(useragent,~GAME,_) participant(~USER,~GAME,_)
		user(~USER, ~NAME) game_event(~USER,~MSG);
'>;
</applet>

<applet code="TextFieldAgent" width=300 height=40>
<param name="trailer" value="">
<param name="text_out_s" value = '
	@update . speak_event(useragent,text);
'>;
</applet>

</td></tr>
</table>


<table> <tr>

<td>

<applet code="ButtonAgent" width=110 height=25>
<param name="text" value="Start game">
<param name="pressed_s" value='
	@updateq -setcard(_,~GAME, _, _, _) : participant(useragent,~GAME,1);
	@updateq -solution(~GAME,_, _, _) : participant(useragent,~GAME,1);
	@updateq -solution_shown(~GAME,_, _, _) : participant(useragent,~GAME,1);
	@updateq +game(~GAME,~NAME,*1) :
		participant(useragent,~GAME,_) game(~GAME,~NAME,0);
	@update .new_game(useragent);
'>;
</applet>
<br>
<applet code="ButtonAgent" width=110 height=25>
<param name="text" value="More cards">
<param name="pressed_s" value='
	@update .more_cards(useragent);
'>;
</applet>
<br>
<applet code="ButtonAgent" width=110 height=25>
<param name="text" value="--- SET! ---">
<param name="pressed_s" value='
	@update .call_set(useragent);
'>;
</applet>
<applet code="ButtonAgent" width=110 height=25>
<param name="text" value="Show solutions">
<param name="pressed_s" value='
	@update .show_solutions(useragent);
'>;
</applet>
<br>
<applet code="ButtonAgent" width=110 height=25>
<param name="text" value="Remove solutions">
<param name="pressed_s" value='
	@update .end_show_solutions(useragent);
'>;
</applet>
<br>

</td><td>

<applet code="ObjectCanvasAgent" width=600 height=350>
<param name="init_s" value='
	@addquery graphical_object ~OBJ,"2",~IMG,~X+5,~Y+5 :
		setcard(~OBJ,~GAME,~IMG,~X,~Y) participant(useragent,~GAME,_);
	@addquery graphical_object ~OBJ, "0", "card_selected.gif", ~X, ~Y :
		setcard(~OBJ,~GAME, ~IMG, ~X, ~Y) selected(_, ~OBJ) 
		participant(_,~GAME,_);
	@addquery graphical_object ~OBJ, "1", "solution_card.gif", ~X+2, ~Y+2 :
		setcard(~OBJ,~GAME, ~IMG, ~X, ~Y) solution_shown(~GAME,~IMG, _, _) 
		participant(useragent,~GAME,_);
	@addquery graphical_object ~OBJ, "1", "solution_card.gif", ~X+2, ~Y+2 :
		setcard(~OBJ,~GAME, ~IMG, ~X, ~Y) solution_shown(~GAME,_, ~IMG, _) 
		participant(useragent,~GAME,_);
	@addquery graphical_object ~OBJ, "1", "solution_card.gif", ~X+2, ~Y+2 :
		setcard(~OBJ,~GAME, ~IMG, ~X, ~Y) solution_shown(~GAME,_, _, ~IMG) 
		participant(useragent,~GAME,_);
'> 
<param name="pointing_at_s" value = '
	@update state pointing_at(useragent, source);
'> 
<param name="selected_s" value = '
	if state and type=="right" : {{
		@updateq -selected(useragent, ~OBJ) :
			setcalled(useragent) pointing_at(useragent, ~OBJ);
	}}
	if state and type=="left" : {{
		@updateq +selected(useragent, ~OBJ) :
			setcalled(useragent) pointing_at(useragent, ~OBJ);
	}}
'>;
</applet>

</td></tr></table>


</BODY></HTML>

Start new game specification: webset_game.vs

// IN: useragent
@new label LabelAgent text="Name of game";
@new text TextFieldAgent width=170, height=50,trailer="",columns=30,
text_out_s = <<
	@wclose(self_window);
	@window("dummy",10,10);
	@new setgame SetGameAgent width=0, height=0,
	init_s = <<
		@update -setcard(_,self, _, _, _);
		@addquery new_game ~USER :
			new_game(~USER) participant(~USER,self,1);
		@addquery user_calls_set ~USER :
			call_set(~USER) participant(~USER,self,1);
		@addquery show_solutions ~USER :
			show_solutions(~USER) participant(~USER,self,1);
		@addquery end_show_solutions ~USER :
			end_show_solutions(~USER) participant(~USER,self,1);
		@addquery user_gets_card ~USER, ~C :
			selected(~USER,~O) setcard(~O,self,~C,_,_)
			participant(~USER,self,1);
		@addquery user_requests_new_cards ~USER :
			more_cards(~USER) participant(~USER,self,1);
	>> del_card_s = <<
		@updateq -solution(~c1, ~c2, ~c3) : solution(~c1, ~c2, ~c3)
			setcard(~c1,self,_,_,_)
			setcard(~c2,self,_,_,_)
			setcard(~c3,self,_,_,_);
		@update -solution_shown(self,_, _, _);
		@update -setcard(_,self, card, _, _);
	>> add_card_s = <<
		@new card_id;
		@update +setcard(card_id,self, card, xpos, ypos);
	>> user_scoring_s = <<
		@update -selected(user, _);
		@updateq -setcalled(~USER) : participant(~USER,self,_);
		@update .game_event(user, status);
	>> solution_s = <<
		@updateq +solution(~card1id, ~card2id, ~card3id) :
			setcard(~card1id,self,card1,_,_)
			setcard(~card2id,self,card2,_,_)
			setcard(~card3id,self,card3,_,_);
	>> set_called_s = <<
		@update +setcalled(user);
	>> game_over_s = <<
		@update .game_event(~USER,"GameOver") : participant(~USER,self,1);
	>>;
	@new solcycle RoundRobinAgent width=0, height=0,
	init_s = <<
		@addquery set ~C1, ~C2, ~C3 :
			solution(~CID1, ~CID2, ~CID3)
			setcard(~CID1,setgame,~C1,_,_)
			setcard(~CID2,setgame,~C2,_,_)
			setcard(~CID3,setgame,~C3,_,_);
		@addquery next ~USER :
			show_solutions(~USER) participant(~USER,setgame,1);
	>> next_s = <<
		if not set_empty :
			@update +solution_shown(setgame, *r0, *r1, *r2);
	>>;
	@update + game(setgame, text, 0) participant(useragent,*setgame,*0);
	@new trig TriggerAgent init_s = <<
		@addquery in "game",~name : game(setgame,~name,_);
	>> out_s = <<
		if not is_addition : @wclose(self_window);
	>>;
	@wadd(dummy,setgame_window);
	@wadd(dummy,solcycle_window);
	@wadd(dummy,trig_window);
>>;
@window("win",250,150);
@wadd(win,label_window);
@wadd(win,text_window);
@wadd(root,win);


Agent incorporating game rules: webset.vcl

imports {
	java.util.*,
	java.awt.*
}
agent SetGameAgent(
Integer xtile=new Integer(80), Integer ytile=new Integer(112),
String del_card_s, /* del_card(String card) */
String add_card_s, /* add_card(String card,Integer xpos,Integer ypos) */
String solution_s, /* solution(String card1,String card2,String card3) */
String user_scoring_s, /* Object user,String status */
String set_called_s, /* Object user */
String game_over_s /* */
) {

	handle new_game() { if (is_addition) {
		shuffleDeck();
		table_by_cards = new Hashtable();
		table_by_pos = new Hashtable();
		refillCards(12,false);
		user_called=null;
		game_on=true;
		System.out.println(generateSolutions());
	} }

	handle show_solutions() { if (game_on) if (is_addition) {
		for (Enumeration e=generateSolutions().elements();
		e.hasMoreElements(); ) {
			String [] sol = (String [])e.nextElement();
			execscript(solution_s,
				"card1",sol[0], "card2",sol[1], "card3",sol[2] );
			showing_solutions=true;
		}
	} }

	handle end_show_solutions() { if (showing_solutions) if (is_addition) {
		for (Enumeration e=generateSolutions().elements();
		e.hasMoreElements(); ) {
			String [] sol = (String [])e.nextElement();
			removeCard(sol[0]);
			removeCard(sol[1]);
			removeCard(sol[2]);
			if (!refillCards(12,false)) {
				execscript(game_over_s);
				game_on=false;
			}
		}
		showing_solutions=false;
	} }

	startupdate {
		ignore_get_results=false;
		System.out.println("Startupdate");
	}

	endupdate {
		System.out.println("Endupdate");
		ignore_get_results=false;
	}

	handle user_calls_set(Object user) { if (game_on && !showing_solutions)
	if (is_addition) {
			if (user_called != null) {
				execscript(user_scoring_s,
					"user",user, "status","AlreadyCalled");
				return;
			}
			System.out.println("Call set");
			user_called=user;
			execscript(set_called_s,"user",user_called);
			call_end_timer=new HeartbeatGen(self,"call_expired",3000,true);
	} }

	handle user_gets_card(Object user,String card) { if (is_addition) {
		if (!user.equals(user_called)) {
			execscript(user_scoring_s, "user",user, "status","NoCall");
			return;
		}
		/* did we get 3 cards? */
		String [] cards = new String[3];
		int cardnr=0;
		foreach (Object u,String c; results("user_gets_card") ) {
			if (u.equals(user)) cards[cardnr++]=c;
			if (cardnr==3) break;
		}
		if (cardnr != 3) return;
		/* handle selection */
		call_end_timer.stop();
		user_called=null;
		System.out.println("Got cards "+cards[0]+" "+cards[1]+" "+cards[2]);
		if (!table_by_cards.containsKey(cards[0])
		||  !table_by_cards.containsKey(cards[1])
		||  !table_by_cards.containsKey(cards[2])) {
			execscript(user_scoring_s, "user",user, "status", "InternalError");
		} else
		if (isSet(cards[0],cards[1],cards[2])) {
			removeCard(cards[0]);
			removeCard(cards[1]);
			removeCard(cards[2]);
			execscript(user_scoring_s, "user",user, "status", "GotSet");
			if (!refillCards(3,false)) {
				execscript(game_over_s);
				game_on=false;
			}
			System.out.println(generateSolutions());
		} else {
			execscript(user_scoring_s, "user",user, "status", "BadSet");
		}
	} }

	handle user_requests_new_cards(Integer user) {
		if (game_on && user_called==null) if (is_addition) {
			if (!refillCards(3,true)) {
				execscript(game_over_s);
				game_on=false;
			}
		}
	}

	handleinternal call_expired() {
		if (user_called==null) return;
		execscript(user_scoring_s, "user",user_called, "status","CallExpired");
		user_called=null;
	}

  inline {
  	HeartbeatGen call_end_timer;
  	boolean ignore_get_results=false;
	boolean game_on=false,showing_solutions=false;
	Vector deck=null;
	Hashtable table_by_cards=null,table_by_pos=null;
	Object user_called=null;

	void shuffleDeck() {
		deck=new Vector(100,100);
		for (int i=0; i<3; i++)
			for (int j=0; j<3; j++)
				for (int k=0; k<3; k++)
					for (int l=0; l<3; l++)
						deck.addElement(""+i+j+k+l);
		Random r = new Random();
		for (int i=0; i<200; i++) {
			int card1=(int)(r.nextFloat()*deck.size());
			int card2=(int)(r.nextFloat()*deck.size());
			Object tmp = deck.elementAt(card1);
			deck.setElementAt(deck.elementAt(card2), card1);
			deck.setElementAt(tmp, card2);
		}
	}

	boolean aspectIsSet(String card1,String card2,String card3, int aspect) {
		if (card1.charAt(aspect) == card2.charAt(aspect)
		&&  card1.charAt(aspect) == card3.charAt(aspect)
		&&  card2.charAt(aspect) == card3.charAt(aspect)) return true;
		if (card1.charAt(aspect) != card2.charAt(aspect)
		&&  card1.charAt(aspect) != card3.charAt(aspect)
		&&  card2.charAt(aspect) != card3.charAt(aspect)) return true;
		return false;
	}

	boolean isSet(String card1, String card2, String card3) {
		return aspectIsSet(card1,card2,card3,0)
		&&     aspectIsSet(card1,card2,card3,1)
		&&     aspectIsSet(card1,card2,card3,2)
		&&     aspectIsSet(card1,card2,card3,3);
	}
	/* returns false when no more cards could be drawn */
	boolean refillCards(int nr_cards, boolean do_extend) {
		int nr_laid=0;
		for (int x=0; x< (do_extend ? 7 : 4); x++) {
			for (int y=0; y<3; y++) {
				String card = (String) table_by_pos.get(
					new ComparableArray(new Integer(x), new Integer(y)));
				if (card == null) {
					if (!drawCard(x,y)) return nr_laid > 0;
					nr_laid++;
					if (nr_laid == nr_cards) return true;
				}
			}
		}
		return true;
	}

	boolean drawCard(int xpos, int ypos) {
		if (deck.size() == 0) return false;
		String card = (String)deck.elementAt(0);
		ComparableArray cardpos =
			new ComparableArray(new Integer(xpos),new Integer(ypos));
		deck.removeElementAt(0);
		table_by_cards.put(card,cardpos);
		table_by_pos.put(cardpos, card);
		execscript(add_card_s,
			"card",card,
			"xpos",new Integer(xpos*xtile.intValue()),
			"ypos",new Integer(ypos*ytile.intValue()) );
		return true;
	}

	/* does nothing if card is not on table */
	void removeCard(String card) {
		ComparableArray pos = (ComparableArray)table_by_cards.get(card);
		if (table_by_cards.containsKey(card)) {
			table_by_cards.remove(card);
			table_by_pos.remove(pos);
			execscript(del_card_s, "card",card);
		}
	}

	Vector generateSolutions() {
		String [] cards = new String[table_by_cards.size()];
		Vector solutions = new Vector(20,20);
		int i=0;
		for (Enumeration e=table_by_cards.keys(); e.hasMoreElements(); i++) {
			cards[i] = (String)e.nextElement();
		}
		for (int a=0; a<cards.length-2; a++) {
			for (int b=a+1; b<cards.length-1; b++) {
				for (int c=b+1; c<cards.length; c++) {
					if (isSet(cards[a],cards[b],cards[c])) {
						String [] sol = new String[3];
						sol[0]=cards[a];
						sol[1]=cards[b];
						sol[2]=cards[c];
						solutions.addElement(sol);
					}
				}
			}
		}
		return solutions;
	}
  }
}