Um Exemplo Final

Objetivos da seção

O Jogo de Mancala

mancala1.gif (16977 bytes)

mancala2.gif (16779 bytes)

java -classpath "." p1.aplic.mancala.cui.JogoCUI ENIAC PDP11
...
java -classpath "." p1.aplic.mancala.cui.JogoCUI seunome PDP11

Os Requisitos da Solução

mancala3.gif (6898 bytes)

O Modelo Conceitual

modeloconceitual.gif (9312 bytes)

A Arquitetura da Solução

mvc.gif (6702 bytes)

O Projeto de Baixo Nível

O Model

mancala-jogo.gif (34852 bytes)

A View

mancala-observer.gif (21698 bytes)

mancala-cui.gif (5695 bytes)

O Controller

mancala-controller-cui.gif (7897 bytes)

Javadoc

A Implementação da Versão a Caractere

Controller CUI

package p1.aplic.mancala.cui;
import p1.aplic.mancala.jogo.*;
import java.util.*; 

/**
 * O controlador de jogo Mancala quando a interface é a caractere.
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public class JogoCUI {
  public static void main(String[] args) {
    Jogador j1 = null;
    Jogador j2 = null;
    switch(args.length) {
      case 0:
        j1 = criaJogador("ENIAC", Tabuleiro.EM_BAIXO);
        j2 = criaJogador("PDP11", Tabuleiro.EM_CIMA);
        break;
      case 1:
        j1 = criaJogador(args[0], Tabuleiro.EM_BAIXO);
        j2 = criaJogador("PDP11", Tabuleiro.EM_CIMA);
        break;
      case 2:
        j1 = criaJogador(args[0], Tabuleiro.EM_BAIXO);
        j2 = criaJogador(args[1], Tabuleiro.EM_CIMA);
        break;
      default:
        sintaxe();
    }
    // cria um Model
    JogoMancala jogo = new UmJogo(j1, j2);
    // cria uma View e cadastre-se junto ao Model
    jogo.addMancalaListener(new OutputMancalaCUI());
    try {
        joga(jogo);
    } catch(MancalaException e) {
        System.err.println(e.getMessage());
        System.exit(1);
    }
  }

  /**
   * Joga um jogo de Mancala com interface a caractere.
   *
   * @param   jogo   O objeto para controlar o jogo.
   * @throws  MancalaException em caso de erro de jogo.
   */
  private static void joga(JogoMancala jogo) throws MancalaException {
    jogo.iniciaJogo();
    Jogador jogadorAtual = jogo.getJogadorAtual();
    while(!jogo.fimDeJogo()) {
      jogadorAtual = jogo.umaJogada(jogadorAtual.escolheJogada(jogo), JogoMancala.COM_EVENTO);
    }
  }

  /**
   * Cria um jogador de acordo com nomes especiais conhecidos.
   *
   * @param   nome   O nome do jogador.
   *                 "ENIAC" é o nome de um computador com algoritmo burro.
   *                 "PDP11" é o nome de um computador com algoritmo melhorzinho.
   *                 Qualquer outro nome indica um jogador humano.
   * @return  O jogador correspondente.
   */
  private static Jogador criaJogador(String nome, int posição) {
    if(nome.equals("ENIAC")) {
      return new JogadorComputador1(nome, posição);
    } else if(nome.equals("PDP11")) {
      return new JogadorComputador2(nome, posição);
    } else {
      return new JogadorHumanoCUI(nome, posição);
    }
  }

  /**
   * Dá mensagem de erro de sintaxe e cai fora.
   *
   */
  private static void sintaxe() {
    System.err.println("Sintaxe: JogoCUI [primeiro_nome] [segundo_nome]");
    System.err.println("         O nome ENIAC é um computador burro");
    System.err.println("         O nome PDP11 é um computador melhorzinho");
    System.exit(1);
  }
}
    while(!jogo.fimDeJogo()) {
      jogadorAtual = jogo.umaJogada(jogadorAtual.escolheJogada(jogo), JogoMancala.COM_EVENTO);
    }

O jogador humano

package p1.aplic.mancala.jogo;
import java.util.*;

/**
 * A parte comum de qualquer jogador: manter nome e posição (EM_BAIXO ou EM_CIMA).
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public abstract class JogadorAbstrato implements Jogador {
  private String nome;
  private int posição;

  /**
   * Construtor genérico de um jogador.
   *
   * @param   nome   O nome do jogador.
   * @param   posição   A posição do jogador (EM_BAIXO ou EM_CIMA).
   */
  public JogadorAbstrato(String nome, int posição) {
    this.nome = nome;
    this.posição = posição;
  }

  /**
   * Obtém o nome do jogador.
   *
   * @return  O nome do jogador
   */
  public String getNome() {
    return nome;
  }

  /**
   * Informa a posição do jogador.
   *
   * @return  A posição do jogador (EM_BAIXO ou EM_CIMA)
   */
  public int getPosição() {
    return posição;
  }

  /**
   * Pede ao jogador para escolher um buraco para jogar.
   *
   * @param   jogo   O jogo do qual o jogador está participando.
   * @return  O número do buraco (entre 0 e 5)
   * @throws  MancalaException se o buraco escolhido não for possível.
   */
  public abstract int escolheJogada(JogoMancala jogo) throws MancalaException;
}
package p1.aplic.mancala.cui;
import p1.aplic.mancala.jogo.*;
import p1.io.*;

