/*lightside connect 4 made by Sieuwert van Otterloo, Utrecht 1998. this applet plays connect four. mouse support, 3 difficluty settings, undo button. To use it: insert the tag < PARAM NAME=pixels VALUE="30"> in a html document. If available, the images lside0.gif,lside1.gif,lside2.gif are used. pixels is the size of these images. 30 is default value. more information: www.students.cs.uu.nl/~smotterl mailto:smotterl@cs.uu.nl */ import java.awt.*; import java.net.URL; import java.applet.*; public class lightside extends java.applet.Applet /*The applet displays a board and buttons. It listens to the buttons and mouseclicks. Important methods: init makes all objects. usercolumn(int) recieves mouseclicks. action recieves buttonclicks. catchamove recieves the moves the computer wants to do. newgame,undo,setdifficulty an lsfirst are called when their buttons are pressed.*/ { board B; boardview Bview; Button newgame, ls_first,undo,difficulty; boolean thinking=false; mover Sieuwert; int userside;//the side the user played with in this game. Label message; boolean fullhard; int undos; /* fullhard=true means that all the mover were done playing hard. undos is the number of times the user pressed undo.*/ public void init() { String s=getParameter("pixels"); System.out.println(s); if(s==null) s="30"; int pixels=Integer.parseInt(s); B=new board(); Bview=new boardview(B,this,pixels); B.setview(Bview); Sieuwert=new mover(B,this); setBackground(Color.white);//lightblue background Panel buttonpanel=new Panel();//panel to keep buttons together buttonpanel.setLayout(new GridLayout(1,4)); buttonpanel.add(newgame=new Button("new game")); buttonpanel.add(ls_first=new Button("PC first")); buttonpanel.add(difficulty=new Button("")); buttonpanel.add(undo=new Button("undo move")); Panel superpanel=new Panel(); superpanel.setLayout(new GridLayout(2,1)); superpanel.add(buttonpanel); superpanel.add(message=new Label()); add("North",Bview); add("South", superpanel); setdifficulty(); newgame(); } void setmessage(String s) /*The string s will be shown to the user.*/ {message.setText(s);} public void usercolumn(int c) /*If the computer is thinking, we do nothing. Else we try to do the user's move, and a computermove.*/ { synchronized(this) { if(thinking) return; if(B.status!=B.Q) { setmessage("Press newgame for a rematch."); return; } if(!B.canmove(c)) { setmessage("That is no valid move."); return; } B.domove(c); if(B.status!=B.Q) { resultmessage(); return; } if(thinking!=true) {thinking=true; if(Sieuwert.strength!=Sieuwert.HARD) fullhard=false; setmessage("Thinking..."); Sieuwert.think_a_move(); } } } void resultmessage() /*Tell the user wether he won or not. the messages differ depending on the way you win/lose.*/ { if(B.status==B.Q) return; if(B.status==0) setmessage("You draw. It could be worse."); else if(B.getplayer()==userside) { if(fullhard) setmessage("You lose. Try it easy?"); else setmessage("You lose. Get some practice."); } else { if(!fullhard) setmessage("Congratulations. Try playing hard?"); else if(undos!=0) setmessage("Congratulations. Can you win without undo?"); else setmessage("Congratulations, you master the game."); } } void catchamove(int c) /*Some time after we ask the mover for a move, he will call this method to give us his move. We will do his move.*/ { synchronized(this) { if(thinking==false) return; B.domove(c); setmessage("Lightside moved. You can."); resultmessage(); thinking=false; } } void newgame() { setmessage("throw a stone or press PC first"); undos=0; B.clear(); userside=B.P1; fullhard=true; } void setdifficulty() { Sieuwert.changestrength(); difficulty.setLabel(Sieuwert.strengthname[Sieuwert.strength]); } void lsfirst() {//the user asks the computer to move first if(B.moves==0)//this is only possible for the first move { thinking=true; Sieuwert.think_a_move(); userside=B.P2; setmessage("computer opens. please wait"); } else setmessage("The board is not empty."); } void undo() { undos++; B.undomove();//undo two moves if(B.getplayer()!=userside) B.undomove();//so the user keeps the same color setmessage("You can move."); } public boolean action(Event ev, Object O) /*If a button was clicked, the browser calls this function. If a computerplayer is thinking, we do nothing. Else, we do the right action. */ { synchronized(this) { if(thinking) return true; if(ev.target==newgame) {newgame();} else if(ev.target==ls_first) lsfirst(); else if(ev.target==undo) undo(); else if(ev.target==difficulty) setdifficulty(); return true; } } } class board /*A board is the board of a connect 4 games. You can do and undo moves on it, you can read who is to move [getplayer()], you can see where the stones are(look in A[7][6]), you can see wether and how the game ended: status.*/ { final static int P1=0,P2=1,EMPTY=2; //the possible values of A final static int Q=200379; //possible value of status int[][] A;//the main array, with the positions of all the stones int moves;//the number of moves done int[] move;// the moves done int status; /*0: game ended in draw. Q: game not ended yet (Q of questionmark.it means 'result unknown') 1 player to move has won -1 last moving player has won*/ boardview myview; board()//create an empty board { A= new int[7][6];//new table for(int i=0;i<7;i++) for(int j=0;j<6;j++) A[i][j]=EMPTY;//make table empty moves=0;//no moves done status=Q;//game is still undecided move=new int[42];//to store all moves } void setview(boardview bv) {myview=bv;} int getplayer() //returns from P1 or P2 the player that is about to move {return moves%2;} public board boardclone()//clone this board: make a copy {board b=new board(); b.moves=moves; b.status=status; for(int i=0;i<7;i++) for(int j=0;j<6;j++) b.A[i][j]=A[i][j]; for(int i=0;i<42;i++) b.move[i]=move[i]; return b; //the view is not copied. } void clear() {//wipe clean this board for(int i=0;i<7;i++) for(int j=0;j<6;j++) A[i][j]=EMPTY; moves=0; status=Q; if(myview!=null) myview.repaint(); } int height(int column) { //lowest free field in column:one of {0,1,..,6} for(int i=0;i<6;i++) if(A[column][i]==EMPTY)//search empty field return i; return 6;//nothing free: first free field is not on the board } boolean canmove(int column)// is this move legal? //is there room in the column, and is the game not finished {return (A[column][5]==EMPTY&&status==Q) ;} void domove(int c) {//the move is done, and the view is notified synchronized(this) {fastdomove(c);} if(myview!=null) myview.repaint(); } void fastdomove(int column)//do this move { A[column][height(column)]=getplayer();//put stone in column move[moves]=column;//register the move moves++;//a move was done status=score();//check wether the game ended } void undomove() { fastundomove(); if(myview!=null) myview.repaint(); } void fastundomove() { if(moves==0) return;//cannot undo moves--; A[move[moves]][height(move[moves])-1]=EMPTY;//remove a stone status=Q;//game is undecided } boolean onboard(int x,int y)//is this field still in the board? {return x>=0&&x<7&&y>=0&&y<6;} int score() /*Calculate the status of the game. We have to check wether someone has won, by searching four connected fields. If not, check if it is a draw. Else, it is still undecided (Q)*/ { if(moves==0) return Q; /*If there are are 4 connected, one of the four is the latest move, because one move back the game was undecided. We will search from the former move (A[fx][fy]).*/ int fx=move[moves-1]; int fy=height(fx)-1; int[] vx= { 0, 1, 1,-1}; int[] vy= { 1, 0, 1, 1}; /*We search in 4 directions: up, right, rightup,rightdown.*/ int fiches,x,y; for(int d=0;d<4;d++) { fiches=0; x=fx; y=fy; for(int i=0;i<4;i++) {//count fiches of the opposite colour in direction d if(onboard(x,y)&& A[x][y]==1-(getplayer()) ) fiches++; else break; x+=vx[d]; y+=vy[d]; } x=fx; y=fy; for(int i=0;i<3;i++) {//look for stones in the opposite direction. x-=vx[d]; y-=vy[d]; if(onboard(x,y)&& A[x][y]==1-(getplayer()) ) fiches++; else break; } /*If the player has more than 4 stones in this direction, he wins*/ if(fiches>=4) return -1; } /*If A contains 42 stones, it is full, and the game is a draw.*/ if(moves==42) return 0; return Q; } int grouppoints(int a,int b,int c,int d) { int points1=0,points2=0; /*group = {four fields in a connected line} A group with both players in it is worthless to both. No one can use it to win. If you have stones in it, and the opponent not, you get a point per stone. This method returns scores for P1.*/ if(a==P1) points1++; else if(a==P2) points2++; if(b==P1) points1++; else if(b==P2) points2++; if(c==P1) points1++; else if(c==P2) points2++; if(d==P1) points1++; else if(d==P2) points2++; if(points1==0) return -points2; else if(points2==0) return points1; return 0;//worthless group } int groupestimate() /*Groupestimate returns a number that tells something about how good the current player is doing. The higher the better. It is allways between -1000(sure loss) and 1000 (sure win). the computer must simply optimize this function in his moves.*/ { /*Groupestimate sums the grouppoints. The more points a player gets, the more possible groups he can win with he has, and the better his chances of winning.*/ int total=0; //vertical groups: for(int i=0;i<7;i++) for(int j=0;j<3;j++) total+=grouppoints(A[i][j],A[i][j+1],A[i][j+2],A[i][j+3]); for(int i=0;i<4;i++) { //horizontal groups: for(int j=0;j<6;j++) total+=grouppoints(A[i][j],A[i+1][j],A[i+2][j],A[i+3][j]); //diagonal groups: for(int j=0;j<3;j++) { total+= grouppoints(A[i][j],A[i+1][j+1],A[i+2][j+2],A[i+3][j+3]); total+= grouppoints(A[i+3][j],A[i+2][j+1],A[i+1][j+2],A[i][j+3]); } } /*we counted for P1, because grouppoints does. We must return for the player to move.*/ if(getplayer()==P1) return total; return -total; } } class boardview extends Canvas /*A boardview can show a board onscreen in a Canvas It listens to mouseclicks, and reports them to the applet*/ { board b;//the board that is viewed Color[] color;//the colors of the stones int size;//the size of 1 stone lightside applet;//the applet that owns this view long timemillis; Image stone[]; public boardview(board bb, lightside l,int param1) {//board to show, applet to throw mouse events at size=param1; applet=l; b=bb; color= new Color[2]; color[0]= Color.red; color[1]= Color.green; stone = new Image[3]; try { stone[0]=applet.getImage(new URL(applet.getDocumentBase(),"lside0.gif")); stone[1]=applet.getImage(new URL(applet.getDocumentBase(),"lside1.gif")); stone[2]=applet.getImage(new URL(applet.getDocumentBase(),"lside2.gif")); } catch(java.net.MalformedURLException e) {applet.setmessage("loading stone?.gif failed. (?=0,1,2)");} resize(7*size,6*size+20);//leave room for text repaint(); timemillis=System.currentTimeMillis(); } public boolean mouseDown(Event evt,int x,int y) {// is called by browser at every mouseclick long now=System.currentTimeMillis(); if(now-timemillis<300) return true; timemillis=now; /*It was possible in some browsers to crash the computer by mouseclicking often in a short time. To prevent this, we ignore clicks that come too fast.*/ if(x>=0&&x<7*size&&y>=0&&y<=6*size) applet.usercolumn(x/size); return true; } public void update(Graphics g) { paint(g);}//avoids flickering public void paint(Graphics g) //paint it. { for(int i=0;i<7;i++) for(int j=0;j<6;j++) drawstone(g,i*size,(5-j)*size,b.A[i][j]); g.setColor(Color.white); g.fillRect(0,6*size,7*size,20); g.setColor(Color.black);//text in black for(int c=0;c<7;c++) g.fillRect(c*size+3,6*size,size-6,3); if(b.status==0) g.drawString("It is a draw.",10, 6*size+10); else if(b.status==-1) {if(b.getplayer()==0) g.drawString("Green has won.",10, 6*size+14); else g.drawString("Red has won.",10, 6*size+14); } else if(b.status==b.Q) {if(b.getplayer()==0) g.drawString("Red to move.",10, 6*size+14); else g.drawString("Green to move.",10, 6*size+14); } } void drawstone(Graphics g,int sx,int sy,int snum) { if(stone[snum]==null||g.drawImage(stone[snum],sx,sy,null)!=true) {/*picture display failed. Do it another way.*/ g.setColor(Color.white); g.fillRect(sx,sy,size,size); if(snum!=b.EMPTY) { g.setColor(color[snum]); g.fillRect(sx+3,sy+3,size-6,size-6); } } } } class mover implements Runnable /*mover uses a thread, because that makes it possible to return the method do a move before doing the move. this is more convenient for many browsers: the mouseclicks that ask for moves are returned fast, so the user can click other buttons while the computer is calculating. the result is given to the applet by applet.catchamove(); */ { board original; //the board to do moves on board b; // a copy to do calculations on static int maxlevel; //the maximum searchdepth static int strength=0; //the current playing strength final static int EASY=0,MEDIUM=1,HARD=2;//possible strengths String strengthname[]={"easy","normal","hard"}; lightside applet; mover(board bb,lightside l) //make a mover {original=bb; applet=l; } void think_a_move() //this wakes up the mover. It will do a move {new Thread(this).start();} public void run() /*does a move on original. It tries all moves it can do, and then does the move that gave the highest prognosis.*/ { int best=-1; int alpha=-1001;//below the minimum score b=original.boardclone(); //make a copy for calculations int s; /*I use fastdomove and fastundomove, because no boardview has to be repainted in these calculations.*/ for(int c=0;c<7;c++) {if(b.canmove(c)) { b.fastdomove(c); s=-prognosis(-1000,-alpha,1); b.fastundomove(); if(s>alpha) {alpha=s; best=c; } } } applet.catchamove(best); } int prognosis(int alpha, int beta, int level) /*Try to give a prognosis about how good the situation is for the player to move on the board b. It does that by looking at the status(if the game is result), by doing any move and calling prognosis, by calling estimate (if it has called itself often enough)*/ { if(b.status!=b.Q) {if(b.status==-1) return -1000+b.moves; else if(b.status==1) return 1000-b.moves; return 0; } if(level>=maxlevel) if(strength==MEDIUM) return (int)(Math.random()*101)-50; else return b.groupestimate(); int s; /*try all moves and return the estimate we get when doing the best move*/ for(int c=0;c<7;c++) {if(b.canmove(c)) { b.fastdomove(c); s=-prognosis(-beta,-alpha,level+1); b.fastundomove(); if(s>beta) return s; /*clever cutoff. This does not affect the result. see alphabeta algoritm to see how this works.*/ if(s>alpha) alpha=s; } } return alpha; } void changestrength() /*change the playing strength of this computerplayer. returns the name of the new strength */ {strength=(strength+1)%(HARD+1); if(strength==EASY) maxlevel=2; else maxlevel=4; } }