Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Expand
titleDel 1 - Dice-klassen
Expand
titleInnledning til del 1

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.

Code Block
/**
* 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.

Expand
titleLF
Code Block
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".

Expand
titleLF
Code Block
@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++) {i < dieValueStrings.length; i++) {
      dieValues.add(Integer.valueOf(dieValueStrings[i].trim()));
   }
   return new  dieValues.add(Integer.valueOf(dieValueStrings[i].trim()));
   }
   return new Dice(dieValues, score);
}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 getDieCountgetDieValueog getValueCount iht. API-beskrivelsen.

Expand
titleLF
Code Block
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.

Expand
titleLF
Code Block
public int getScore() {
   return score;
}

public void setScore(final 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.

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

Kan da skrive

Code Block
Dice dice = ...
for (int dieValue : dice) {
   ... her er dieValue neste verdi i dice sin dieValues-liste
}
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.

Expand
titleLF

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.

Code Block
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) {
      dieValues.remove(dieValue);
      // evt.
      // int pos = dieValues.indexOf(dieValue);
      // if (pos >= 0) {
      //    dieValues.remove(pos);
      // }
   }
   return new Dice(dieValues, -1);
}

...