/**
 * Interface a caractere para jogar Mancala.
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public class JogadorHumanoCUI extends JogadorAbstrato {
  /**
   * Construtor de um jogador humano com interface a caractere.
   *
   * @param   nome   O nome do jogador.
   * @param   posição   A posição do jogador (EM_BAIXO ou EM_CIMA).
   */
  public JogadorHumanoCUI(String nome, int posição) {
    super(nome, posição);
  }

  /**
   * Pede ao jogador, na entrada padrão, para escolher um buraco para jogar.
   *
   * @param   jogo   O jogo do qual o jogador está participando.
   * @return  O número do buraco (entre 0 e 5)
   * @throws  MancalaException se o buraco escolhido não for possível.
   */
  public int escolheJogada(JogoMancala jogo) throws MancalaException {
    String resp;
    boolean ok = false;
    int numBuraco = 0;
    while(!ok) {
      resp = Entrada.in.lerLinha("Jogador " + getNome() + ": informe o numero do buraco: ");
      if(resp.startsWith("q")) {
        throw new MancalaException("Jogador " + getNome() + " abandonou o jogo. Tchau.");
      }
      try {
        numBuraco = jogo.getTabuleiro().numAIndex(Integer.parseInt(resp)-1, this.getPosição());
        ok = true;
      } catch(NumberFormatException e) {
        System.out.println("Tente algo numerico, ta?");
      } catch(MancalaException e) {
        System.out.println(e.getMessage());
      }
    }
    return numBuraco;
  }
}

O Jogo

package p1.aplic.mancala.jogo;
import java.util.*; 

