Denne delen av oppgaven omhandler Dice-klassen, som brukes til å representere (verdien av) én eller flere terninger (engelsk: dice = terninger, die = terning), og en mulig poengverdi (score) for terningene. En slik klasse kan være nyttig i mange typer terningspill. I f.eks. Yatzy kan en Dice-instans brukes til både å representere et helt kast før det er gitt poeng, og terningene en ender opp med etter en runde, med poengene satt.

Dice-klassen er vist under, ... erstatter kodefragmenter som det spørres om i oppgaveteksten. For oversiktens skyld er det også oppgitt et grensesnitt som brukes i senere oppgaver.

/**
* Represents a set of die values. A die has six possible values 1-6,
* but the number of dice may vary from Dice instance to Dice instance.
* In addition, a Dice-instance can have a score.
*/
public class Dice implements Iterable<Integer> {

   /**
   * @param dieCount
   * @return a collection of random integer values in the range 1-6
   */
   public static Collection<Integer> randomDieValues(int dieCount) {
      ...
   }

   ... fields (part 1) ...

   /** (part 1)
   * Initializes this Dice with the values in dieValues, and a score.
   * @param dieValues
   * @param score the score to set, may be -1 for yet unknown
   * @throws a suitable exception if the die values are outside the valid range
   */
   public Dice(Collection<Integer> dieValues, int score) {
      ...
   }

   /** (part 1)
   * Initializes this Dice with dieCount random values (using Math.random())
   * @param dieCount
   */
   public Dice(int dieCount) {
      ...
   }

   /** (part 1)
   * Initializes this Dice with the values in dice, and a score
   * @param dieValues // Denne skulle vært bare "dice", ikke "dieValues"
   * @param score the score to set, may be -1 for yet unknown
   */
   public Dice(Dice dice, int score) {
      ...
   }

   /** (part 2)
   * Format: [die1, die2, ...] = score (score is omitted when < 0)
   */
   ... method for generating a String representation of a Dice instance ...

   /** (part 2)
   * Parses a string using the toString format (see above) and
   * returns a corresponding Dice.
   * @param s
   * @return a new Dice instance initialized with die values and score from the String argument
   */
   public static Dice valueOf(String s) {
      ...
   }

   /** (part 3)
   * @return the number of die values
   */
   public int getDieCount() {
      ...
   }

   /** (part 3)
   * @param dieNum
   * @return the value of die number dieNum
   */
   public int getDieValue(int dieNum) {
      ...
   }

   /** (part 3)
   * @param value
   * @return the number of dice with the provided value
   */
   public int getValueCount(int value) {
      ...
   }

   /** (part 4)
   * @return the current score
   */
   public int getScore() {
      ...
   }

   /** (part 4)
   * Sets the score, but only if it isn't already set to a non-negative value
   * @param score
   * @throws a suitable exception if score already is set to a non-negative value
   */
   public void setScore(int score) {
      ...
   }

   ... Iterable methods (part 5) ...

   /** (part 6) // Denne ble det ikke spurt om, og det var ikke meningen at den skulle implementeres, men den kunne brukes
   * @param dice
   * @return true if all die values in the argument appear in this Dice
   */
   public boolean contains(Dice dice) {
      ...
   }

   /** (part 6)
   * @param dices a Collection of Dice // Denne linja var feil, det skulle være bare "dice a Dice"
   * @return a new Dice instance with the all the die values this Dice and
   * all Dice in the argument, without any specific order
   */
   public Dice add(Dice dice) {
      ...
   }

   /** (part 6)
   * @param dice
   * @return a new Dice instance with the die values from this Dice, but
   * without those from the argument, without any specific order
   */
   public Dice remove(Dice dice) {
      ...
   }
}

/** (part 7)
* Interface for scoring rules, i.e.
* logic for computing a score for a subset of dice in a Dice
*/
public interface DiceScorer {
   /**
   * Computes a score for (a subset of) the dice in the Dice argument.
   * The return value includes those dice that gives the score, and
   * of course the score itself.
   * @param dice
   * @return The dice for which the rule computes a score, and the score itself, or
   * null, if this rule isn't applicable
   */
   Dice getScore(Dice dice);
}
Oppgave a) - Felt og konstruktører (6 poeng)

En Dice-instans skal ha informasjon om (verdien til) et visst antall terninger og en poengverdi (score). Terning-verdiene settes ved initialisering (på ulike måter), mens poengverdien kan settes ved initialisering eller senere.

Skriv kode for felt og konstruktører, samt metoden randomDieValuesiht. API-beskrivelsen. Bruk random-metoden i Math-klassen, som er beskrevet som følger:

public static double random()

Returns a double value with a positive sign, greater than or equal to 0.0 and less than 1.0. Returned values are chosen pseudorandomly with (approximately) uniform distribution from that range.

public static Collection<Integer> randomDieValues(int dieCount) {
   Collection<Integer> dieValues = new ArrayList<>(dieCount);
   while (dieValues.size() < dieCount) {
      dieValues.add((int) (Math.random() * 6 + 1));
   }
   return dieValues;
   // Evt. med Stream-teknikken:
   // return new Random().ints(dieCount, 1,6).boxed().collect(Collectors.toList())
}
 
private final List<Integer> dieValues = new ArrayList<>(); // Kunne også brukt int[]-typen og lage den i konstruktøren
private int score = -1;

public Dice(Collection<Integer> dieValues, int score) {
   // Kunne brukt this.dieValues.addAll(dieValues), en da får en ikke validert enkeltverdiene
   for (int dieValue : dieValues) {
      if (! (dieValue >= 1 && dieValue <= 6)) {
         throw new IllegalArgumentException("A die value must be in the range 1-6");
      }
      this.dieValues.add(dieValue);
   }
   this.score = score;
}

public Dice(int dieCount) {
   this(Dice.randomDieValues(dieCount), -1);
}

public Dice(Dice dice, int score) {
   this(dice.dieValues, score);
}

