// Textris.java -- Richard W. DeVaul -- Thu Nov 6 21:54:26 EST 1997
//
// Solution to Problem 2 of Problem Set 8 of MAS962, Fall 1997
/* $Id$ */
/*
$Log$
*/
import java.applet.*;
import java.awt.*;
import java.io.*;
import java.util.*;
import java.net.*;
public class Textris extends Applet {
// ***************
// ** Constants **
// ***************
public static final int PORT = 6801;
protected static final int MAXWORDS = 40;
protected static final int ROWS = 20;
protected static final int COLUMNS = 20;
protected static final boolean debug =false;
/* 5 10 15 20! 25*/
protected static final String instructions =
"Welcome to Textris.\n\n"+
"Textris is a dynamic\n"+
"wordgame similar to\n"+
"Scrabble and tetris.\n\n"+
"Words typed in other\n"+
"clients move down\n"+
"through text area to\n"+
"the right. Words you\n"+
"type move across.\n"+
"You score when they\n"+
"intersect on\n"+
"matching letters.";
protected static final String about =
"Textris was written\n"+
"by Richard W. DeVaul\n"+
"for MAS.962, Digital\n"+
"Typography taught by\n"+
"Professor John Maeda\n"+
"at the MIT Media Lab\n"+
"fall semester 1997.\n\n";
// *****************
// ** Member data **
// *****************
// Special font textarea
protected GridAreaTextris textArea;
protected GridAreaTextris textArea1;
protected GridArea textArea2;
protected TextrisTitle title;
protected Thread titleThread;
// AWT interface objects
protected Label label;
protected Label label2;
protected Label label3;
protected Panel panel0;
protected Panel panel1;
//protected Panel panel2;
protected Button button0;
protected Button button1;
protected Button button2;
boolean aboutShowing = false;
protected int countDown = 0;
protected int wordRow = 0;
// Client stuff
protected int clientID = -1;
protected Socket s;
protected DataInputStream in;
protected PrintStream out;
protected PS10Listener listener;
protected String serverHost;
/*
* Member data specifically game related.
*/
// current row for "launching" word.
int row;
// current score.
int score;
// string buffer representing current word.
StringBuffer word;
// The wordObject is an internal class
// representing a single word in game
// space and all its relevent
// attributes.
class wordObject extends Object {
public String word;
public int clientID;
public int colorIndex;
public int x,y;
public int xOld,yOld;
public int dx,dy;
public boolean horizontal;
public boolean oldHorizontal;
public boolean dead;
public void copy(wordObject source) {
word = source.word;
clientID = source.clientID;
colorIndex = source.colorIndex;
x = source.x;
y = source.y;
xOld = source.xOld;
yOld = source.yOld;
dx = source.dx;
dy = source.dy;
horizontal = source.horizontal;
oldHorizontal = source.oldHorizontal;
dead = source.dead;
}
}
/* The run() method is the "game loop" which updates the state of
* the game independent of user input. User I/O is handled through
* the standard Java event handing techniques. */
class Runnit extends Thread {
protected int delay;
protected GridAreaTextris textArea;
// Counters for incomming and outgoing
// words.
protected int inCount, outCount;
protected wordObject list[][];
protected wordObject inList[], outList[];
protected wordObject inNewList[],outNewList[];
protected wordObject swapList[];
protected boolean running = true;
Runnit(GridAreaTextris textArea,
int delay) {
this.textArea = textArea;
this.delay = delay;
// Allocate lists
list = new wordObject[4][MAXWORDS];
int i,j;
for (i = 0; i < 4; i++) {
for (j = 0; j < MAXWORDS; j++) {
list[i][j] = new wordObject();
}
}
// Assign the appropriate references.
inList = list[0];
inNewList = list[1];
outList = list[2];
outNewList = list[3];
// Set the counters to zero
inCount = outCount = 0;
}
// The addWord method must be
// synchronized to avoid a conflict
// with processList()
public synchronized void addWord(String word, int client) {
if (!running) {
return;
}
wordObject current;
if (client == clientID) {
row = textArea.row;
if (row > textArea.maxRow) {
row = textArea.maxRow;
}
countDown = word.length()+1;
wordRow = row;
current = outList[outCount++];
current.horizontal = true;
current.x = -10*word.length();
current.y = row*10;
current.dx = 10;
current.dy = 0;
}
else {
current = inList[inCount++];
current.horizontal = false;
current.x = (int)(Math.random()*(double)((COLUMNS-1)*10))+10;
current.y = -10*word.length();;
current.dx = 0;
current.dy = (int)Math.round(Math.random()*4)+1;
}
current.word = word;
current.clientID = client;
current.colorIndex = client%256;
current.dead= false;
}
// This method is synchronized to
// avoid conflict with addWord.
protected synchronized void processList() {
int i;
wordObject current,old;
/* Step through the coordinates of incoming and outgoing words,
* flagging as dead any which have moved off the screen.
*/
if (debug) {
System.out.println("inCount: " + inCount +" outCount: " +
outCount);
}
// Do the "in" list
for (i = 0; i < inCount; i++) {
if (inList[i].x > COLUMNS*10 || inList[i].y > ROWS*10) {
inList[i].dead = true;
}
}
// Do the "out" list
for (i = 0; i < outCount; i++) {
if (outList[i].x > COLUMNS*10 || outList[i].y > ROWS*10) {
outList[i].dead = true;
}
}
/*
* Now create updated lists, including only live words.
*/
int j=0,k=0;
// Do the "in" lists
for (i = 0; i < inCount; i++) {
if (!inList[i].dead) {
current = inNewList[j];
current.copy(inList[i]);
current.xOld = current.x;
current.yOld = current.y;
current.oldHorizontal = current.horizontal;
current.x += current.dx;
current.y += current.dy;
j++;
}
}
// Do the "out" lists
for (i = 0; i < outCount; i++) {
if (!outList[i].dead) {
current =outNewList[k];
current.copy(outList[i]);
current.xOld = current.x;
current.yOld = current.y;
current.x += current.dx;
current.y += current.dy;
if (current.y > textArea.maxRow*10) {
current.y = textArea.maxRow*10;
}
k++;
}
}
/* Update inCount and outCount to reflect the lengths of the new
* lists.
*/
inCount = j;
outCount = k;
/* Swap the list references so that the new lists become the
* current lists. The temptation with java garbage collection is
* to just allocated new lists, of course, but that is a _serious_
* performance hit... The downside to garbage collection is that
* one tends not to think about how much it costs to pay the
* garbage man.
*/
// Do the "in" list.
swapList = inList;
inList = inNewList;
inNewList = swapList;
// Do the "out" list.
swapList = outList;
outList = outNewList;
outNewList = swapList;
/*
* Check to see if any "in" words have reached bottom of screen
*/
for (i = 0; i < inCount; i++) {
current = inList[i];
if (current.y/10+current.word.length()-1 >= textArea.maxRow
&& !current.horizontal) {
current.horizontal = true;
}
if (current.y/10 >= textArea.maxRow && current.horizontal
&& current.dy > 0 ) {
char array[] = new char[COLUMNS];
for (j=0; j < COLUMNS; j++) {
array[j] = '-';
}
for (j =0; j < current.word.length() && j+current.x/10 < COLUMNS; j++) {
array[current.x/10+j] = current.word.charAt(j);
}
current.word = new String(array);
current.x = 0;
current.dx = 0;
current.dy = 0;
textArea.maxRow--;
if (textArea.row > textArea.maxRow) {
textArea.updateCursor(-1,0,true);
}
}
}
if (debug) {
System.out.println("Exiting processList()");
}
}
protected void renderInList() {
int i;
wordObject current,outgoing;
char intersect;
for (i = 0; i < inCount; i++) {
current = inList[i];
// render and check for collision
intersect = textArea.renderWord(current.word,
current.x/10,
current.y/10,
current.horizontal,
current.colorIndex);
if (intersect != (char)0 && current.y/10 < textArea.maxRow) {
// Collision happened. increment score
// and change vector.
int j,k;
for (j = 0; j < current.word.length(); j++ ) {
if (current.word.charAt(j) != intersect) {
score++;
}
}
// label3.setText("Score: " + score);
current.colorIndex = (current.colorIndex+1)%256;
current.dx =1;
current.dy =0;
}
}
}
protected void renderScore() {
textArea1.setText("Score: " + score + "\n " +
word.toString());
}
protected void renderOutList() {
int i;
wordObject current;
for (i = 0; i < outCount; i++) {
current = outList[i];
textArea.renderWord(current.word,
current.x/10,
current.y/10,
current.horizontal,
current.colorIndex);
}
}
protected void clearInList() {
int i;
wordObject current;
for (i = 0; i < inCount; i++) {
current = inList[i];
// Clear
textArea.clearWord(current.word,
current.xOld/10,
current.yOld/10,
current.oldHorizontal);
}
}
protected void clearOutList() {
int i;
wordObject current;
for (i = 0; i < outCount; i++) {
current = outList[i];
textArea.clearWord(current.word,
current.xOld/10,
current.yOld/10,
current.horizontal);
}
}
public void run() {
running = true;
int i;
try {
while(running) {
if (debug) {
System.out.println("Runnit alive.");
}
if (textArea.maxRow <=1) {
running = false;
}
countDown--;
/*
* Process the lists
*/
processList();
// textArea.clearRaster();
/*
* "Undraw" the words.
*/
clearInList();
clearOutList();
/*
* Draw the score and current word
*/
// clearScore();
renderScore();
/*
* Render the "out" list to the text area raster.
*/
renderOutList();
/*
* Do collision detection and render the "in" list.
*/
renderInList();
/*
* Update the display
*/
textArea.update(getGraphics());
try {
sleep(delay);
}
catch (InterruptedException foo) {
}
}
textArea.clearRaster();
String soSorry = "--> Game Over <--";
i = clientID;
while (true) {
textArea.renderWord(soSorry,
(COLUMNS-soSorry.length())/2,
ROWS/2,
true,
(i++)%256);
textArea.update(getGraphics());
try {
sleep(delay);
}
catch (InterruptedException foo) {
}
}
}
finally {
if(debug) {
System.out.println("Runnit died -- stopped?");
}
}
}
public int getInCount() {
return inCount;
}
public int getOutCount() {
return outCount;
}
}
protected Runnit runnit;
// ********************
// ** Member methods **
// ********************
// Init is called by the superclass to
// set up the applet.
public void init() {
int i,j;
// Get new stringBuffer.
word = new StringBuffer();
// Allocate the wordObject lists.
// Initialize the server stuff.
if(debug) {
System.out.println("Init called ... ");
}
// serverHost = this.getCodeBase().getHost();
// *** REMOVE FOR LOCAL DEBUG ****
serverHost = "acg.media.mit.edu";
// Lay out the applet
layoutApplet();
}
// layoutApplet is called to lay out
// the various components of the user
// interface. It is broken out as a
// seperate method so that it can be
// overridden in derived classes.
protected void layoutApplet() {
this.setLayout(new BorderLayout(5,5));
textArea = new GridAreaTextris(this,ROWS,COLUMNS,
220,330,
220,330);
textArea1 = new GridAreaTextris(this,2,15,
200,50,
200,50);
textArea1.minRow = 1;
textArea2 = new GridArea(this,15,21,
200,200,
200,200);
title = new TextrisTitle(200,50,200);
textArea2.setText(instructions);
textArea.editable = false;
textArea1.editable = false;
textArea2.editable = false;
this.add("Center",textArea);
button0 = new Button("Reset Game");
button1 = new Button("About Textris");
button2 = new Button("Instructions");
panel0 = new Panel();
panel1 = new Panel();
// panel2 = new Panel();
panel0.setLayout(new FlowLayout(FlowLayout.LEFT,15,5));
panel1.setLayout(new BorderLayout(5,5));
// panel2.setLayout(new FlowLayout(FlowLayout.CENTER,5,5));
panel0.add(button0);
panel0.add(button1);
panel0.add(button2);
this.add("South",panel0);
panel1.add("North",title);
panel1.add("Center",textArea2);
panel1.add("South",textArea1);
this.add("West",panel1);
}
// Start is called whenever the page
// is shown.
public void start() {
super.start();
textArea.start();
textArea1.start();
textArea2.start();
if(debug) {
System.out.println("Start called");
}
try {
s = new Socket(serverHost, PORT);
in = new DataInputStream(s.getInputStream());
out = new PrintStream(s.getOutputStream());
listener = new PS10Listener(this, in);
if(debug) {
System.out.println("Connected to "
+ s.getInetAddress().getHostName()
+ ":" + s.getPort());
}
}
catch (IOException e) { System.out.println(e.toString()); }
titleThread = new Thread(title);
titleThread.start();
reset();
}
// Stop: notify the server, tell our listener thread to stop, and destroy
// all the thread objects (to catch possible errors)
public void stop() {
if(debug) {
System.out.println("stop called - disconnecting from server.");
}
sendMessage("BYE");
try {
listener.stop();
runnit.stop();
titleThread.stop();
}
catch (SecurityException foo) {
System.out.println("Unable to stop threads.");
}
listener = null;
in = null;
out = null;
s = null;
super.stop();
}
public void setID(int id) {
if (clientID == -1) {
clientID = id;
if(debug) {
System.out.println("ID set to " + id);
}
textArea.setColorTable(Color.white,
clientID%256);
// label.setText("ID: " + clientID);
} else {
System.out.println("Refused to set our ID to " + id);
}
}
public void receiveMessage(int otherID, String msg) {
System.out.println(otherID + "--> " + msg);
if (otherID != clientID && msg.charAt(0) != '<') {
// Ugly hack to manage some
// non-complient behavior.
StringTokenizer st = new StringTokenizer(msg);
String token;
while (st.hasMoreTokens()) {
token = st.nextToken();
runnit.addWord(token,otherID);
textArea2.appendString(otherID + ": " + token);
}
}
}
public void sendMessage(String msg) {
runnit.addWord(msg,clientID);
if (out != null) {
if (debug) {
System.out.println(clientID + " "+msg);
}
out.println(clientID + " "+msg);
}
else {
System.out.println("Tried to send a message, but wasn't connected.");
}
}
public boolean handleEvent(Event event) {
if (debug) {
if (runnit != null) {
if (runnit.isAlive()) {
System.out.println("Runnit is alive" + runnit);
System.out.println("Runnit priority: " + runnit.getPriority() +
" Max priority: " + runnit.MAX_PRIORITY);
}
else {
System.out.println("Runnint not alive" + runnit);
}
if (runnit.isInterrupted()) {
System.out.println("Runnit is interrupted");
}
}
}
switch(event.id) {
case Event.KEY_ACTION:
processActionKey(event.key);
break;
case Event.KEY_PRESS:
if (!specialKey((char)event.key)) {
word.append((char)event.key);
}
if (debug) {
System.out.println(word.toString());
}
// label2.setText("Current word:"+ word.toString());
break;
default:
if (event.target == button2 && aboutShowing) {
textArea2.setText(instructions);
aboutShowing = false;
break;
}
if (event.target == button1 && !aboutShowing) {
textArea2.setText(about + getStateString());
aboutShowing = true;
break;
}
if (event.target == button0) {
reset();
return true;
}
return super.handleEvent(event);
}
return true;
}
protected String getStateString() {
String rstring = "Client ID:" + clientID+"\n\n";
if (runnit.isAlive()) {
rstring += "Game thread alive\n";
}
else {
rstring += "Game thread dead!\n";
}
if (listener.isAlive()) {
rstring += "Listener alive\n\n";
}
else {
rstring += "Listener dead!\n\n";
}
rstring += "(C) 1997 MIT and\nDeVaul Java Studios.";
return rstring;
}
protected boolean specialKey(char key) {
int i;
int length = word.length();
switch (key) {
case '\n':
if (textArea.row != wordRow || countDown <= 0) {
sendMessage(word.toString());
word.setLength(0);
}
out.println(clientID + "
");
break;
case '\r':
case '\t':
case ' ':
if (textArea.row != wordRow || countDown <= 0) {
sendMessage(word.toString());
word.setLength(0);
}
out.println(clientID + " ");
break;
case '\b':
if (length > 0) {
word.setLength(length-1);
}
break;
case '\u007f':
word.setLength(0);
break;
default:
return false;
}
return true;
}
// Process action keys
protected void processActionKey(int actionKey) {
switch (actionKey) {
case Event.UP:
if (row > 0) {
textArea.updateCursor(-1,0,true);
}
break;
case Event.DOWN:
if (row < ROWS-1) {
textArea.updateCursor(1,0,true);
}
break;
}
}
protected void reset() {
textArea.clearRaster();
score = 0;
textArea.maxRow = ROWS-1;
if (runnit != null) {
runnit.stop();
}
runnit = new Runnit(textArea,200);
try {
runnit.start();
runnit.setPriority(runnit.MAX_PRIORITY);
}
catch (IllegalThreadStateException foo) {
System.out.println("Thread was already started");
}
}
}
class TextrisTitle extends Canvas implements Runnable {
// ***************
// ** Constants **
// ***************
protected static final int NUMSTEPS = 7;
protected static final int NUMCOLORS = 100;
protected static final char logo[] = {'T','E','X','T','R','I','S'};
// *****************
// ** Member data **
// *****************
MyFont font[];
// Image used for double buffering.
protected Image image;
// Off-screen graphics context
protected Graphics offScreenGraphics;
// On-screen graphics contenxt
protected Graphics onScreenGraphics;
// Prefered and minimum dimensions
Dimension prefSize, minSize, howBig;
// Size of a letter box
double width,height;
// index for array of fonts.
int index;
// delay
int delay;
// Color table for iridescent colors
protected Color colorTable[];
// ********************
// ** Member methods **
// ********************
public TextrisTitle(int width,int height,
int delay) {
prefSize = new Dimension(width,height);
minSize = new Dimension(width,height);
font = new MyFont[NUMSTEPS];
this.delay = delay;
colorTable = new Color[NUMCOLORS];
int i;
for (i = 0; i < NUMCOLORS; i++) {
colorTable[i] = new Color(Color.HSBtoRGB((float)i/(float)(NUMCOLORS),
1.0f,1.0f));
}
}
public Dimension preferredSize() {
return prefSize;
}
// Return the minimum size of the
// component.
public synchronized Dimension minimumSize() {
return minSize;
}
public void paint(Graphics g) {
int i;
int x,y;
MyFont current;
y = (int)(height/2.0);
for (i = 0; i < 7; i++) {
x = (int)((double)i*width + width/2.0);
// current = font[(i+index)%NUMSTEPS];
current = font[(i)%NUMSTEPS];
g.setColor(colorTable[(i+index)%NUMCOLORS]);
current.drawCharacter(g,
logo[i],
x-current.halfWidth,
y+current.halfHeight);
}
}
public void run() {
onScreenGraphics = getGraphics();
howBig = size();
image = createImage(howBig.width,howBig.height);
offScreenGraphics = image.getGraphics();
setBackground(Color.black);
setForeground(Color.white);
width = (double)(howBig.width/7);
height =(double)(howBig.height);
offScreenGraphics.setColor(Color.black);
offScreenGraphics.fillRect(0,0,howBig.width,howBig.height);
int i;
double p;
double ns = (double)NUMSTEPS;
for (i = 0; i < NUMSTEPS; i++) {
p = (double)i*Math.PI/ns;
font[i] =
new MyFont(width * (2.0 -Math.sin(p))/2.0,
height * (2.0 - Math.sin(p))/2.0);
}
try {
while (true) {
paint(offScreenGraphics);
onScreenGraphics.drawImage(image,0,0,Color.black,this);
index++;
try {
Thread.sleep(delay);
}
catch (InterruptedException foo) {
}
}
}
finally { System.out.println("Logo died -- stopped?"); }
}
}