/**
 *
 * Regras deste jogo de Mancala (também chamado Kalaha).
 * Mancala é jogado com sete buracos -- seis buracos de jogo
 * e um buraco de pontuação, a <I>Kalaha</I> -- por jogador.
 * No início do jogo, cada um dos 12 buracos de jogo contém
 * 3 sementes (ou contas, ou pedras, ou bolas, ...).
 * Para jogar, o jogador escolhe um buraco a partir do qual ele
 * vai "semear" as sementes. Cada semente do buraco é colocada,
 * uma de cada vez, em buracos sucessivos, movendo-se em sentido
 * anti-horário. Sementes colocadas numa Kalaha são pontos para
 * o dono da Kalaha. Sementes não são colocadas na Kalaha do oponente.
 * Se a última semente cair na Kalaha do jogador, ele pode jogar novamente
 * Se a última semente cair num buraco vazio do próprio jogador, 
  * ele captura as sementes do buraco oposto e as transfere para sua Kalaha
 * Todas as sementes capturadas, além da semente que fez a captura,
 * são colocadas na Kalaha do jogador.
 * O jogo termina quando todos os buracos em algum lado do tabuleiro estejam vazios.
 * O jogador com sementes em jogo os recolhe para sua Kalaha.
 * O ganhador é o jogador com mais sementes na sua Kalaha.
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public class UmJogo implements JogoMancala {
  private final static int SEMENTES_INICIAIS = 3;
  private Jogador[] jogadores;
  private Jogador jogadorAtual;
  private Tabuleiro tabuleiro;
  private Collection mancalaListeners = new LinkedList();

  /**
   * Construtor de um jogo de Mancala com dois jogadores.
   *
   * @param   jogador1   O primeiro jogador.
   * @param   jogador2   O segundo jogador.
   */
  public UmJogo(Jogador jogador1, Jogador jogador2) {
    jogadores = new Jogador[2];
    jogadores[0] = jogador1;
    jogadores[1] = jogador2;
    inicializaTabuleiro();
  }

  /**
   * Inicializa a configuração do tabuleiro.
   * Neste jogo, temos 3 sementes em cada buraco (que não seja Kalaha).
   *
   */
  private void inicializaTabuleiro() {
    tabuleiro = new Tabuleiro();
    for(int i = 0; i < Tabuleiro.NUM_BURACOS; i++) {
      Buraco buraco = tabuleiro.getBuraco(Tabuleiro.EM_BAIXO, i);
      buraco.adicionaSementes(SEMENTES_INICIAIS);
      buraco = tabuleiro.getBuraco(Tabuleiro.EM_CIMA, i);
      buraco.adicionaSementes(SEMENTES_INICIAIS);
    }
  }

  /**
   * Obtém o tabuleiro sendo usado para o jogo.
   *
   * @return  O tabuleiro sendo usado para o jogo.
   */
  public Tabuleiro getTabuleiro() {
    return tabuleiro;
  }

  /**
   * Altera o tabuleiro sendo usado para o jogo.
   *
   * @param   tabuleiro   O tabuleiro a usar.
   */
  public void setTabuleiro(Tabuleiro tabuleiro) {
    this.tabuleiro = tabuleiro;
  }

  /**
   * Obtem o jogador cuja vez é de jogar.
   *
   * @return  O jogador cuja vez é de jogar.
   */
  public Jogador getJogadorAtual() {
    return jogadorAtual;
  }

  /**
   * Obtém um jogador através do seu número. O jogador de baixo é 0, o de cima é 1.
   *
   * @param   númeroDoJogador   o número do jogador (0 = em baixo, 1 = em cima).
   * @return  O jogador indicado.
   */
  public Jogador getJogador(int númeroDoJogador) {
    return jogadores[númeroDoJogador];
  }

  /**
   * Obtém o jogador que ganhou a partida.
   *
   * @return  O jogador que ganhou a partida, ou null se tiver sido empate.
   */
  public Jogador getGanhador() {
    int diferença = getKalaha(jogadores[0]).getNúmeroDeSementes() -
                    getKalaha(jogadores[1]).getNúmeroDeSementes();
    if(diferença > 0) {
      return jogadores[0];
    } else if(diferença < 0) {
      return jogadores[1];
    } else {
      return null; //empate
    }
  }

  /**
   * Obtem a Kalaha do jogador indicado.
   *
   * @param   jogador   O jogador cuja Kalaha se deseja.
   * @return  Uma referência ao buraco representando a Kalaha do jogador.
   */
  public Buraco getKalaha(Jogador jogador) {
    return tabuleiro.getKalaha(jogador.getPosição());
  }

  /**
   * Avisa ao jogo que queremos iniciar.
   *
   */
  public void iniciaJogo() {
    jogadorAtual = jogadores[0];
    disparaMancalaEvent(MancalaEvent.INICIO_DE_JOGO, null, jogadorAtual, null);
  }

  /**
   * Faz uma jogada para o jogadorAtual do jogo no buraco indicado por indexBuraco.
   * 
   * @param   indexBuraco   O índice (de 0 a 5) do buraco escolhido.
   * @param   querEvento   igual a SEM_EVENTO, para uma jogada que não aparecer na interface com o usuário.
   *          Seria o caso para estratégias de jogadores automatizados que querem tentar várias jogadas.
   *          Uma jogada normal (com efeito total) usa COM_EVENTO.
   * @return  O próximo jogador a jogar.
   * @throws  MancalaException se um buraco ilegal for escolhido.
   */
  public Jogador umaJogada(int indexBuraco, int querEvento) throws MancalaException {
    Buraco buracoEscolhido = tabuleiro.getBuraco(jogadorAtual.getPosição(), indexBuraco);
    Buraco últimoBuraco = semeia(buracoEscolhido);
    Jogador quemJogou = jogadorAtual;
    Jogador próximoJogador = jogadorAtual;
    // vê quem é o próximo jogador
    if(!jogaNovamente(últimoBuraco)) {
       próximoJogador = jogadorAtual.equals(jogadores[0]) ?
                        jogadores[1] : jogadores[0];
    }
    if(querEvento == COM_EVENTO) {
      jogadorAtual = próximoJogador;
      // gera evento de mudança de tabuleiro
      disparaMancalaEvent(MancalaEvent.JOGADOR_JOGOU, buracoEscolhido, jogadorAtual, quemJogou);
    }
    return próximoJogador;
  }

  /**
   * Informa se o jogador corrente pode jogar novamente
   *
   * @param   últimoBuraco   O último buraco em que uma semente caiu.
   * @return  true, se o jogador pode jogar novamente.
   */
  private boolean jogaNovamente(Buraco últimoBuraco) {
    return últimoBuraco.equals(getKalaha(jogadorAtual));
  }

  /**
   * Informa quem é o jogador oposto de um certo jogador.
   *
   * @param   jogador   O jogador cujo oponente procuramos.
   * @return  O oponente do jogador
   */
  private Jogador jogadorOposto(Jogador jogador) {
    return jogador.equals(jogadores[0]) ?
           jogadores[1] : jogadores[0];
  }

  /**
   * Espalha as sementes de um buraco como resultado de uma jogada e
   * aplica a regra de roubar as sementes opostas quando o último
   * buraco está vazio e é do próprio jogador.
   *
   * @param   buracoEscolhido   O buraco escolhido para jogar.
   * @return  O último buraco onde uma semente foi depositada.
   */
  private Buraco semeia(Buraco buracoEscolhido) {
    int numSementes = buracoEscolhido.getNúmeroDeSementes();
    buracoEscolhido.removeSementes(numSementes);
    Buraco buracoCorrente = buracoEscolhido;
    while(numSementes > 0) {
      buracoCorrente = tabuleiro.próximoBuraco(buracoCorrente);
      if(buracoCorrente.equals(getKalaha(jogadorOposto(jogadorAtual)))) {
        continue; // não põe semente na Kalaha do oponente
      }
      numSementes--;
      buracoCorrente.adicionaSementes(1);
    }
    // se o último buraco é meu estava vazio, rouba sementes do buraco oposto
    if(tabuleiro.getPosição(buracoCorrente) == jogadorAtual.getPosição() &&
       buracoCorrente.getNúmeroDeSementes() == 1) {
      // rouba
      Buraco buracoOposto = tabuleiro.buracoOposto(buracoCorrente);
      numSementes = buracoOposto.getNúmeroDeSementes();
      buracoOposto.removeSementes(numSementes);
      getKalaha(jogadorAtual).adicionaSementes(numSementes);
    }
    return buracoCorrente;
  }

  /**
   * Verifica se o jogo acabou.
   *
   * @return  true, se o jogo acabou; false, caso contrário.
   */
  public boolean fimDeJogo() {
    if(tabuleiro.ladoEstáVazio(Tabuleiro.EM_BAIXO) ||
       tabuleiro.ladoEstáVazio(Tabuleiro.EM_CIMA)) {
      recolheSementes(jogadores[0]);
      recolheSementes(jogadores[1]);
      disparaMancalaEvent(MancalaEvent.FIM_DE_JOGO, null, null, null);
      return true;
    } else {
      return false;
    }
  }
  
  /**
   * No fim do jogo, recolhe as sementes de um jogador para a Kalaha dele.
   *
   * @param   jogador   O jogador cujas sementes devem ser recolhidas à Kalaha.
   */
  private void recolheSementes(Jogador jogador) {
    for(int i = 0; i < Tabuleiro.NUM_BURACOS; i++) {
      Buraco buraco = tabuleiro.getBuraco(jogador.getPosição(), i);
      int numSementes = buraco.getNúmeroDeSementes();
      buraco.removeSementes(numSementes);
      getKalaha(jogador).adicionaSementes(numSementes);
    }
  }

  /**
   * Adiciona um listener interessado em receber eventos do jogo.
   * Normalmente usado para conectar o jogo a uma interface com o usuário.
   *
   * @param   listener   O objeto que deseja receber os eventos do jogo.
   */
  public synchronized void addMancalaListener(MancalaListener l) {
    if(mancalaListeners.contains(l)) {
	    return;
    }
    mancalaListeners.add(l);
  }

  /**
   * Remove um listener não mais interessado em receber eventos do jogo.
   *
   * @param   listener   O listener a ser descadastrado.
   */
  public synchronized void removeMancalaListener(MancalaListener l) {
    mancalaListeners.remove(l);
  }

  /**
   * Envia um evento de jogo para todos os listeners cadastrados.
   * Os eventos podem indicar:
   * 1) o início do jogo (INICIO_DE_JOGO);
   * 2) Uma jogada de algum jogador (JOGADOR_JOGOU);
   * 3) o fim do jogo (FIM_DE_JOGO).
   *
   * @param   oQueOcorreu   Indica um dos tr~es eventos acima.
   * @param   buraco   o buraco que foi jogado.
   * @param   jogadorAtual   O jogador que passa a ser o jogador atual.
   * @param   jogadorQueJogou   o jogador que acabou de jogar.
   */
  void disparaMancalaEvent(int oQueOcorreu, Buraco buraco, Jogador jogadorAtual, Jogador jogadorQueJogou) {
    Collection ml;
    MancalaEvent evento = new MancalaEvent(this, buraco, jogadorAtual, jogadorQueJogou);
    synchronized (this) {
      // A interface Collection não tem clone()
      // mas a classe LinkedList tem.
      // Clonar para evitar problemas de sincronização
      // durante a propagação
      ml = (Collection)((LinkedList)mancalaListeners).clone();
    }
    Iterator it = ml.iterator();
    while(it.hasNext()) {
      MancalaListener umListener = (MancalaListener)(it.next());
      switch(oQueOcorreu) {
      case MancalaEvent.INICIO_DE_JOGO:
  	    umListener.inicioDeJogo(evento);
        break;
      case MancalaEvent.JOGADOR_JOGOU:
  	    umListener.jogadorJogou(evento);
        break;
      case MancalaEvent.FIM_DE_JOGO:
  	    umListener.fimDeJogo(evento);
        break;
      }
  	}
  }
}