Vanlige feil:

  • Bruk av Collection som typen til samlingen av terningverdier. Dette gjør indeksbasert uthenting av verdier unaturlig og tregt. Selv om man kan implementere getDieValue-metoden uten en tilsvarende get-metode, f.eks. med en Iterator, så var litt av poenget å velge en hensiktsmessig type, og da er List riktig. Mange kommenterte at det var unaturlig, men at det virket som om det var slik det var ment, siden konstruktør-argumentet var en Collection.
  • Manglende validering av terningverdiene i konstruktøren som tar inn en Collection. Dette er nødvendig siden mye av logikken er avhengig av at terningverdiene er lovlige.
  • Direkte bruk av Collection-argumentet i konstruktøren, i stedet for å kopiere innholdet over i en ny liste. Uten dette, så kan man endre lista utenifra, siden Collection-objektet er delt.
  • Manglende initialisering av score i konstruktørene.
  • Bruk av null som verdi for score-egenskapen.
  • Setting av lista med terningverdier i den statiske randomDieValues-metoden. Denne metoden er jo ikke knyttet til noen instans og skal bare lage og returnere en Collection.
  • Feil omregning av random-verdien mellom 0.0 og til (men ikke med) 1.0 til en terningverdi fra 1 til og med 6.
Oppgave b) - Dice som tekst (8 poeng)

Skriv standardmetodensom brukes for å lage en tekstlig representasjon av et Dice-objekt på formatet som er angitt i API-beskrivelsen. Skriv også valueOf-metoden, som brukes for å lage et nytt Dice-objekt med spesifikke terningverdier og evt. en poengverdi fra en Stringpå samme format.

 

Formatet er "[t1, t2,... tn]=poeng", hvor t1tn er terningverdier og poeng er poengverdien. Poeng-delen, altså " =poeng" skal ikke være med hvis poeng er -1 (ennå ikke satt).

Eksempel: Hvis et Dice-objekt har terningverdiene 1, 1 og 3 og poengene ikke er satt (= -1), så vil den tekstlige representasjonen være "[1, 1, 3]". Dersom terningene er tre 6-ere som har gitt 600 poeng, så vil teksten være "[6, 6, 6] = 600".

@Override
public String toString() {
   String result = "[";
   for (int dieValue : dieValues) {
      if (result.length() > 1) {
         result = result + ", ";
      }
      result = result + dieValue;
   }
   result = result + "]";
   // Det over er samme som: result = dieValues.toString()
   if (getScore() >= 0) {
      result = result + " = " + getScore();
   }
   return result;
}
 
public static Dice valueOf(String s) {
   int score = -1;
   int scorePos = s.indexOf("=");
   if (scorePos >= 0) {
      score = Integer.valueOf(s.substring(scorePos + 1).trim());
      s = s.substring(0, scorePos).trim();
   }
   if (s.startsWith("[") && s.endsWith("]")) {
      s = s.substring(1, s.length() - 1);
   } else {
      throw new IllegalArgumentException("Illegal format");
   }
   String[] dieValueStrings = s.split(",");
   Collection<Integer> dieValues = new ArrayList<>();
   for (int i = 0; i < dieValueStrings.length; i++) {
      dieValues.add(Integer.valueOf(dieValueStrings[i].trim()));
   }
   return new Dice(dieValues, score);
}

Vanlige feil:

  • Et ekstra kommaskilletegn i toString-resultatet.
  • Ikke sjekke om score var ulik -1, før en evt. legger den til på slutten etter likhetstegnet.
  • Ideelt sett skal en sjekke syntaks og utløse hjelpsomme unntak. Men hvis en antar at syntaksen er riktig, og implisitt blir utløst unntak likevel, når en f.eks. antar at strengen inneholder et likhetstegn, så går det i praksis for det samme. Det er nesten verre å å lage så vanntett valueOf-kode at det ikke utløses unntak når syntaksen er feil.
  • Bruk av en annen type enn String[] for resultatet fra split, f.eks. List.
Oppgave c) - Terningverdier (5 poeng)

Skriv metodene getDieCountgetDieValue og getValueCount iht. API-beskrivelsen.

public int getDieCount() {
   return this.dieValues.size();
}


public int getDieValue(int dieNum) {
   return this.dieValues.get(dieNum);
}

public int getValueCount(int value) {
   int count = 0;
   for (int dieValue : this.dieValues) {
      if (dieValue == value) {
         count++;
      }
   }
   return count;
   // Alternativ med streams: return (int) this.dieValues.stream().filter(dieValue -> value == dieValue).count();
 }

Vanlige feil:

  • Bruke get når (den deklarerte) typen til dieValues eller tilsvarende variabel ikke er en List.
Oppgave d) - Poeng (score) (3 poeng)

Skriv metodene getScore og setScore iht. API-beskrivelsen.

public int getScore() {
   return score;
}

public void setScore(int score) {
   if (this.score >= 0) {
      throw new IllegalStateException("Cannot set score more than once");
   }
   this.score = score;
}

Vanlige feil:

  • Sjekke this.score med > 0, ikke >= 0 eller > -1.
Oppgave e) - Iterable (3 poeng)

Dice-klassen implementerer Iterable<Integer>-grensesnittet. Implementer metoden(e) som da er nødvendig. Skriv også kode som eksemplifiserer hvordan man ved bruk av Dicekan dra nytte av at den implementerer nettopp dette grensesnittet.

@Override
public Iterator<Integer> iterator() {
   return this.dieValues.iterator();
}
 

Kan da skrive

Dice dice = ...
for (int dieValue : dice) {
   ... her er dieValue neste verdi i dice sin dieValues-liste
}

Vanlige feil:

  • Si at fordelen er å kunne bruke iterator()-metoden direkte, ikke implisitt med foreach-løkketypen.
Oppgave f) - add og remove (8 poeng)

Skriv metodene add og remove, som alle tar et Dice-objekt som eneste argument. Merk at ingen av disse endrer på verken this-objektet eller argumentet, og poengverdien(e) benyttes ikke.

 

Her er noen eksempler på bruken av disse metodene, hvor tekst-formatet i API-beskrivelsen brukes for å representere Dice-objekter:

[1, 2].add([1, 4]) returnerer [1, 2, 1, 4] // merk at rekkefølgen ikke spiller noen rolle

[1, 1, 2].remove([1, 4]) gir [1, 2] // merk at rekkefølgen ikke spiller noen rolle

Merk at remove ikke har samme logikk som Collectionsin removeAll-metode.

Add-metoden skal være nokså rett frem, men remove-metoden kompliseres av håndtering av duplikater. Her lages den en kopi av dieValues-lista og så fjernes én og én av verdiene som skal fjernes. Dersom en fjerner med removeAll, så vil alle forekomstene av samme verdi fjernes på én gang, og det er ikke riktig. En må bruke en av remove-metodene som bare fjerner ett element, enten remove(Integer) eller remove(int), hvor den siste fjerner elementet på den angitte posisjonen.

public Dice add(Dice dice) {
   Collection<Integer> dieValues = new ArrayList<>(this.dieValues);
   for (int dieValue : dice.dieValues) {
      dieValues.add(dieValue);
   }
   // evt. dieValue.addAll(dice.dieValues) i stedet for løkka
   return new Dice(dieValues, -1);
}

public Dice remove(final Dice dice) {
   List<Integer> dieValues = new ArrayList<>(this.dieValues);
   for (int dieValue : dice.dieValues) {
      int pos = dieValues.indexOf(dieValue);
      if (pos >= 0) {
         dieValues.remove(pos);
      }
      // kan ikke bruke remove(dieValue) fordi da blir dieValue brukt som om den er en indeks
   }
   return new Dice(dieValues, -1);
}

Vanlige feil:

  • Endre på den interne dieValues-lista, enten ved å bruke variabelen direkte, eller ved å deklarere en ny variabel, men sette den til den interne lista, slik at denne likevel blir endret.
  • Fjerne alle verdiene for et bestemt tall, selv om bare et begrenset antall skal fjernes. Dette er i praksis logikken til removeAll, som det ble hintet om i oppgaveteksten.

 

Denne deloppgaven handler om poengberegning basert på et sett terningverdier. Felles for mange terningspill er at man kaster terninger og så finner ut hvor mange poeng en får basert på terningverdiene. Visse kombinasjoner gir mer elle mindre poeng, og noen gir ingen. I Yatzy har en mange poenggivende kombinasjoner, f.eks. ett par (to like), to par, tre og fire like, liten (1-5) og stor (2-6) straight, hus (ett par og tre like) og Yatzy, og poengene en får er stort sett summen av terningverdiene som inngår i kombinasjonen. I Farkle, derimot, så får en 100 poeng for enere, 50 poeng for femmere, og for de andre får en bare poeng hvis en får tre eller flere i slengen (se vedlegg for detaljene).

For å støtte (minst) begge disse spillene, så introduseres et DiceScorer-grensesnitt, som representerer en poengregel generelt, og én implementasjonsklasse for hver (type) regel. DiceScorer-grensesnittet har kun én metode, getScore, som tar inn et Dice-objekt med alle terningverdiene som skal vurderes samlet og returnerer et nytt Dice-objekt med akkurat de terningverdiene som dekkes av regelen og med poengverdien satt riktig for akkurat disse terningverdiene:

/**
* Interface for scoring rules, i.e.
* logic for computing a score for a subset of dice in a Dice
*/
public interface DiceScorer {
   /**
   * Computes a score for (a subset of) the dice in the Dice argument.
   * The return value includes those dice that gives the score, and
   * of course the score itself.
   * @param dice
   * @return The dice for which the rule computes a score, and the score itself, or
   * null, if this rule isn't applicable
   */
   Dice getScore(Dice dice);
}

En implementasjon av DiceScorer som tilsvarer "tre like"-regelen i Yatzy, vil altså returnere et Dice-objekt med de tre like terningverdiene den finner i Dice-argumentet, eller null hvis den ikke finner tre like verdier. I tillegg settes poengene i Dice-objektet som returneres til riktig verdi. Hvis f.eks. argumentet er [1, 2, 2, 2, 5] så vil den iht. Yatzy-logikk returnere [2, 2, 2] = 6. En tilsvarende implementasjon for Farkle vil ha samme logikk for å finne tre like, men ha annen logikk for å sette poengene ( = 2 * 100).

Oppgave a) - Tre eller flere like (6 poeng)

Skriv kode for klassen ThreeOrMoreOfAKind (tre eller flere like), som implementerer DiceScorer med følgende logikk:

For et Dice-argument uten tre eller flere like returneres null. Ellers returneres et (nytt) Dice-objekt med alle de like terningverdiene, altså tre eller flere like verdier. For tre like får en 100 ganger verdien det er tre eller flere like av, f.eks. 300 poeng for tre 3-ere. For hver ekstra terning med samme verdi så doblespoengene, f.eks. får en 1200 (= 300 * 2 * 2) poeng for fem 3-ere.

Merk at hvis en har mange nok terninger, så kan en få tre eller flere like for flere terningverdier. En skal da velge den terningverdien som gir flest poeng. Hvis en f.eks. har terningene [2, 2, 2, 2, 5, 5, 5] så skal en velge de tre 5-erne, siden de gir 500 poeng, mens de fire 2-erne gir bare 400. Hvis flere terningverdier gir samme, høyeste poengverdi, så skal den med flest like terninger velges.

public class ThreeOrMoreOfAKind implements DiceScorer {


   // hjelpemetode som beregner poengsum for count antall av value
   private int getScore(final int value, int count) {
      int score = value * 100;
      while (count > 3) {
         score = score * 2;
         count--;
      }
      return score;
      // Samme som: return value * 100 * (int) Math.pow(count - 3, 2);
   }

   // går gjennom hver mulige verdi 1-6 og finner den med nok like som gir flest poeng
   protected int getBestValue(final Dice dice) {
      int value = 0, max = 0;
      for (int i = 1; i <= 6; i++) {
         int count = dice.getValueCount(i);
         int score = getScore(i, count);
         if (count >= 3 && score > max) {
            value = i;
            max = score;
         }
      }
      return value;
   }

