Logo der Hochschule  
 aufwärts

Objektorientierte Programmierung

Entwicklung des Spiels "Vier gewinnt"

Im Folgenden sind die bisher gemeinsam entwickelten Klassen für unsere Implementierung des Spiels Vier gewinnt dokumentiert (Stand der Entwicklung: 21.06.2017). Das gesamte Programm als Java-Archiv findet sich unter http://www.inf.fh-flensburg.de/lang/veranst/oop/viergewinnt.jar.

 

 

Klasse Board

 

Board.java
// repräsentiert das Spielbrett
public class Board
{
    public static final int ROWS=6, COLS=7;
    public int[][] b;  // Belegung des Spielbretts
    public int[] h;    // Höhen der Säulen von Spielsteinen in den Spalten
    public int moves;  // Anzahl der gezogenen Spielzüge
    public int haswon; // 0 oder 1, wenn Spieler 0 oder 1 gewonnen hat, sonst -1

    public Board()
    {
        b=new int[ROWS][COLS];
        h=new int[COLS];
    }

    // Factory-Methode: liefert ein leeres Spielbrett
    public static Board emptyBoard()
    {
        Board board=new Board();
        board.clear();
        return board;
    }

    // leert das Spielbrett
    public void clear()
    {
        for (int i=0; i<ROWS; i++)
            for (int j=0; j<COLS; j++)
                b[i][j]=-1;
        for (int j=0; j<COLS; j++)
            h[j]=0;
        moves=0;
        haswon=-1;
    }

    // liefert eine Kopie des Spielbretts
    public Board copy()
    {
        Board board=new Board();
        for (int i=0; i<ROWS; i++)
            for (int j=0; j<COLS; j++)
                board.b[i][j]=b[i][j];
        for (int j=0; j<COLS; j++)
            board.h[j]=h[j];
        board.moves=moves;
        board.haswon=haswon;
        return board;
    }

    // true, wenn die Spalte des Spielbretts noch nicht voll ist
    public boolean isValidMove(Move move)
    {
        if (move==null)
            return false;
        int j=move.s;
        int i=h[j];
        return i<ROWS;
    }

    // true, wenn Position p auf dem Spielbrett vorhanden ist
    private boolean isValidPosition(Position p)
    {
        return p.z>=0 && p.z<ROWS && p.s>=0 && p.s<COLS;
    }

    // liefert die Farbe (0, 1 oder -1) des Spielsteins an Position p
    private int getColor(Position p)
    {
        return b[p.z][p.s];
    }

    // setzt die Farbe c=0, 1 oder -1 des Spielsteins an Position p
    private void setColor(Position p, int c)
    {
        b[p.z][p.s]=c;
    }

    // führt einen gültigen Spielzug move auf dem Spielbrett aus
    public void applyMove(Move move)
    {
        int j=move.s; // Spalte
        int i=h[j];
        Position p=new Position(ROWS-i-1, j);
        setColor(p, move.c);
        h[j]++;
        moves++;
        if (fourInAnyDirection(p))
            haswon=move.c;
    }

    // true, wenn kein Zug mehr möglich oder wenn ein Spieler gewonnen hat
    public boolean gameOver()
    {
        return moves>=ROWS*COLS || haswon>=0;
    }

    // liefert true, wenn mindestens vier gleichfarbige
    // Spielsteine von der Position p des Spielbretts aus
    // gesehen in Richtung dir und in entgegengesetzter
    // Richtung dir.neg() zusammengenommen liegen
    public boolean fourInDirection(Position p, Position dir)
    {
        int f=getColor(p);
        int count=-1;
        for (int i=0; i<2; i++)
        {
            Position q=p;
            while (isValidPosition(q)&&getColor(q)==f)
            {
                count++;
                q=q.add(dir);
            }
            dir=dir.neg();
        }
        return count>=4;
    }