O Tabuleiro e os Buracos

package p1.aplic.mancala.jogo;
import java.util.*;

/**
 *
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public class Tabuleiro {
  /**
   * Posição "em baixo" do tabuleiro, com buracos 1 a 6 e Kalaha à direita.
   */
  public final static int EM_BAIXO = 0;

  /**
   * Posição "em cima" do tabuleiro, com buracos 8 a 13 e Kalaha à esquerda.
   */
  public final static int EM_CIMA = 1;

  /**
   * Posição de um buraco indicando uma Kalaha.
   */
  public final static int KALAHA = 2;

  /**
   * Número de buracos por jogador (sem incluir Kalahas)
   */
  public final static int NUM_BURACOS = 6;

  private final static int TOTAL_BURACOS = 2*(NUM_BURACOS+1);
  private final static int PRIMEIRO_BURACO_EM_BAIXO = 0;
  private final static int ÚLTIMO_BURACO_EM_BAIXO = NUM_BURACOS-1;
  private final static int KALAHA_DIREITA = NUM_BURACOS;
  private final static int PRIMEIRO_BURACO_EM_CIMA = NUM_BURACOS+1;
  private final static int ÚLTIMO_BURACO_EM_CIMA = TOTAL_BURACOS-2;
  private final static int KALAHA_ESQUERDA = TOTAL_BURACOS-1;
  private Buraco[] buracos;

  /**
   * Construtor de um tabuleiro padrão vazio para jogar Mancala.
   */
  Tabuleiro() {
    buracos = new Buraco[TOTAL_BURACOS];
    for(int i = 0; i < TOTAL_BURACOS; i++) {
      buracos[i] = new Buraco(i, 0);
    }
  }

  /**
   * Informa a Kalaha correspondendo a uma posição (EM_BAIXO ou EM_CIMA)
   * A Kalaha do jogador "em baixo" está à direita.
   * A Kalaha do jogador "em cima" está à esquerda.
   *
   * @param   posição   A posição de interesse (EM_BAIXO ou EM_CIMA).
   * @return  O buraco correpondendo à Kalaha da posição de interesse.
   */
  Buraco getKalaha(int posição) {
    return posição == EM_BAIXO ?
           buracos[KALAHA_DIREITA] :
           buracos[KALAHA_ESQUERDA];
  }

  /**
   * Informa o buraco correspondendo a um número de buraco
   * de uma certa posição (EM_BAIXO ou EM_CIMA).
   *
   * @param   posição   A posição de interesse (EM_BAIXO ou EM_CIMA).
   * @param   númeroDoBuraco   O número do buraco (0 a 5) de interesse.
   * @return  O buraco de interesse.
   */
  public Buraco getBuraco(int posição, int númeroDoBuraco) {
    return posição == EM_CIMA ? buracos[PRIMEIRO_BURACO_EM_CIMA + númeroDoBuraco] :
                                buracos[PRIMEIRO_BURACO_EM_BAIXO + númeroDoBuraco];
  }

  /**
   * Verifica se um lado do tabuleiro está vazio.
   *
   * @param   posição   A posição de interesse (EM_BAIXO ou EM_CIMA).
   * @return  true se o lado do tabuleiro indicado pela posição está vazio,
   *          isto é, com os 6 buracos vazios.
   */
  boolean ladoEstáVazio(int posição) {
    for(int i = 0; i < Tabuleiro.NUM_BURACOS; i++) {
      Buraco buraco = getBuraco(posição, i);
      if(buraco.getNúmeroDeSementes() > 0) {
        return false;
      }
    }
    return true;
  }

  /**
   * Informa o próximo buraco depois de um certo buraco, incluindo Kalahas,
   * no sentido do jogo (anti-horário).
   *
   * @param   buraco   O buraco de referência.
   * @return  O buraco depois do buraco de referência.
   */
  public Buraco próximoBuraco(Buraco buraco) {
    return buracos[(buraco.getNúmero()+1)%TOTAL_BURACOS];
  }

  /**
   * Dado um buraco de referência, informa o buraco do lado oposto do tabuleiro.
   *
   * @param   buraco   O buraco de referência.
   * @return  O buraco do lado oposto do tabuleiro
   */
  Buraco buracoOposto(Buraco buraco) {
    int index = getPosição(buraco) == EM_BAIXO ?
                KALAHA_DIREITA + (KALAHA_DIREITA - buraco.getNúmero()):
                KALAHA_ESQUERDA + (KALAHA_ESQUERDA - buraco.getNúmero());
    return buracos[index % TOTAL_BURACOS];
  }

  /**
   * Informa se um buraco está EM_CIMA, EM_BAIXO, ou é KALAHA.
   *
   * @param   buraco   O buraco de interesse.
   * @return  EM_BAIXO, para buracos de cima
   *          EM_CIMA, para buracos de baixo
   *          KALAHA para as duas Kalahas.
   */
  int getPosição(Buraco buraco) {
    int num = buraco.getNúmero();
    if(PRIMEIRO_BURACO_EM_BAIXO <= num && num <= ÚLTIMO_BURACO_EM_BAIXO) {
      return EM_BAIXO;
    } else if(PRIMEIRO_BURACO_EM_CIMA <= num && num <= ÚLTIMO_BURACO_EM_CIMA) {
      return EM_CIMA;
    } else {
      return KALAHA;
    }
  }

  /**
   * Converte um número absoluto (de 0 a 13) para um índice de buraco (0 a 5)
   * Se o buraco não corresponder à posição desejada, lança exceção.
   * (Isso fede e precisa de refatoramento)
   *
   * @param   numAbsoluto   Um número de buraco do tabuleiro, de 0 a 13.
   * @param   posição   A posição (EM_BAIXO, EM_CIMA) na qual este buraco deve estar.
   * @return  O índice do buraco correspondente (de 0 a 5).
   * @throws  MancalaException se O buraco não se localiza na posição indicada.
   */
  public int numAIndex(int numAbsoluto, int posição) throws MancalaException {
    if(posição == EM_CIMA) {
      if(PRIMEIRO_BURACO_EM_CIMA <= numAbsoluto && numAbsoluto <= ÚLTIMO_BURACO_EM_CIMA) {
        return numAbsoluto - PRIMEIRO_BURACO_EM_CIMA;
      }
    } else {
      if(PRIMEIRO_BURACO_EM_BAIXO <= numAbsoluto && numAbsoluto <= ÚLTIMO_BURACO_EM_BAIXO) {
        return numAbsoluto - PRIMEIRO_BURACO_EM_BAIXO;
      }
    }
    throw new MancalaException("Esse buraco nao eh seu!");
  }

  /**
   * Duplica o tabuleiro. É um Deep Copy.
   *
   * @return  O clone do objeto.
   */
  public Object clone() {
    Tabuleiro novo = new Tabuleiro();
    for(int i = 0; i < TOTAL_BURACOS; i++) {
      novo.buracos[i].adicionaSementes(this.buracos[i].getNúmeroDeSementes());
    }
    return novo;
  }
}