   @Override
   // finner den beste og lager et nytt Dice-objekt med riktig antall terningverdier og poengsum
   public Dice getScore(final Dice dice) {
      int best = getBestValue(dice);
      if (best == 0) {
         return null;
      }
      int count = dice.getValueCount(best);
      Collection<Integer> dieValues = new ArrayList<Integer>(count);
      for (int i = 0; i < count; i++) {
         dieValues.add(best);
      }
      return new Dice(dieValues, getScore(best, count));
   }
}

Vanlige feil/svakheter:

  • Beregning av poeng med logikk tilsvarende multiplikasjon og ikke opphøyd i. Dette vil f.eks. gi 600 for fem 2-ere, i stedet for 800.
  • Anta at kombinasjonen med flest terninger gir flest poeng, men dette er ikke riktig. F.eks. gir fire 2-ere færre poeng enn tre 6-ere.
  • Returnere feil antall terninger i Dice-objektet som returneres.
  • Ikke bruke getValueCount-metoden, men beregne antallet på nytt.
Oppgave b) - Beregning av total poengsum (6 poeng)

Hvis en har mange terninger og er heldig, så kan én eller flere DiceScore-regler kombineres flere ganger. En får da summen av poengene som hver bruk av en regel gir. Én terning kan selvsagt ikke være med i mer enn én kombinasjon. Hvis en f.eks. har "tre eller flere like"-regelen implementert av ThreeOrMoreOfAKind-klassen og får fire 3-ere og tre 4-ere så vil en få 600 poeng for 3-erne og 400 poeng for 4-erne og dermed 1000 til sammen. 

Det blir mer komplisert hvis flere regler "konkurrerer" om de samme terningene. F.eks. vil jo de samme fire 3-ere og tre 4-ere utgjøre hus (to 3-ere og tre 4-ere) og ett par (to 3-ere), hvis tilsvarende poengregler var med.

Poengberegningen for et spesifikt spill gjøres generelt som følger:

  • En har en oversikt over alle DiceScorer-objektene som gjelder, f.eks. i en tabell eller liste. Gjør nødvendige antagelser om hvor disse er lagret og kan hentes ut.
  • En prøver alle DiceScorer-objektene etter tur og det som gir flest poeng brukes først. Terningene som er i den tilsvarende kombinasjonen fjernes, og så gjentar en dette til alle poenggivende terninger er "brukt opp". Resultatet er (en Collection med) alle Dice-objektene som tilsvarer de kombinasjonene som ble brukt med poengverdien satt.

For eksemplet over, med bare ThreeOrMoreOfAKind-regelen og terningene [1, 3, 3, 3, 3, 4, 4, 4, 5] så vil en få som resultat (en Collection med) Dice-objektene [3, 3, 3, 3] = 600og [4, 4, 4] = 400. To av terningene, 1 og 5, ble i dette tilfellet ikke poenggivende og dermed ikke med i resultatet.

/**
* Computes a set of Dice with scores for the provided Dice.
* @param dice
* @return the set of Dice with die values and corresponding scores.
*/
public Collection<Dice> computeDiceScores(Dice dice) {
   ...
}

Her antar vi at metoden computeDiceScores ligger i en klasse, som også har et felt for DiceScore-objektene

private Collection<DiceScorer> diceScorers;

public Collection<Dice> computeDiceScores(Dice dice) {
   Collection<Dice> result = new ArrayList<>();
   // Vi følger algoritmen gitt over:
   // Så lenge det er terninger igjen, så
   //   finner vi regelen som gir flest poeng og sparer på de terningene den "brukte" og
   //   fjerner dem fra gjenværende terninger
   while (dice.getDieCount() > 0) {
      Dice best = null;
      for (DiceScorer diceScorer : diceScorers) {
         Dice score = diceScorer.getScore(dice);
         if (score != null && (best == null || score.getScore() > best.getScore())) {
            best = score;
         }
      }
      if (best == null) {
         break;
      } else {
         result.add(best);
         dice = dice.remove(best);
      }
   }
   return result;
}

Vanlige feil/svakheter:

  • Ikke bruke en samling DiceScorer-objekter, men anta det bare var ett ThreeOrMoreOfAKind-objekt.
  • Bruke alle kombinasjoner som gir poeng, ikke bare den beste (i hver runde).
  • Bruke Dice.remove(Dice)-metoden uten å lagre resultatet, som om Dice-objektet den ble kalt på ble endret.

 

Oppgave c) - Summering med Stream-teknikken (3 poeng)

Vis hvordan du kan beregne totalpoengsummen som en Collection av Dice-objekter tilsvarer, vha. Stream-teknikken. Du har altså en Collection av Dice-objekter med poengverdier og skal summere disse poengverdiene:

Collection<Dice> diceObjectsWithScore = computeDiceScores(...);

int totalPoints = ... compute total points using the Stream technique ...

// Med reduce
diceCol.stream().map(Dice::getScore).reduce(0, (n1, n2) -> n1 + n2)
 
// Med sum (må bruke mapToInt i stedet for map for å få en IntStream, men det er vi ikke så nøye på ved sensur)
diceCol.stream().mapToInt(Dice::getScore).sum()
 
// Dice::getScore er her det samme som dice -> dice.getScore()

 

 

Det finnes flere måter å implementere Dice-klassen og alle dens metoder, med ulike fordeler og ulemper. En måte å tillate bruk av flere implementasjoner er å gjøre om Dice til et grensesnitt og så ha en eller flere implementasjonsklasser, hvor den eksisterende Dice-klassen blir en av disse:

public interface Dice {

   ...

}

public class DiceImpl1 implements Dice { ... tilsvarer løsningen i deloppgavene 1-6 ... }

public class DiceImpl2 implements Dice { ... alternativ løsning ... }

 

Navnene på implementasjonsklassene kan selvsagt være mer forklarende.

Oppgave a) - Dice-grensesnittet (4 poeng)