    // liefert true, wenn von der Position p des Spielbretts
    // aus gesehen mindestens vier gleichfarbige Spielsteine
    // in irgendeiner Richtung liegen
    public boolean fourInAnyDirection(Position p)
    {
        return fourInDirection(p, new Position(1, 0))
                ||fourInDirection(p, new Position(0, 1))
                ||fourInDirection(p, new Position(1, 1))
                ||fourInDirection(p, new Position(1, -1));
    }

    // Bewertung der Spielstellung aus der Sicht des Spielers turn:
    // positiver Zahlenwert, wenn der Spieler turn gewonnen hat,
    // negativer Zahlenwert, wenn der andere gewonnen hat. Der Betrag
    // des Zahlenwerts ist umso größer, je weniger Spielzüge zum
    // Gewinn führen
    public int rate(int turn)
    {
        int m=ROWS*COLS+1-moves;  // maximale Anzahl von Zügen plus 1 minus moves
        if (haswon==turn)
            return m;
        if (haswon==1-turn)
            return -m;
        return 0;
    }

    @Override
    public String toString()
    {
        String s="";
        for (int i=0; i<ROWS; i++)
        {
            for (int j=0; j<COLS; j++)
                s+=b[i][j]+" ";
            s+="\n";
        }
        return s;
    }
}

Klasse Circle

 

Circle.java
import java.awt.Color;
import java.awt.Graphics;

// repräsentiert einen Kreis auf dem Bildschirm
public class Circle
{
 // Attribute
    public int left, top, diameter;
    public Color fill=Color.WHITE;   // Initialisierung des Attributs fill

    // Konstruktor
    public Circle(int l, int t, int d)
    {
        left=l;
        top=t;
        diameter=d;
    }

    // Farbe festlegen
    public void setFill(Color f)
    {
        fill=f;
    }

    // true, wenn die Klick-Position (x, y) innerhalb des Kreises liegt
    // (genauer: innerhalb des Quadrats, das den Kreis unmschließt)
    public boolean clicked(int x, int y)
    {
        return x>=left && x<=left+diameter && y>=top && y<=top+diameter;
    }

    // Kreis zeichnen
    public void draw(Graphics gr)
    {
        gr.setColor(fill);
        gr.fillOval(left, top, diameter, diameter);
    }

}

Klasse CircleGrid

 

CircleGrid.java
import java.awt.Color;
import java.awt.Graphics;

// repräsentiert eine Matrix von Kreisen
public class CircleGrid
{
    public int m, n;    // Anzahl Zeilen, Anzahl Spalten
    public int left=40, top=20, diameter=40, space=10;
    public Circle[][] grid;

    // Konstruktor
    public CircleGrid(int m, int n)
    {
        this.m=m;
        this.n=n;
        grid=new Circle[m][n];
        for (int i=0; i<m; i++)
            for (int j=0; j<n; j++)
                grid[i][j]=new Circle(j*(space+diameter)+left, i*(space+diameter)+top,
                     diameter);
    }

    // auf dem Bildschirm darstellen
    public void draw(Graphics gr)
    {
        for (int i=0; i<m; i++)
            for (int j=0; j<n; j++)
                grid[i][j].draw(gr);
    }

    // gibt die Position (Zeile, Spalte) des angeklickten Kreises
    // zurück, bzw. null, wenn mit dem Klick kein Kreis getroffen wurde
    public Position clicked(int x, int y)
    {
        for (int i=0; i<m; i++)
            for (int j=0; j<n; j++)
                if (grid[i][j].clicked(x, y))
                        return new Position(i, j);
        return null;
    }

    // setzt die Farbe des Kreises an Position (i, j) auf f
    public void fill(int i, int j, Color f)
    {
        grid[i][j].setFill(f);
    }
    public void fill(Position p, Color f)
    {
        fill(p.z, p.s, f);
    }

    // überträgt die Belegung eines Spielbretts in das CircleGrid
    public void display(Board board)
    {
        for (int i=0; i<m; i++)
            for (int j=0; j<n; j++)
                fill(i, j, color(board.b[i][j]));
    }