package p1.aplic.mancala.jogo;

/**
 * Este classe representa um buraco do tabuleiro do jogo de mancala.
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public class Buraco {
  /**
   * O número que identifica do buraco (de 0 a 13)
   */
  private int número;
  /**
   * O número de sementes no buraco
   */
  private int numSementes;

  /**
   * Construtor de um buraco com dada identificação
   *
   * @param   número   A identificação do buraco (de 0 a 13).
   */
  Buraco(int número) {
    this(número, 0);
  }

  /**
   * Construtor de um buraco com dada identificação e com sementes iniciais.
   *
   * @param   número   A identificação do buraco (de 0 a 13).
   * @param   numSementes   O número de sementes a colocar inicialmente no buraco.
   */
  Buraco(int número, int numSementes) {
    this.número = número;
    this.numSementes = numSementes;
  }

  /**
   * Informa quantas sementes estão no buraco.
   *
   * @return  O número de sementes no buraco.
   */
  public int getNúmeroDeSementes() {
    return numSementes;
  }

  /**
   * Informa a identificação do buraco.
   *
   * @return  A identificação do buraco (de 0 a 13)
   */
  public int getNúmero() {
    return número;
  }

  /**
   * Adiciona sementes ao buraco.
   *
   * @param   numSementes   O número de sementes a adicionar ao buraco.
   */
  void adicionaSementes(int numSementes) {
    this.numSementes += numSementes;
  }

  /**
   * Remove sementes do buraco.
   *
   * @param   numSementes   O número de sementes a remover do buraco.
   */
  void removeSementes(int numSementes) {
    this.numSementes -= numSementes;
  }

  /**
   * Representa o buraco como string
   *
   * @return  Um string representando o buraco.
   */
  public String toString() {
    return "Buraco " + número + ", " + getNúmeroDeSementes() + " sementes";
  }
}