Dette var en flervalgsoppgave med to spørsmål og valg av ett av flere alternativer.

 

Spørsmål 1: Tre alternative grensesnitt er foreslått, basert på den nåværende Dice-klassen:

// Alternativ 1, alle metoder og konstruktører:
public interface Dice ... {
    static Collection<Integer> randomDieValues(int dieCount);
    Dice(Collection<Integer> dieValues, int score);
    Dice(int dieCount);
    Dice(Dice dice, int score);
    static Dice valueOf(String s);
    int getScore();
    void setScore(int score);
    int getDieCount();
    int getDieValue(int dieNum);
    int getValueCount(int value);
    boolean contains(Dice dice);
    Dice add(Dice dice);
    Dice remove(Dice dice);
}
 
// Alternativ 2, alle metoder:
public interface Dice ... {
    static Collection<Integer> randomDieValues(int dieCount);
    static Dice valueOf(String s);
    int getScore();
    void setScore(int score);
    int getDieCount();
    int getDieValue(int dieNum);
    int getValueCount(int value);
    boolean contains(Dice dice);
    Dice add(Dice dice);
    Dice remove(Dice dice);
}
 
// Alternativ 3, utvalgte metoder:
public interface Dice ... {
    int getScore();
    void setScore(int score);
    int getDieCount();
    int getDieValue(int dieNum);
    int getValueCount(int value);
    boolean contains(Dice dice);
    Dice add(Dice dice);
    Dice remove(Dice dice);
}

Spørsmål 2: Den opprinnelige Dice-klassen implementerer Iterable<Integer>. Spørsmålet er hvordan dette skal håndteres ved overgangen til et Dice-grensesnitt.

  1. Dice-grensesnittet utvide (extends) Iterable<Integer>.
  2. Dice-grensesnittet både utvide (extends) Iterable<Integer> og liste opp metoden(e) fra Iterable.
  3. Dice-grensesnittet  liste opp metoden(e) fra Iterable.
  4. Dice-grensesnittet utvide (extends) Iterable<Integer> og kan liste opp metoden(e) fra Iterable.

 

På det første spørsmålet er alternativ 3 riktig, for et grensesnitt skal ikke ha konstruktører og statiske metoder. Det er faktisk lov å ha statiske metoder i grensesnitt i Java, men det hører ikke med i objektorientert tenkning.

På det andre spørsmålet er alternativ 4 riktig, for et grensesnitt kan gjenta metoder fra et grensesnitt det utvider, men må ikke. Her ga vi (manuelt) poeng for alternativ 1.

Oppgave b) - Arv (6 poeng)

Hvis en har flere implementasjoner av det nye Dice-grensesnittet, så kan en regne med at visse deler av disse vil bli nokså eller helt like.

Ett aspekt som typisk vil bli (nokså) likt er håndtering av poengene (score). Forklar med tekst og/eller kode hvordan du vil håndtere det vha. arvingsmekanismen, slik at løsningen tillater stor grad av gjenbruk av kode i subklasser og blir ren og ryddig.  

Her er poenget å skille ut det som har med score å gjøre i en abstrakt superklasse, nemlig feltet score, (den delen av) konstruktøren som tar inn og setter score og getScore og setScore. Dette er viktigst.

Hvis en lar den abstrakte superklassen implementere Dice-grensesnittet evt. deklarer en del av Dice sine metoder som abstrakte metoder, så kan enda flere metoder implementeres, f.eks. trenger toString og getValueCount bare getDieCount og getDieValue. En trenger ikke implementere disse, men forklare at det går an og vil være lurt.

public abstract class AbstractDice implements Dice {

   private int score = -1;
 
   protected AbstractDiceImpl(int score) {
     this.score = score;
   }
 
   @Override
   public String toString() {
      ... kan implementeres fordi getDieCount og getDieValue er deklarert (selv om de er abstrakte) ...
   }
 
   // implementasjon som bare er avhengig av at Dice utvider Iterable
   public int getValueCount(int value) {
      int count = 0;
      // Iterable er implementert!
      for (int dieValue : this) {
         if (dieValue == value) {
            count++;
         }
      }
      return count;
   }
 
   @Override
   public int getScore() {
      return score;
   }
 
   @Override
   public void setScore(int score) {
      if (this.score >= 0) {
         throw new IllegalStateException("Cannot set score more than once");
      }
      this.score = score;
    }
}

Vanlige feil/svakheter:

  • Generell beskrivelse av arv og fraser om at arv gir gjenbruk, uten å være spesifikk på hva slags variabler og metoder som er aktuelle å ha i en superklasse i dette tilfellet.
  • Forklare hvordan en kan ha en generell superklasse for poengberegning, med subklasser for spesifikk spill som Yatzy og Farkle. Her var det spesifikt snakk om Dice, og poengberegning er ikke en del av Dice-grensesnittet eller naturlig å ha med i en Dice-klasse.
  • Ikke nevne muligheten til å implementere flere metoder, f.eks. toString, som bruker deklarerte abstrakte metoder.
Oppgave c) - Delegering (10 poeng)

Dice sin add-metode skal lage en ny Dice-instans (altså instans av en klasse som implementerer Dice) som kombinerer terningverdier fra to andre Dice-instanser (this og argumentet). En kan tenke seg at metoden returnerer en instans av en ny Dice-implementasjon kalt DiceAdder, som bruker delegering. Den vil ha to Dice-felt og en konstruktør som tar inn to Dice-instanser:

DiceAdder(Dice dice1, Dice dice2) { ... feltene settes her ... }

Hver Dice-metode kan da bruke/delegere til disse to Dice-instansene i sin løsning, f.eks. vil getDieCount() returnere summen av getDieCount() for hver av de to Dice-instansene:

public int getDieCount() {

   return dice1.getDieCount() + dice2.getDieCount();

}

Forklar med tekst og/eller kode hvordan delegeringsteknikken vil bli brukt i følgende metoder i en slik DiceAdder-klasse:  getDieValuegetValueCountcontainsadd og remove. Kommenter spesielt hvis delegeringsteknikken ikke passer for en spesifikk metode!

