Vier gewinnt!


Um mal wieder ein wenig Scala zu schreiben, habe ich „Vier gewinnt!“ für die Textkonsole umgesetzt. Künstliche Intelligenz gibt es noch nicht, was ich hoffentlich in einem Folgebeitrag nachholen kann.

Die Spielsteine sind wenig überraschend:

//Scala 2.8

sealed abstract class Coin(override val toString: String)
case object Naught extends Coin("O")
case object Cross extends Coin("X")

Praktisch ist, dass man Methoden auch mit Werten überschreiben kann, wie hier mit der toString-Methode geschehen.

Für mein Spielfeld habe ich keine wirklich gute Datenstruktur gefunden. Ein Array habe ich vermieden, da das Umkopieren bei einem Zug dann lästig wäre. Schließlich bin ich bei einer Map von x-Koordinaten auf Listen gelandet, auch wenn man ein wenig rumrechnen muss. Aber bei so einer winzigen Datenstruktur kommt es nun wirklich nicht auf das letzte Quentchen Performance an.

class Board private(data:Map[Int, List[Coin]]) {

  def move(x:Int, coin:Coin) = if (data(x).size == 6)
    throw new IllegalArgumentException("column " + x + " is full.")
  else new Board(data + (x -> (coin :: data(x))))

  def apply(x: Int, y: Int):Option[Coin] = data.get(x) match {
    case Some(list) if 6 - list.size <= y =>  Some(list(y - 6 + list.size))
    case _ => None
  }

  override def toString = {
    val sb = new StringBuilder
    for(y <- 0 to 5) {
      for(x <- 0 to 6) sb.append(apply(x, y).getOrElse(".")).append("|")
      sb.append("\n")
    }
    sb.append("0 1 2 3 4 5 6\n")
    sb.toString();
  }

  lazy val isFull = (0 to 6).forall(data(_).size == 6)

  lazy val winner:Option[Coin] = winner(Cross).orElse(winner(Naught))

  private def winner(c:Coin):Option[Coin] = {
    val rows = for(y <- 0 to 5) yield for(x <- 0 to 6) yield(apply(x,y))
    val cols = for(x <- 0 to 6) yield for(y <- 0 to 5) yield(apply(x,y))
    val dia1 = for(x <- -2 to 3) yield for(y <- 0 to 5) yield(apply(x+y,y))
    val dia2 = for(x <- 3 to 8 ) yield for(y <- 0 to 5) yield(apply(x-y,y))

    if(List(rows, cols, dia1, dia2).exists(_.exists(_.containsSlice(
            List(Some(c), Some(c), Some(c), Some(c)))))) Some(c)
    else None
  }
}

object Board {
  def apply() = new Board(Map((0 to 6).map(_ -> Nil):_*))
}

Es ist übrigens praktisch, wenn man auch Abfragen links und rechts über den Rand hinaus erlaubt – das macht die Abfrage der Diagonalen in winner(Coin) viel einfacher.

Fehlen noch die Spieler:

trait Player {
  var coin: Coin = _
  def name:String
  def move(board: Board):Board
}

case class Human(name: String) extends Player {
  private val scanner = new java.util.Scanner(System.in)

  def move(board: Board):Board = {
    var m = -1
    do {
      println("Your move?")
      try {
        m = Integer.parseInt(scanner.next)
      } catch {
        case _ =>
      }
    } while(m < 0 || m >= 7 || board(m,0) != None)
    return board.move(m, coin)
  }
}

Die nachträglich Übergabe der Spielerfarbe (also Coin) über ein var ist wenig elegant. Ich werde nochmal drüber meditieren. Auch das Einlesen des Zugs in Human geht bestimmt noch einfacher.

Das eigentliche „Spiel“ (oder besser gesagt, die Spielrunde), ist trivial:

object ConnectFour {

  def apply(player1:Player, player2:Player) {

    def opposite(player: Player) = if(player == player1) player2 else player1

    def play(board: Board, player: Player) {
      println(board)
      println
      if(board.winner != None) println("The winner is " + board.winner.get)
      else if (board.isFull) println("Board is full, draw.")
      else {
        println(player.name + "'s move (" + player.coin + ")")
        play(player.move(board), opposite(player))
      }
    }

    player1.coin = Cross
    player2.coin = Naught
    play(Board(), player1)
  }
}

Zum Starten brauch man einfach nur noch das ConnectFour-Objekt aufzurufen:

ConnectFour(Human("adam"), Human("eva"))

Und schon haben wir unser kleines Spielchen für die Textkonsole fertig. Der Code ist etwas Quick’n’Dirty, aber das liegt auch daran, dass man in Scala viel mehr Möglichkeiten hat, etwas doch noch zum Laufen zu bringen, wenn es nicht ganz so funktioniert, wie man sich es vorgestellt hatte. Oder anders ausgedrückt: Wo man in Java am Ende einer Sackgasse steht, steht man in Scala an einer Kreuzung – oft sogar mit ausgeschilderter Umleitung. Trotzdem lohnt es sich immer, nach einiger Zeit nochmal über sein Machwerk zu schauen, denn oft springen einem dann noch bessere Lösungswege ins Auge.

Vielleicht schreibe ich ja bei Gelegenheit eine Swing-Oberfläche oder einen Computergegner…

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s