O Observer: Events e Listeners

package p1.aplic.mancala.jogo;

/**
 * Representação de um evento "interessante" de um jogo de Mancala.
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public class MancalaEvent extends java.util.EventObject {
  /**
   * Um possível evento de jogo: o jogo acabou de iniciar mas ninguém jogou ainda.
   */
  public final static int INICIO_DE_JOGO = 0;

  /**
   * Um possível evento de jogo: um jogador acabou de jogar.
   */
  public final static int JOGADOR_JOGOU = 1;

  /**
   * Um possível evento de jogo: o jogo acabou.
   */
  public final static int FIM_DE_JOGO = 2;

  private Buraco buraco;
  private Jogador jogadorAtual;
  private Jogador jogadorQueJogou;

  /**
   * Construtor de um evento de jogo.
   *
   * @param   source   O jogo que é a fonte do evento.
   * @param   buraco   O buraco que acabou de ser jogado.
   * @param   jogadorAtual   O próximo jogador a jogar.
   * @param   jogadorQueJogou   Quem acabou de jogar.
   */
  public MancalaEvent(JogoMancala source, Buraco buraco, Jogador jogadorAtual, Jogador jogadorQueJogou) {
    super(source);
    this.buraco = buraco;
    this.jogadorAtual = jogadorAtual;
    this.jogadorQueJogou = jogadorQueJogou;
  }

  /**
   * Accessor para o buraco jogado.
   *
   * @return  O buraco que foi jogado.
   */
  public Buraco getBuraco() {
    return buraco;
  }

  /**
   * Accessor para o jogador que acabou de jogar.
   *
   * @return  O jogador que acabou de jogar.
   */
  public Jogador getJogadorQueJogou() {
    return jogadorQueJogou;
  }

  /**
   * Accessor para o próximo jogador a jogar.
   *
   * @return  O próximo jogador a jogar.
   */
  public Jogador getJogadorAtual() {
    return jogadorAtual;
  }
}
package p1.aplic.mancala.jogo;