    // übersetzt die Werte 0, 1, und -1 in Farben
    private static Color color(int i)
    {
        if (i==0)
            return Color.RED;
        if (i==1)
            return Color.YELLOW;
        return Color.WHITE;
    }
}

Klasse Configuration

 

Configuration.java
import java.util.Observable;

// repräsentiert eine Spielstellung, bestehend aus der
// Belegung des Spielbretts und der Information, welcher
// Spieler (0 oder 1) am Zug ist
public class Configuration extends Observable
{
    public Board board;
    public int turn;

    // setzt die Spielstellung auf den Anfangszustand zurück
    public void reset()
    {
        this.board=Board.emptyBoard();
        this.turn=0;
        this.setChanged();
        this.notifyObservers();
    }

    // liefert eine Kopie der Spielstellung
    public Configuration copy()
    {
        Configuration config=new Configuration();
        config.board=this.board.copy();
        config.turn=this.turn;
        return config;
    }

    // führt einen Spielzug auf dem Spielbrett aus;
    // anschließend ist der andere Spieler am Zug
    public void setMove(Move move)
    {
        board.applyMove(move);
        turn=1-turn;
    }

    // prüft, ob Spielzug möglich ist und führt ihn aus; benachrichtigt die Observer
    public void applyMove(Move move)
    {
        if (move.c==turn && board.isValidMove(move) && !gameOver())
        {
            setMove(move);
            this.setChanged();
            this.notifyObservers();
        }
    }

    // true, wenn das Spiel zu Ende ist
    public boolean gameOver()
    {
        return board.gameOver();
    }

    // Bewertung der Spielstellung aus der Sicht des Spielers, der
    // am Zug ist (Negamax-Variante)
    public int rate()
    {
        return board.rate(turn);
    }
}

Klasse GameFrame

 

GameFrame.java
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;

public class GameFrame extends JFrame
{
 // Attribute
    public PaintPanel pnl1;
    private JPanel pnl2;
    private JButton btn1;

    // Konstruktor
    public GameFrame()
    {
        setTitle("Vier gewinnt");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(500, 450);

        pnl1=new PaintPanel();
        pnl1.setBackground(Color.GREEN);
        add(pnl1, BorderLayout.CENTER);

        pnl2=new JPanel();
        pnl2.setBackground(new Color(220, 220, 220));
        add(pnl2, BorderLayout.SOUTH);

        btn1=new JButton("Neues Spiel");
        pnl2.add(btn1);
        // anonymes Objekt einer anonymen Klasse, 
        // die das Interface ActionListener implementiert
        btn1.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent evt)
            {
                System.out.println("Neues Spiel");
                pnl1.config.reset();
            }
        });

        setVisible(true);   // als letzte Anweisung
    }
}

Klasse Main

 

Main.java
// Stand der Entwicklung 21.06.2017
public class Main
{
    public static void main(String[] args)
    {
        Configuration config=new Configuration();
        GameFrame f1=new GameFrame();
        //GameFrame f2=new GameFrame();
        Player f2=new SuperPlayer();
        Player f3=new SuperPlayer();
        f1.pnl1.setConfiguration(config);
        //f1.pnl1.setColor(0);
        f2.setConfiguration(config);
        f2.setColor(0);
        f3.setColor(1);
        f3.setConfiguration(config);
        config.reset();
    }
}

Klasse MiniMaximizer

 

MiniMaximizer.java
// siehe www.iti.fh-flensburg.de/lang/basic/java/minimaximizer.htm
public class MiniMaximizer<Type>
{
    private double minval, maxval;
    private Type minobj, maxobj;
    private boolean empty;

    public MiniMaximizer()
    {
        reset();
    }

    public void reset()
    {
        minval=Double.POSITIVE_INFINITY;
        maxval=Double.NEGATIVE_INFINITY;
        minobj=null;
        maxobj=null;
        empty=true;
    }