Her ønsker vi kode for de som er greie å implementere med delegering, og tekst som forklarer hvorfor det ikke er greit. Det går også an med en blanding. Vi sa ingenting i oppgaven om håndtering av score, så det dropper vi helt her.

public class DiceAdder implements Dice {
 
   private final Dice dice1, dice2;
 
   public DiceAdder(Dice dice1, final Dice dice2) {
      super(score);
      this.dice1 = dice1;
      this.dice2 = dice2;
   }
 
   @Override
   public int getDieCount() {
      // Rett frem bruk av delegering, som vist i oppgaveteksten
      return dice1.getDieCount() + dice2.getDieCount();
   }
 
   @Override
   public int getDieValue(final int dieNum) {
      int dieCount1 = dice1.getDieCount();
      // Vi henter de første verdiene fra dice1,
      // så sjekker om indeksen er lav nok
      if (dieNum < dieCount1) {
         return dice1.getDieValue(dieNum);
      }
      // De neste verdiene hentes fra dice2, med (ned)justert indeks 
      return dice2.getDieValue(dieNum - dieCount1);
   }
 
   @Override
   public int getValueCount(final int value) {
      // Rett frem bruk av delegering, som vist i oppgaveteksten
      return dice1.getValueCount(value) + dice2.getValueCount(value);
   }
 
   @Override
   public Dice add(final Dice dice) {
      // Vi lager en instans av denne klassen,
      // den representerer jo nettopp to Dice slått sammen
      return new DiceAdder(this, dice);
   }
 
   @Override
   public boolean contains(final Dice dice) {
      // Dette er ikke så rett frem, men
      // algoritmen bruker delegering til getValueCount
      for (int dieValue = 1; dieValue <= 6; dieValue++) {
         if (dice.getValueCount(dieValue) > getValueCount(dieValue)) {
            return false;
         }
      }
      return true;
   }
 
   @Override
   public Dice remove(final Dice dice) {
      // Denne er ikke rett frem i det hele tatt, så den utelater vi...
   }
}

Vanlige feil/svakheter:

  • Implementere getDieValue ved å summere terningverdier fra de to Dice-instansene (én fra hver på den angitt indeksen). Det gir ikke mening i det hele tatt.
  • Implementere contains med dice1.contains(dice) || dice1.contains(dice) evt. med &&. Dette vil gi rett resultat i enkelttilfeller, men ikke generelt.
  • Implementere add og remove omtrent på samme måte som i del 1. Det kan virke, men er ikke delegering.

 

 

Så langt har koden vært relativt uavhengig av typen terningspill. I denne deloppgaven handler det om en runde i Farkle-spillet. I en slik runde så akkumuleres det poeng ved at en gjentatte ganger kaster terninger, og sparer på en eller flere poenggivende terningkombinasjoner.

Reglene for hvilke kombinasjoner som gir poeng varierer noe mellom ulike varianter av Farkle, men en får typisk poeng for tre eller flere like, og for femmere (50 poeng pr. stk) og enere (100 poeng pr. stk). En tenker seg at dette beregnes av en metode tilsvarende computeDiceScores fra deloppgave 8 om totalpoengberegning.

Etter hvert kast har en tre tilfeller/muligheter:

  1. Hvis ingen terningkombinasjoner gir poeng, så er runden over og en mister alle poengene en evt. har akkumulert.
  2. En kan velge å stoppe og får poeng for terningkombinasjonene en har spart så langt og for de gjenværende som en akkurat kastet.
  3. En kan velge å legge til side en eller flere poenggivende terningkombinasjoner, og få poeng for dem, og så kaste resten. Hvis en beholder alle terningene en kastet så det ikke er noen igjen å kaste, så kan en kaste alle på nytt.

For hvert kast så får en de samme tre mulighetene. Avveiningen i spillet er altså om en skal beholde poengene en har fått så langt, eller kaste på nytt for å få flere poeng, men risikere å miste alle. 

Her er noen eksempler som illustrerer reglene:

  1. En kaster 2, 3, 3, 4 og 4. Runden stopper opp av seg selv, med 0 poeng som resultat :-(
  2. En kaster 2, 3, 4, 5 og 5. En legger til side en 5-er for 50 poeng, og kaster de fire gjenværende terningene. En får 1, 3, 5 og 6. En legger til side 1-eren og 5-eren for 150 nye poeng, altså 200 til sammen så langt, og kaster de to terningene som er igjen. En får da 4 og 4, og mister alle poengene :-(
  3. En kaster som i pkt. 3, men stopper før det siste kastet og får altså 200 poeng for runden :-) // Her skulle det stått "som i pkt. 2"
  4. En kaster som over, men er heldig og får to 5-ere for 50+50 poeng i de siste kastet og kan kaste alle på nytt. En får da 1, 1, 1, 4 og 6, sparer de tre 1-erne for 1000 poeng (spesialregel for poenggiving i Farkle) og stopper. Runden gir da 200+50+50+1000 poeng :-)

Et forslag til realisering av en klasse som håndterer en Farkle-runde, er vist under. Noen deler er imidlertid utelatt...

/**
 * Represents a round of Farkle, where a player throws and keeps dice,
 * until s/he either "bank", i.e. save your points, or
 * farkle, i.e. get no points in a throw and hence looses all gathered points.
 * During and after a finished round, kept sets of dice and their scores are available.
 * During a round, the remaining dice are also available.
 */
public class FarkleRound {
    private int dieCount;
    private Collection<Dice> kept = new ArrayList<>();
    private Dice currentDice;
 
    /**
     * Initializes a new round, where dieCount number of dice is immediately rolled.
     * Note that the round may be immedielately finished, if the initial roll give no points.
     * @param dieCount the number of dice rolled
     * @param scoring the object responsible for computing the score
     */
    public FarkleRound(int dieCount) {
        this.dieCount = dieCount;
        roll(dieCount);
    }
    private void roll(int dieCount) {
        this.currentDice = new Dice(Dice.randomDieValues(dieCount), -1);
        if (computeDiceScores(currentDice).isEmpty()) {
            this.kept.clear();
            this.currentDice = null;
        }
    }
    private Collection<Dice> computeDiceScores(Dice dice) {
       ... implemented earlier in this exam ...
    }
 