/**
 * Listeners de um jogo de Mancala são normalmente objetos que implementam
 * interfaces com o usuário para o jogo. Tais objetos
 * devem implementar essa interface.
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public interface MancalaListener extends java.util.EventListener {
  /**
   * Este método do listener é chamado para indicar que o jogo iniciou.
   *
   * @param   evento   O objeto que descreve o inicio do jogo.
   */
   void inicioDeJogo(MancalaEvent evento);

  /**
   * Este método do listener é chamado para indicar que um jogador jogou.
   *
   * @param   evento   O objeto que descreve a jogada feita.
   */
   void jogadorJogou(MancalaEvent evento);

  /**
   * Este método do listener é chamado para indicar que o jogo terminou.
   *
   * @param   evento   O objeto que descreve o resultado do jogo.
   */
   void fimDeJogo(MancalaEvent evento);
}
package p1.aplic.mancala.cui;
import p1.aplic.mancala.jogo.*;
import java.util.*; 

/**
 * Classe que recebe os eventos de jogo e fornece a saída
 * com interface a caractere.
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public class OutputMancalaCUI implements MancalaListener {

  /**
   * Recebe o evento de início de jogo.
   *
   * @param   evento   O evento INICIO_DE_JOGO.
   *                   O evento contém o jogo fonte e o primeiro jogador.
   */
  public void inicioDeJogo(MancalaEvent evento) {
    JogoMancala jogo = (JogoMancala)evento.getSource();
    mostraTabuleiro(jogo, evento.getJogadorAtual());
  }

  /**
   * Recebe o evento de jogada feita.
   *
   * @param   evento   O evento JOGADOR_JOGOU.
   *                   O evento contém o jogo fonte,
   *                   o buraco jogado, quem jogou,
   *                   e o próximo jogador.
   */
  public void jogadorJogou(MancalaEvent evento) {
    JogoMancala jogo = (JogoMancala)evento.getSource();
    System.out.println("Jogador " + evento.getJogadorQueJogou().getNome() +
                       " jogou buraco " + (evento.getBuraco().getNúmero()+1));
    mostraTabuleiro(jogo, evento.getJogadorAtual());
  }

  /**
   * Recebe o evento de fim de jogo.
   *
   * @param   evento   O evento FIM_DE_JOGO.
   *                   O evento contém o jogo fonte,
   *                   e quem jogou.
   */
  public void fimDeJogo(MancalaEvent evento) {
    JogoMancala jogo = (JogoMancala)evento.getSource();
    System.out.println();
    System.out.println("Fim do jogo");
    mostraTabuleiro(jogo, evento.getJogadorAtual());
    System.out.println();
    if(jogo.getGanhador() == null) {
      System.out.println("Empate!");
    } else {
      System.out.println("Jogador " + jogo.getGanhador().getNome() + " ganhou!");
    }
  }

  /**
   * Desenha o tabuleiro na saída.
   *
   * @param   jogo   O jogo sendo jogado.
   * @param   jogadorAtual   O próximo jogadora jogar.
   */
  private void mostraTabuleiro(JogoMancala jogo, Jogador jogadorAtual) {
    final String SEPARADOR = "-----------------------";
    String espacoLinhaMancala = "";  // Usado para colocar espacos na linha
                                     // da mancala

    System.out.println("  13 12 11 10  9  8");
    System.out.println(SEPARADOR);
    // linha de buracos do jogador de cima
    System.out.print( "   " );
    for(int i = Tabuleiro.NUM_BURACOS-1; i >= 0; i--) {
      Buraco buraco = jogo.getTabuleiro().getBuraco(Tabuleiro.EM_CIMA, i);
      System.out.print( buraco.getNúmeroDeSementes() + "  " );
      espacoLinhaMancala += "   ";
    }
    mostraJogador(jogo.getJogador(1), jogadorAtual, jogo);

    // linha das mancalas
    System.out.print(jogo.getKalaha(jogo.getJogador(1)).getNúmeroDeSementes() + "   " );
    System.out.print( espacoLinhaMancala );
    System.out.println(jogo.getKalaha(jogo.getJogador(0)).getNúmeroDeSementes());

    // linha de buracos do jogador de baixo
    System.out.print( "   " );
    for(int i = 0; i < Tabuleiro.NUM_BURACOS; i++) {
      Buraco buraco = jogo.getTabuleiro().getBuraco(Tabuleiro.EM_BAIXO, i);
      System.out.print( buraco.getNúmeroDeSementes() + "  " );
      espacoLinhaMancala += "   ";
    }
    mostraJogador(jogo.getJogador(0), jogadorAtual, jogo);
    System.out.println(SEPARADOR);
    System.out.println("   1  2  3  4  5  6");
    p1.io.Entrada.in.lerLinha();
  }

  /**
   * Imprima a informação de um jogador na saída.
   *
   * @param   jogador   O jogador sendo impresso.
   * @param   jogadorAtual   Quem é o próximo jogador a jogar no jogo.
   * @param   jogo   O jogo sendo jogado.
   */
  private void mostraJogador(Jogador jogador, Jogador jogadorAtual, JogoMancala jogo) {
    // indicador da vez
    if(jogador == jogadorAtual) {
      System.out.print( "            -->" );
    } else {
      System.out.print( "               ");
    }
    // informacao sobre o jogador
    System.out.println("Jogador " + jogador.getNome());
  }
}