    public void add(double val, Type obj)
    {
        if (empty || val<minval)
        {
            minval=val;
            minobj=obj;
        }
        if (empty || val>maxval)
        {
            maxval=val;
            maxobj=obj;
        }
        empty=false;
    }

    // wenn es nur auf die Werte ankommt
    public void add(double val)
    {
        add(val, null);
    }

    public double getMinVal()
    {
        return minval;
    }

    public Type getMinObj()
    {
        return minobj;
    }

    public double getMaxVal()
    {
        return maxval;
    }

    public Type getMaxObj()
    {
        return maxobj;
    }

    public boolean isEmpty()
    {
        return empty;
    }
}

Klasse ModIntIterator

 

ModIntIterator.java
import java.util.Iterator;

// liefert die Zahlen 0...n-1, beginnend bei einer zufälligen Zahl,
// zyklisch, z.B. 3 4 0 1 2 für n=5
public class ModIntIterator implements Iterator<Integer>
{
    private int n, i, k;

    public ModIntIterator(int n)
    {
        this.n=n;
        k=(int)(Math.random()*n);  // Zufallszahl zwischen 0 und n-1
        i=0;
    }

    @Override
    public boolean hasNext()
    {
        return i<n;
    }

    @Override
    public Integer next()
    {
        return (k+i++)%n;
    }

    @Override
    public void remove()
    {
        // not implemented
    }
}

Klasse Move

 

Move.java
// repräsentiert eine Spielzug von Vier gewinnt
public class Move
{
    public int s;   // Spalte
    public int c;   // Farbe

    public Move(int s, int c)
    {
        this.s=s;
        this.c=c;
    }

    @Override
    public String toString()
    {
        return s+" "+c;
    }

}

Klasse PaintPanel

 

PaintPanel.java
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Observable;
import javax.swing.JPanel;

// stellt das Spielbrett auf dem Bildschirm dar und fungiert als Spieler
public class PaintPanel extends JPanel implements MouseListener, Player
{
    private CircleGrid circlegrid;
    public Configuration config;
    private int mycolor=-1;

    public PaintPanel()
    {
        circlegrid=new CircleGrid(Board.ROWS, Board.COLS);
        addMouseListener(this);
    }

    @Override
    public void paint(Graphics gr)
    {
        super.paint(gr);
        circlegrid.draw(gr);
    }

    @Override
    public void mouseClicked(MouseEvent arg0)
    {
    }

    @Override
    public void mouseEntered(MouseEvent arg0)
    {
    }

    @Override
    public void mouseExited(MouseEvent arg0)
    {
    }

    @Override
    public void mousePressed(MouseEvent evt)
    {
        int x=evt.getX();
        int y=evt.getY();
        Move move;
        Position p=circlegrid.clicked(x, y);
        if (p!=null)
        {
            move=new Move(p.s, mycolor);
            makeMove(move);
        }
    }

    @Override
    public void mouseReleased(MouseEvent arg0)
    {
    }

    @Override // Player -> Observer
    public void update(Observable arg0, Object arg1)
    {
        circlegrid.display(config.board);
        if (config.gameOver())
            if (config.board.haswon>=0)
                System.out.println(strcolor(config.board.haswon)+" hat gewonnen");
            else
                System.out.println("Unentschieden");
        repaint();
    }

    @Override // Player
    public void setColor(int k)
    {
        mycolor=k;
    }

    @Override // Player
    public void makeMove(Move move)
    {
        config.applyMove(move);
    }

    @Override  // Player
    public void setConfiguration(Configuration config)
    {
        this.config=config;
        config.addObserver(this);
    }
    
    // übersetzt die Werte 0, 1 in "Rot" und "Gelb"
    private static String strcolor(int i)
    {
        if (i==0)
            return "Rot";
        if (i==1)
            return "Gelb";
        return "";
    }
}

Interface Player

 