    /**
     * @return true of the round is finised, false otherwise
     */
    public boolean isFinished() {
        return this.currentDice == null;
    }

    /**
     * Called when the player decides to stop and secure points.
     * Finishes the round, by keeping all scoring Dice, as computed by the scoring object.
     */
    public void bank() {
        if (isFinished()) {
            throw new ... which exception class ? ...
        }
        this.kept.addAll(computeDiceScores(currentDice));
        ... put this object in a state that indicates this round is finished ...
    }
 
    /**
     * Called when the player decides to keep specific dice and roll the rest.
     * All the dice kept must be scoring ones, but not all scoring dice need to be kept.
     * @param dice the dice to keep, the rest of the current dice should be thrown.
     */
    public void keepAndRoll(final Dice dice) {
        if (isFinished()) {
            throw new ... which exception class ? ...
        }
        if (! currentDice.contains(dice)) {
            throw new ... which exception class ? ...
        }
        final Collection<Dice> scores = computeDiceScores(dice);
        if (scores.stream().mapToInt(Dice::getDieCount).sum() != dice.getDieCount()) {
            throw new SomeKindOfException("You can only set aside dice that contribute to the score");
        }
        kept.addAll(scores);
        currentDice = currentDice.remove(dice);
        // roll remaining dice or all, if there are none left
        ... what code needs to be here ? ...
    }
}

Oppgave a) - final (2 poeng)

Dette var en flervalgsoppgave med ett spørsmål og to korrekte alternativer og ett galt et.

Spørsmål: Hvilke, om noen, av feltene øverst i FarkleRound-klassen kan ha modifikatoren final? 

  1. dieCount
  2. kept
  3. currentDice

 

Her er 1 og 2 riktig og 3 er gal, siden 1 og 2 bare settes én gang (i initialiseringsdel av deklarasjon eller i konstruktør). Riktignok endres innholdet i kept-lista, men det er det samme objektet som endres hele tiden.

Oppgave b) - Kode i bank (2 poeng)

Dette er en tekstinnfyllingsoppgave som vurderes automatisk.

Spørsmål: Nederst i bank-metoden er en kodelinje erstattet med "... put this object in a state that indicates this round is finished ...". Skriv inn den korrekte kodelinjen. Koden må være korrekt, komplett og konsis, uten kommentarer, siden den sjekkes automatisk.

Her er poenget å skjønne at isFinished-metoden i FarkleRound sjekker om currentDice er null, så koden må sette dette feltet til null.

Alternativene som ble godkjent var currentDice = null med eller uten this foran og ; bak. Mellomrom rundt = ignoreres.

 

Oppgave c) - Unntakstyper (3 poeng)

Dette er en flervalgsoppgave med tre spørsmål.

keepAndRoll-metoden utløses unntak i tre tilfeller.

Spørsmål 1: Hvilken av disse unntakstypene passer i det første tilfellet?

  1. Exception
  2. RuntimeException
  3. IllegalArgumentException
  4. IllegalStateException

Spørsmål 2: Hvilken av disse unntakstypene passer i det andre tilfellet?

  1. Exception
  2. RuntimeException
  3. IllegalArgumentException
  4. IllegalStateException

Spørsmål 3: I det tredje tilfellet bruker vi unntakstypen SomeKindOfException, som er tenkt som en egendefinert unntakstype. Hvilken unntakstype bør brukes som superklassen til SomeKindOfException?

  1. Exception
  2. RuntimeException
  3. IllegalArgumentException
  4. IllegalStateException

 

Svaret på det første spørsmålet er IllegalStateException, fordi metoden skal ikke kalles (uansett hva argumentet er) når runden er ferdig.

Svaret på de to neste IllegalArgumentException, fordi metoden det (primært) handler om validering av argumentet.

 

Oppgave d) - Kode i keepAndRoll (2 poeng)

Nederst i keepAndRoll-metoden er én eller flere kodelinjer erstattet med "... what code needs to be here ? ...". Skrive linjen(e) som mangler. Koden vil bli lest av sensor.

Her er poenget at vi må avslutte metoden med å kalle roll, og må finne ut om vi skal kaste de som er igjen (hvis det er noen terninger igjen) eller alle.

 

int dieCount2 = currentDice.getDieCount();
if (dieCount2 == 0) {
   dieCount2 = this.dieCount;
}
roll(dieCount2);

// Evt. bareint dieCount2 = currentDice.getDieCount();
roll(dieCount2 == 0 ? this.dieCount : dieCount2);

Vanlige feil/mangler:

  • Bruke metoder som ikke finnes, f.eks. Dice.size()
  • Kalle keepAndRoll (rekursivt)

 

Oppgave e) - FarkleRound-objektdiagram (6 poeng)

Underveis i og etter en Farkle-runde vil tilstanden til runden være representert ved hjelp av flere objekter, inkludert et FarkleRound-objekt. Tegn objektdiagram for tilstanden etter runden som er beskrevet i det tredje eksemplet (fremhevet med fet skrift) for Farkle-reglene. 

Bruk blyantikonet til høyre på verktøylinja for tekstfeltet, for å aktivere tegneverktøyet. Det kan være lurt å kladde på forhånd, siden tegneverktøyet er litt begrenset...

 

 

 #fr: FarkleRounddieCount = 5#dice1: DicedieValues = 5score = 50#dice2: DicedieValues = 1, 5score = 150keptkept

Vanlige feil/svakheter:


  • Bruke klassediagramnotasjon blandet med objektdiagramnotasjon
  • Blande inn metoder og evt. ha sekvenser av objekter som iobjekttilstandsdiagrammer
  • Bruke tekstrepresentasjonen til Dice inni FarkleRound-instansen, slik at de ikke blir med som egne objekter.
  • Ikke tegne på pilspisser eller sette navn på streker/linker
  • Tilstand som ikke tilsvarer den en får etter den angitt Farkle-runden, f.eks. currentDice ulik null, tre Dice-objekter i keep-lista, dieCount ulik 5, ...

 

 

 

 