Os jogadores automáticos

package p1.aplic.mancala.jogo;
import java.util.*;

/**
 * Um jogador automático bastante burrinho.
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public class JogadorComputador1 extends JogadorAbstrato {
  /**
   * Construtor de um jogador automático (computador)
   * com algoritmo bem burrinho para jogar.
   *
   * @param   nome   O nome do jogador.
   * @param   posição   A posição do jogador (EM_BAIXO ou EM_CIMA).
   */
  public JogadorComputador1(String nome, int posição) {
    super(nome, posição);
  }

  /**
   * Pede ao jogador para escolher um buraco para jogar.
   * A estratégia é burra: o primeiro buraco que contém sementes
   *
   * @param   jogo   O jogo do qual o jogador está participando.
   * @return  O número do buraco (entre 0 e 5)
   * @throws  MancalaException se o buraco escolhido não for possível.
   */
  public int escolheJogada(JogoMancala jogo) throws MancalaException {
    for(int i = 0; i < Tabuleiro.NUM_BURACOS; i++) {
      Buraco buraco = jogo.getTabuleiro().getBuraco(getPosição(), i);
      if(buraco.getNúmeroDeSementes() > 0) {
        return i;
      }
    }
    throw new MancalaException("A partida nao terminou mas nao tenho sementes!");
  }
}
package p1.aplic.mancala.jogo;
import java.util.*;

/**
 * Um jogador automático mais-ou-menos razoável para jogar Mancala.
 *
 * @author   Jacques Philippe Sauvé, jacques@dsc.ufpb.br
 * @version 1.0
 * <br>
 * Copyright (C) 2001 Universidade Federal da Paraíba.
 */
public class JogadorComputador2 extends JogadorAbstrato {
  /**
   * Construtor de um jogador automático (computador)
   * com algoritmo razoável para jogar.
   *
   * @param   nome   O nome do jogador.
   * @param   posição   A posição do jogador (EM_BAIXO ou EM_CIMA).
   */
  public JogadorComputador2(String nome, int posição) {
    super(nome, posição);
  }

  /**
   * Pede ao jogador para escolher um buraco para jogar.
   * A estratégia é razoável: tenta todos os buracos e escolhe o que
   * dá "melhor" resultado, numa avaliação simples.
   *
   * @param   jogo   O jogo do qual o jogador está participando.
   * @return  O número do buraco (entre 0 e 5)
   * @throws  MancalaException se o buraco escolhido não for possível.
   */
  public int escolheJogada(JogoMancala jogo) throws MancalaException {
    // Jogador eh computador: devemos determinar a melhor jogada
    int melhorJogada = -1;
    int jogadaComRepetição = -1;
    int maxPedrasAdicionais = -1;

    // tente as possiveis jogadas
    Tabuleiro tabuleiroOriginal = jogo.getTabuleiro();
    int sementesOriginais = jogo.getKalaha(this).getNúmeroDeSementes();
    for(int i = 0; i < Tabuleiro.NUM_BURACOS; i++) {
      if(tabuleiroOriginal.getBuraco(getPosição(), i).getNúmeroDeSementes() > 0) {
        Tabuleiro tabuleiroDeTeste = (Tabuleiro)tabuleiroOriginal.clone();
        jogo.setTabuleiro(tabuleiroDeTeste);
        if(jogo.umaJogada(i, JogoMancala.SEM_EVENTO) == this) {
          jogadaComRepetição = i;
        }
        int pedrasAdicionais = jogo.getKalaha(this).getNúmeroDeSementes() -
                               sementesOriginais;
        if( pedrasAdicionais > maxPedrasAdicionais ) {
          maxPedrasAdicionais = pedrasAdicionais;
          melhorJogada = i;
        }
      }
    }
    jogo.setTabuleiro(tabuleiroOriginal);

    // tentamos todas as possibilidades: escolhe a melhor
    if(maxPedrasAdicionais > 1) {
      return melhorJogada;
    } else if(jogadaComRepetição >= 0) {
      return jogadaComRepetição;
    } else {
      return melhorJogada;
    }
  }
}

A Implementação da Versão Gráfica

exemplo-1 programa