Player.java
import java.util.Observer;

public interface Player extends Observer
{
    public void setColor(int k);
    public void setConfiguration(Configuration config);
    public void makeMove(Move move);
}

Klasse Position

 

Position.java
// repräsentiert eine Position auf dem Spielbrett
// bestehend aus Zeilenindex z und Spaltenindex s
public class Position
{
    public int z, s;

    public Position(int z, int s)
    {
        this.z=z;
        this.s=s;
    }

    // liefert die Summe der Positionen this und other
    public Position add(Position other)
    {
        return new Position(this.z+other.z, this.s+other.s);
    }

    // liefert die zu this negierte Position    
    public Position neg()
    {
        return new Position(-z, -s);
    }
}

Klasse SimplePlayer

 

SimplePlayer.java
import java.util.Observable;

// stellt einen Computerspieler dar;
// zieht einen zufälligen Spielzug, wenn er als
// Observer benachrichtigt wird und wenn er dran ist
public class SimplePlayer implements Player
{
    protected int mycolor;           // Farbe des Spielers
    protected Configuration config;  // aktuelle Spielstellung

    // simple Spielstrategie: irgendwohin setzen
    public Move findMove()
    {
        int j;
        Move move=null;
        while (!config.board.isValidMove(move))
        {
            j=(int)(Math.random()*Board.COLS);  // zufällige Spalte
            move=new Move(j, mycolor);
        }
        return move;
    }

    // stellt die Farbe des Spielers ein
    @Override // Player
    public void setColor(int c)
    {
        mycolor=c;
    }

    @Override // Player
    public void setConfiguration(Configuration config)
    {
        this.config=config;
        config.addObserver(this);   // als Observer anmelden
    }

    @Override // Player
    public void makeMove(Move move)
    {
        config.applyMove(move);
    }

    @Override // Player, Observer
    public void update(Observable arg0, Object arg1)
    {
        if (config.turn==mycolor && !config.gameOver())
        {
            // anonymes Objekt einer anonymen Klasse, die die Klasse Thread erweitert
            new Thread()
            {
                @Override
                public void run()
                {
                    makeMove(findMove());
                }
            }.start();
        }
    }
}

Klasse SuperPlayer

 

SuperPlayer.java
import java.util.Iterator;

// Spielbaum-Auswertung
// Negamax-Variante mit partieller Auswertung
// siehe www.inf.fh-flensburg.de/lang/algorithmen/graph/spielbaum.htm
public class SuperPlayer extends SimplePlayer
{
    private int searchdepth=10;

    @Override
    public Move findMove()
    {
        Configuration s=config;
        Iterator<Integer> it=new ModIntIterator(Board.COLS);
        MiniMaximizer<Move> minimax=new MiniMaximizer<Move>();
        Move move;
        Configuration t;
        while (it.hasNext())
        {
            move=new Move(it.next(), s.turn);
            if (s.board.isValidMove(move))
            {
                t=s.copy();
                t.setMove(move);
                minimax.add(-eval(t, searchdepth-1, Double.POSITIVE_INFINITY), move);
           }
        }
        return minimax.getMaxObj();
    }

    private double eval(Configuration s, int d, double w)
    {
        if (d==0||s.gameOver())
            return s.rate();
        Iterator<Integer> it=new ModIntIterator(Board.COLS);
        MiniMaximizer<Move> minimax=new MiniMaximizer<Move>();
        Move move;
        Configuration t;
        while (it.hasNext() && minimax.getMaxVal()<w)
        {
            move=new Move(it.next(), s.turn);
            if (s.board.isValidMove(move))
            {
                t=s.copy();
                t.setMove(move);
                minimax.add(-eval(t, d-1, -minimax.getMaxVal()));
            }
        }
        return minimax.getMaxVal();
    }
}

 

up

 

homeH.W. Lang   Hochschule Flensburg   lang@hs-flensburg.de   Impressum   ©  
Valid HTML 4.01 Transitional