Tic-Tac-Toe in Java
Dieses ist ein unfertiges Programm, welches gerade die wesentlichen Funktionen zeigt, aber ohne eine schöne Benutzeroberfläche zu haben.
Das Erstellen des Programms hat zirka 5 Stunden gedauert, davon zirka 2 Stunden für die Suche eines Fehlers in der KI.
Gestaltungsprinzipien waren
- weitgehende Trennung von GUI-Code und Model
- keine willkürlichen „Pixel-Konstanten“ im Quelltext, sondern eine Oberfläche, die sich beim Start der Bildschirmgröße (vom Hand- bis zum Tischrechner) anpaßt und danach zur Laufzeit vergrößert oder verkleinert werden kann
- möglichst eleganter (d.h. kompakter) Code für KI (winner(), playonce()), und GUI (Kreuze nicht selber gezeichnet, sondern den Buchstaben „X“ verwendet)
- Objektorientierter, strukturierter Aufbau durch Klassen für Spieler, Felder und das Spielbrett
Mögliche Übungsaufgaben sind:
- den Fehler beseitigen, der es erlaubt, auf ein gegnerisches Feld zu klicken und es dadurch zu übernehmen
- es in der GUI deutlich anzuzeigen, wenn ein Gewinner oder Patt feststeht, und die Eingabe dann zu blockieren
- die Benutzeroberfläche zu verschönern
- die Benutzeroberfläche mit CSS gestalten
- eine kleine Bedienungsanleitung anzuzeigen
- den Quelltext übersichtlicher zu machen
- die KI etwas zu schwächen, damit der menschliche Spieler eine Chance hat
- längeres „Nachdenken“ der KI vorzutäuschen
- die GUI durch eine Konsolenoberfläche zu ersetzen
- das Programm so umzustellen, daß die KI gegen sich selbst spielt
- Klangeffekte hinzuzufügen
- Ausgabe von Kommentaren der KI hinzuzufügen (z.B. „Das war ein guter Zug!“)
Main.java
/* begin model */
class Player
{ public static Player O = new Player( -1 );
public static Player X = new Player( 1 );
protected int state;
public Player( final int state ){ this.state = state; }
public Player( final char symbol ){ this.state = symbol == 'O' ? -1 : +1; }
protected Player(){ this.state = 0; }
public int value() { return state; }
public Player other(){ return state == -1 ? X : O; }
public static Player valueOf( final int state ){ return state == -1 ? O : X; }
public static Player valueOf( final char symbol ){ return symbol == 'O' ? O : X; }
public char symbol(){ return state == -1 ? 'O' : 'X'; }}
class Square extends Player
{ public static Square O = new Square( -1 );
public static Square X = new Square( 1 );
public static Square Z = new Square( 0 );
private Square( final int state ){ this.state = state; }
public char symbol(){ return state == 0 ? '#' : super.symbol(); }
public void print(){ java.lang.System.out.print( symbol() ); }
public static Player valueOf( final Player player )
{ return player.value() == -1 ? O : player.value() == 1 ? X : Z; }}
class Result extends Player
{ public static Result tie = new Result( 0 );
private Result( final int state ){ this.state = state; }}
class Winner
{ Player player;
int row;
int col;
public Winner( final Player player, final int row, final int col )
{ this.player = player; this.row = row; this.col = col; }}
class Evaluation
{ double value;
Winner winner;
boolean running;
Board board;
public Evaluation( final double value, final Winner winner, final boolean running, final Board board )
{ this.value = value; this.winner = winner; this.running = running; this.board = board; }}
class Board
{ Player[] state =
{ Square.Z, Square.Z, Square.Z,
Square.Z, Square.Z, Square.Z,
Square.Z, Square.Z, Square.Z };
static private boolean valid( final int value )
{ return value >= 0 && value <3; }
static private boolean valid( final int row, final int col )
{ return valid( row )&& valid( col ); }
public Square get( final int row, final int col )
{ return valid( row, col )?( Square )state[ row * 3 + col ]:Square.Z; }
public void put( final int row, final int col, final Player player )
{ assert row >= 0: "row > 0";
assert row < 3: "row <= 3";
assert col >= 0: "col > 0";
assert col < 3: "col <= 3";
assert state[ row * 3 + col ]== Square.Z: "state[ row * 3 + col ]== Square.Z";
state[ row * 3 + col ]= Square.valueOf( player ); }
public int sum( final int row, final int col, final int dr, final int dc )
{ int sum =
get( row + 0 * dr, col + 0 * dc ).value() +
get( row + 1 * dr, col + 1 * dc ).value() +
get( row + 2 * dr, col + 2 * dc ).value();
return sum; }
public Winner winner()
{ for( int r = 0; r < 3; ++r )for( int c = 0; c < 3; ++c )
for( int dr = -1; dr < 2; ++dr )for( int dc = -1; dc < 2; ++dc )
if( Math.abs( dr )+Math.abs( dc ) != 0 && Math.abs( sum( r, c, dr, dc ))> 2 )
{ return new Winner( get( r, c ), r, c ); }
return null; }
public boolean full()
{ for( int r = 0; r < 3; ++r )for( int c = 0; c < 3; ++c )
if( get( r, c )== Square.Z )return false;
return true; }
public void print()
{ for( int r = 0; r < 3; ++r )
{ for( int c = 0; c < 3; ++c )get( r, c ).print();
java.lang.System.out.println(); }}
public Board(){}
public Board( final Board other )
{ java.lang.System.arraycopy( other.state, 0, this.state, 0, 9 ); }}
/* end model */
public final class Main extends javafx.application.Application
{
/* begin model */
public static Evaluation findworst( final java.util.List<Evaluation> list )
{ assert list.size() > 0: "list.size() > 0";
double min = java.lang.Double.POSITIVE_INFINITY;
final java.util.List<Evaluation> worst = new java.util.ArrayList<Evaluation>();
for( Evaluation e : list )if( e.value < min )min = e.value;
for( Evaluation e : list )if( e.value == min )worst.add( e );
assert worst.size() > 0: "worst.size() > 0";
Evaluation b = worst.get( ( int )( java.lang.Math.random() * worst.size() ));
return b; }
public static Evaluation playonce( Player p, Board b )
{ final Evaluation result;
final Winner winner = b.winner();
if( winner != null )
{ final Player player = winner.player;
if( player.value() == p.value() )
result = new Evaluation( 1, winner, false, b );
else
result = new Evaluation( -1, winner, false, b ); }
else if( b.full() )
{ result = new Evaluation( 0, null, false, b ); }
else
{ final java.util.List<Evaluation> list = new java.util.ArrayList<Evaluation>();
final Player o = p.other();
for( int r = 0; r < 3; ++r )for( int c = 0; c < 3; ++c )
if( b.get( r, c )== Square.Z )
{ final Board bb = new Board( b );
bb.put( r, c, p );
final Evaluation e = playonce( o, bb ); /* change POV */
e.running = true; e.board = bb; list.add( e ); }
final Evaluation worst = findworst( list );
final Evaluation best = worst; best.value = -worst.value; /* change POV */
result = best; }
return result; }
/* end model */
/* begin UI */
final javafx.scene.layout.GridPane gridPane = new javafx.scene.layout.GridPane();
private javafx.beans.property.DoubleProperty fontSize = new javafx.beans.property.SimpleDoubleProperty(10);
javafx.scene.Scene scene ;
void button( int row, int col )
{ final javafx.scene.control.Button button = new javafx.scene.control.Button( " " );
button.setMinSize( 1, 1 ); /* Unclamp the max size of for building resizable gui */
button.setMaxSize( java.lang.Double.MAX_VALUE, java.lang.Double.MAX_VALUE ); /* Unclamp the max size of for building resizable gui */
//button.setPrefSize(java.lang.Double.MAX_VALUE, java.lang.Double.MAX_VALUE);
fontSize.bind( scene.widthProperty().add( scene.heightProperty() ).divide( 20 ) );
java.lang.System.out.println( "fontSize.asString() = " + fontSize.asString() );
button.styleProperty().bind( javafx.beans.binding.Bindings.concat
( "-fx-font-size: ", fontSize.asString(), ";", "-fx-font-family: ", "'Consolas', monospace", ";" ));
button.setOnAction( this::handler );
gridPane.add( button, col, row );
}
@java.lang.Override
public void start( final javafx.stage.Stage stage )
{ try
{ scene = new javafx.scene.Scene( gridPane );
for( int row = 0; row < 3; ++row )for( int col = 0; col < 3; ++col )button( row, col );
button( 0, 3 );
gridPane.setMinSize( 1, 1 ); /* Unclamp the max size for resizable gui */
gridPane.setMaxSize( java.lang.Double.MAX_VALUE, java.lang.Double.MAX_VALUE ); /* Unclamp the max size for resizable gui */
stage.setScene( scene );
final javafx.geometry.Rectangle2D bounds = javafx.stage.Screen.getPrimary().getVisualBounds();
stage.setWidth(bounds.getWidth()/2);
stage.setHeight(bounds.getHeight()/2);
stage.setX(bounds.getMinX()+bounds.getWidth()/4);
stage.setY(bounds.getMinY()+bounds.getHeight()/4);
stage.show();
stage.show(); }
catch(Exception e)
{ e.printStackTrace(); }}
/* end UI */
/* begin mixed */
final Player X = Player.valueOf( 'X' );
final Player O = Player.valueOf( 'O' );
Player p = O;
public void printdiag( final Player p, final Board b, final Evaluation evaluation )
{ if( !evaluation.running )
{ java.lang.System.out.println
( evaluation.value == 1 ? "Player " + p.symbol() + " won. " :
evaluation.value == -1 ? "Player " + p.other().symbol() + " won. " :
evaluation.value == 0 ? "Tie. " : "" );
java.lang.System.out.println();
b.print(); }}
public void handler( final javafx.event.ActionEvent evt )
{
javafx.scene.control.Button clickedButton =( javafx.scene.control.Button )evt.getTarget();
clickedButton.setText( "X" );
javafx.collections.ObservableList<javafx.scene.Node> list = gridPane.getChildren();
/* copy from the gridPane to the board */
Board b = new Board(); int row = 0; int col = 0;
for( final javafx.scene.Node node : list )
{ javafx.scene.control.Button button = ( javafx.scene.control.Button )node;
b.put( row, col++, button.getText().equals( "X" )? X : button.getText().equals( "O" )? O : Square.Z );
if( col == 3 && row == 2 )break;
if( col == 3 ){ row++; col = 0; }}
java.lang.System.out.println();
b.print();
java.lang.System.out.println();
/* calculate next board */
p = X;
Evaluation evaluation = null;
evaluation = playonce( X, b ); /* just detect win/loss/tie */ printdiag( X, b, evaluation );
//printdiag( b, evaluation );
evaluation = playonce( O, b );
b = evaluation.board;
b.print();
evaluation = playonce( O, b ); /* just detect win/loss/tie */ printdiag( O, b, evaluation );
/* copy from the board to the gridPane */
row = 0; col = 0;
for( final javafx.scene.Node node : list )
{ javafx.scene.control.Button button = ( javafx.scene.control.Button )node;
final char ch = b.get( row, col++ ).symbol();
button.setText( "" +( ch == '#' ? ' ' : ch ));
if( col == 3 ){ row++; col = 0; }}}}
- Usenet-Posting 20160409204458+0100
From: ram@zedat.fu-berlin.de (Stefan Ram)
Die Rekursion findet in meiner KI bei »playonce« statt, das
versuchsweise einen Zug macht, und dann »playonce« aufruft,
um zu evaluieren, wie gut dieser ist. Die Rekursion endet
bei mir, wenn ein Gewinner feststeht (»winner != null«) oder
kein Platz auf dem Spielbrett mehr frei ist (»b.full()«).
Dabei wird bei jeder neuen Rekursionstiefe von »playonce«
die Perspektive gewechselt, um auch die Züge des Gegners zu
spielen. Daher ist es in dem untenstehenden Pseudocode
nötig das /Negative/ der Bewertung des nächsten Zuges
zu verwenden, weil diese aus der Sicht des /Gegners/ erfolgt.
Dies könnte man als einen »brute-force« Algorithmus
klassifizieren, der alle Spielmöglichkeiten des Spielers
und des Gegners durchspielt, um dann alle Stellungen
bewerten zu können.
Mein Rekursionsschema sieht also so aus:
Bewerte einen Zug:
wenn kein Zug mehr moeglich, dann
Ergebnis = Bewertung der jetzigen Stellung
sonst
für alle Möglichkeiten für den nächsten Zug:
mache diesen nächsten Zug versuchsweise
Ermittle seine Bewertung (durch Rekursion)
Ergebnis = das Negative der Zusammenfassung der Bewertungen
Dabei ist es für die Beendigung der Rekursion wichtig,
daß es einen Spielzustand gibt, bei dem kein weiterer
Zug mehr möglich ist, dieser nach endlich vielen Zügen
erreicht wird, und jede neue Rekursionstiefe einen
Schritt in die Richtung zu jenem Endzustand geht.
Man muß sich auch überlegen, welche Variablen für mehrere
Rekursionstiefen geteilt werden können und welche bei
jeder Rekursion neu anzulegen sind. Im Zweifelsfalle
sollte man alle veränderbaren Zustände bei jeder Rekursion,
bei der sie verändert werden könnten, erst einmal
als Kopie der vorigen Zustände neu anlegen und dann am
Ende des Rekursionsschritts sicherstellen, daß der
vorige Zustand wiederhergestellt ist.
Nun, dieser Code von mir ist bei weitem nicht perfekt,
ich habe ihn ja damals in kurzer Zeit geschrieben und
ich habe noch eine große Aufgabenliste, was ich daran
noch alles verbessern will, wozu ich aber noch nicht
gekommen bin.
- Aussprachehinweise
- super ˈsupɚ (sd)