Dice-klassen har en konstruktør som initialiserer den med tilfeldige terningverdier, og det gjør den vanskelig å teste, fordi en jo ikke vet hvilke verdier random-metoden genererer. Et alternativ er en konstruktør som henter verdier fra en Supplier<Integer>(se nedenfor). Til vanlig kan en bruke en Supplier-implementasjon som leverer tilfeldige tall vha. Math.random(), mens til testing kan en lage en som gir ut bestemte verdier:

/**
 * Initializes this Dice with n die values provided by the supplier argument.
 * @param dieCount the number of dice to "throw"
 * @param supplier provides the die values
 */
public Dice(int dieCount, Supplier<Integer> supplier) { ... }

Supplierer deklarert som følger:

public interface Supplier<T> {
    /**
     * Gets a result.
     * @return a result of the type T
     */
    T get();
}

 

Oppgave a) - Funksjonelt grensesnitt (3 poeng)

Er DiceScorer et funksjonelt grensesnitt? Forklar hvorfor/hvorfor ikke!

DiceScorer-grensesnittet er funksjonelt fordi det 1) har kun én abstrakt metode og 2) er ment å være primærtfunksjonen til klassen som implementerer den.

Oppgave b) - Alternativ Dice-konstruktør (3 poeng)

Implementer den alternative Dice-konstruktøren.

 

Her er koden nokså lik den i randomDieValues, vi bare bytter ut uttrykket med Math.random med supplier.get(). Her bryr vi oss heller ikke om poeng (score).

public Dice(int dieCount, Supplier<Integer> supplier) { 
   this.dieValues = new ArrayList<>(dieCount);
   while (this.dieValues.size() < dieCount) { // evt. for (int i = 0; i < dieCount; i++) {
      this.dieValues.add(supplier.get());
   }
}
 
Oppgave c) - Testing av Dice-klassen (5 poeng)

Skriv en eller flere test-metoder for Dice sin valueOf-metode. Du kan anta det finnes en metode kalt assertDieValuessom sjekker terningverdiene til en Dice:

assertDieValues(dice, 1, 2, 3) // utløser assert-unntak hvis dice ikke har terningverdiene 1, 2, 3 og bare disse.

Her bør det testes med og uten = score og i hvertfall for én feil syntaks.

@Test
public void testValueOf() {
   Dice dice1 = Dice.valueOf("[1, 1, 2]");
   assertDieValues(dice1, 1, 1, 2);
   Assert.assertEquals(-1, dice1.getScore());

   Dice dice2 = Dice.valueOf("[1, 1, 2] = 200");
   assertDieValues(dice2, 1, 1, 2);
   Assert.assertEquals(200, dice2.getScore());

   try {
      Dice.valueOf("1, 5, 2 = 200");
      Assert.fail();
   } catch (final Exception e) {
      Assert.assertTrue(e instanceof IllegalArgumentException);
   }
   try {
      Dice.valueOf("1, x, 2 = 200");
      Assert.fail();
   } catch (final Exception e) {
      Assert.assertTrue(e instanceof IllegalArgumentException);
   }
   try {
      Dice.valueOf("1, 5, 2 = x");
      Assert.fail();
   } catch (final Exception e) {
      Assert.assertTrue(e instanceof IllegalArgumentException);
   }
}

Vanlige feil/mangler:

  • Ikke teste valueOf i det hele tatt, men andre Dice-metoder!
  • Bare teste én av syntaksvariantene (med eller uten score-del)
  • Ikke teste ett eller flere tilfeller med feil syntaks.
  • Ikke sjekke både terningverdiene og poengverdien til Dice-resultatet.
  • Bruke fail() etter assertDieValues for å markere at den skal utløses unntak (altså at det er feil hvis det ikke utløses unntak).

 


Du skal lage en liten JavaFX-app for terningspill basert på Dice-klassen. I første omgang skal du støtte terningkast og poengberegning:

  • Brukeren skal kunne fylle inn et tall ni et tekstfelt (TextField)
  • Når en knapp (Button) trykkes, så skal det lages et Dice-objekt med ntilfeldige tall.
  • Poengverdien til Dice-objektet settes vha. metoden void computeFarkleScore(Dice), som du kan anta finnes.
  • Den tekstlige representasjonen til Dice-objektet skal så vises som en tekst (Label)

 

Følgende FXML er skrevet for appen:

<HBoxxmlns:fx="http://javafx.com/fxml/1" fx:controller="ord2018.farkle.fx.DiceController">
   <!--  text field for inputing the die count -->
   <TextField fx:id="dieCountInput" promptText="Die count"/>
   <!--  button for throwing the dice, i.e. create a Dice object and computing the score -->
   <Button onAction="#handleThrowDice" text="Throw dice"/>
   <!--  label for outputing the textual representation of the Dice object -->
   <Label fx:id="diceOutput" text="No dice thrown, yet"/>
</HBox>

Skriv en JavaFX-kontroller som implementerer ønsket oppførsel. Den skal passe til den oppgitte FXML-en og bruke klasser og metoder beskrevet tidligere i oppgavesettet.

Hvis det er detaljer du er usikker på, så forklar med tekstkommentarer i koden.

Her er vi mest opptatt av:

  • @FXML-annotasjonene
  • riktig type og navn for variablene og metoden
  • at en henter input fra dieCountInput og setter output med diceOutput

 

public class DiceController {
 
   @FXML
   private TextField dieCountInput;
 
   @FXML
   private Label diceOutput;
 
   @FXML
   public void handleThrowDice() {
      Dice dice = new Dice(Integer.valueOf(dieCountInput.getText()));
      computeFarkleScore(dice); // var jo oppgitt
      diceOutput.setText(dice.toString());
   }
}