A3 - Tjugoett
Uppgiftsbeskrivning
Section titled “Uppgiftsbeskrivning”Projektet för denna uppgift finns inte i din grupp på GitLab än — du skapar det själv via ActionHub när du är redo att börja. Projektet baseras på en mall vars README innehåller den fullständiga uppgiftsbeskrivningen.
Mer information om hur du kommer igång med uppgiften hittar du under Examinationsuppgifter->Om->Kom igång.
Om inspelningen
Section titled “Om inspelningen”Inspelningen är från innan 2018, men publicerades på YouTube 2021. Mats och Johan Leitet spelar Tjugoett med en riktig kortlek vid ett bord och pratar under tiden om hur spelet kan “objektifieras”: vilka objekt som finns, vad de ska kunna göra och vem som ansvarar för vad. Johan är giv, Mats spelare. Två högar på bordet får föreställa draghögen och slänghögen.
Sedan inspelningen gjordes har uppgiften fått ett färdigt klassdiagram. Mycket av det Mats och Johan resonerar sig fram till syns direkt i det, och sammanfattningen pekar ut var.
En kortlek som blandar sig själv
Section titled “En kortlek som blandar sig själv”Kortleken är färsk från förpackningen och helt oblandad. I verkligheten tar given kortleken och blandar den. I Johans Harry Potter-värld räcker det att peka på kortleken, så blandar den sig själv.
Vilket sätt ska du välja när du programmerar? Det som är enklast att förstå och enklast att
programmera, säger Mats. Att avbilda verkligheten exakt blir ofta för komplext. En kortlek som kan
blanda sig själv gör det enklare i nästa steg: given behöver inte hålla reda på hur blandningen går
till. I klassdiagrammet är det just så: Deck har metoden shuffle().
På samma sätt får spelaren ett kort, snarare än tar det. Varifrån kortet kommer spelar ingen roll
för spelaren, den ska bara kunna ta emot det. I klassdiagrammet har Player ingen referens till
kortleken alls, bara metoden addToHand(). Det är spelbordet, CardTable, som med sin privata
metod deal() hämtar nästa kort från kortleken och lämnar det till en spelare.
En spelomgång
Section titled “En spelomgång”Mats och Johan spelar några rundor och går igenom reglerna på vägen:
- Alla spelare får först ett kort var. Given tar inget kort själv.
- Given spelar sedan mot en spelare i taget. Spelaren får ett kort till. Efter två kort kan spelaren redan ha vunnit (21) eller spruckit (över 21).
- Annars bestämmer spelaren om den vill ha fler kort eller är nöjd. En spelare måste alltid ta två kort, även med ett mycket försiktigt stoppvillkor som 8.
- Är spelaren nöjd drar given kort tills den är nöjd. Given ser inte spelarens hand, och spelaren behöver inte se givens kort.
- Den högsta summan vinner. Efter delomgången lägger både spelare och giv sina kort i slänghögen.
Har spelaren vunnit eller spruckit direkt behöver given inte dra några kort alls i den delomgången.
Ingen människa styr spelet i applikationen. Varken spelare eller giv ser motståndarens hand, så båda behöver en inbyggd regel för när de är nöjda. Johan väljer att given stannar på 17. En våghalsig giv kan stanna på 18, en fegare på 16 eller till och med 15. Spelarna kan ha andra gränser än given.
Klassdiagrammet löser det här enkelt: given är också ett Player-objekt, precis som spelarna. Både
spelare och giv har alltså ett eget stoppvillkor, och den härledda egenskapen canHit svarar på om
de vill ha ett kort till. Den bygger bara på den egna handen och det egna stoppvillkoret, aldrig på
motståndarens hand.
Det är inte Black Jack
Section titled “Det är inte Black Jack”Två saker skiljer Tjugoett från Black Jack, och båda får Mats och Johan att räkna fel under inspelningen:
- Klädda kort är inte värda 10. En knekt är värd 11, en dam 12 och en kung 13. Johan drar en dam och en sexa, och det blir 18, inte 16.
- Given spelar mot en spelare i taget, inte mot alla på en gång. Med flera spelare vid bordet
spelar given en hel delomgång mot den första spelaren, lägger sina kort i slänghögen och spelar
sedan en ny delomgång mot nästa spelare. I klassdiagrammet motsvaras en sådan delomgång av
CardTables privata metodplayOut(dealer, player), och jämförelsen efteråt avdetermineWinner(dealer, player). Båda tar emot given och en spelare.
Esset: 1 eller 14
Section titled “Esset: 1 eller 14”Esset är värt 1 eller 14, det som är mest fördelaktigt för handen. Mats får först ett ess och sedan en åtta. 14 + 8 blir 22, och då har han spruckit. Räknas esset i stället som 1 blir summan 9, och han kan fortsätta dra.
Esset “byter” inte värde en gång för alla. Du skriver inte 1 eller 14 på kortet, som Mats uttrycker
det. Värdet räknas om varje gång handen värderas, utifrån de kort som just då finns på handen. I
startkoden har esset värdet 1 i PlayingCard, och kortet går inte att ändra eftersom objektet är
fryst. Att esset kan räknas som 14 är alltså handens sak, det vill säga Players valueOf(), inte
kortets.
Fem kort under 21
Section titled “Fem kort under 21”En specialregel: får spelaren fem kort på handen och summan fortfarande är under 21 vinner
spelaren direkt, en naturlig vinst. Det är därför handen i klassdiagrammet har multipliciteten
0..5: fler än fem kort kan en hand aldrig ha. Om spelaren vunnit direkt, med 21 eller fem kort,
svarar den härledda egenskapen isNaturalWinner.
När draghögen tar slut
Section titled “När draghögen tar slut”Under inspelningen tar draghögen slut. Det understa kortet delas aldrig ut, eftersom någon kan ha
sett det när högen lades ner. När det bara finns ett kort kvar i draghögen tar given i stället det
kortet och alla kort i slänghögen, blandar om dem och använder dem som ny draghög. I
klassdiagrammet görs det med Decks metoder add(), som tar emot en array med kort, och
shuffle(). Slänghögen har multipliciteten 0..51: eftersom det understa kortet aldrig delas ut
kan aldrig alla 52 kort hamna där.
Det viktiga, betonar Mats, är att du inte tar fram en ny kortlek när draghögen är slut. Då finns plötsligt 52 kort till i spel, och kort som redan ligger på bordet finns i dubbletter. Det är också ett uttryckligt krav i issue #8: under en spelomgång används en kortlek.
Johan påpekar att korten är objekt, alltså referenstyper. Även om det är lätt att kopiera kort i en
dator ska du tänka på att flytta korten: från draghögen till en hand och vidare till slänghögen.
Ett kort som hamnar i slänghögen finns inte längre i draghögen. I klassdiagrammet lämnar Decks
deal() ifrån sig ett kort, och Players discardHand() tömmer handen och returnerar korten så
att spelbordet kan lägga dem i slänghögen.
Ansvar och objekt
Section titled “Ansvar och objekt”Sist pratar Mats och Johan om vilka objekt som behövs och vad de ansvarar för. Deras resonemang känns igen i klassdiagrammet:
| I inspelningen | I klassdiagrammet |
|---|---|
| Spelbordet samlar given, spelarna, draghögen och slänghögen. | CardTable med fälten dealer, players, deck och discardPile |
app.js skapar ett spelbord med önskat antal spelare. |
new CardTable(numberOfPlayers), sedan playRounds(numberOfRounds) |
| Kortleken kan blanda sig själv. | Deck.shuffle() |
| Kortleken kan tala om hur många kort den innehåller. | Deck.count |
| Man kan be kortleken om ett kort. | Deck.deal() |
| Spelaren, inte kortleken, räknar ut summan av korten. | Player.valueOf() |
Johan ger ett test för om ansvaret hamnat rätt: går kortleken att använda i ett annat kortspel, till
exempel Black Jack eller poker? Då har du antagligen tänkt rätt. Fungerar den bara i Tjugoett har du
troligen lagt för mycket “intelligens” i kortleken eller korten. Deck i klassdiagrammet klarar
testet: inget i den vet något om Tjugoett. Kortleken räknar inte, och korten har ju dessutom redan
lämnat kortleken när summan räknas.
Om inspelningen
Section titled “Om inspelningen”Inspelningen publicerades på YouTube 2022. Johan Leitet och Mats går igenom klassdiagrammet i
examinationsuppgift A3: hur det ska läsas, hur startkoden i Deck hänger ihop med det, och hur du
kan ta dig an uppgiften steg för steg. Johan implementerar också metoden deal() live. Sist tittar
de kort på den frivilliga, mer utmanande varianten av uppgiften.
Klassdiagrammen har ändrats en del sedan inspelningen gjordes, men strukturen är densamma. Sammanfattningen utgår från hur diagrammen ser ut idag.
Fyra klasser, fyra moduler
Section titled “Fyra klasser, fyra moduler”Det grundläggande klassdiagrammet innehåller fyra klasser: CardTable, Deck, Player och
PlayingCard. Varje klass ligger i en egen modul, alltså minst fyra JavaScript-filer, eftersom en
modul bara får innehålla en klass (issue #7).
En pil från Deck till PlayingCard betyder att Deck använder PlayingCard. I praktiken behöver
modulen Deck.js importera klassen PlayingCard. Det syns redan i startkoden:
import { PlayingCard } from './PlayingCard.js'Beroendediagrammet i uppgiftens README visar samma sak på modulnivå, för den färdiga lösningen:
app.js är bara beroende av CardTable.js, som i sin tur är beroende av Deck.js, Player.js och
PlayingCard.js. Det betyder inte att app.js måste se ut så från början. Medan du bygger kan du
mycket väl importera Deck eller PlayingCard direkt i app.js för att testa dem.
Så läser du en klass i diagrammet
Section titled “Så läser du en klass i diagrammet”En klass ritas som en ruta med tre fack:
- Överst klassens namn.
- I mitten klassens attribut, oavsett om de är fält eller egenskaper med en getter.
- Underst klassens operationer, det vill säga metoderna.
För Deck betyder det bland annat ett privat fält playingCards, en array med referenser till
PlayingCard-objekt, och en egenskap count som returnerar ett tal. För metoderna anges
parametrarnas namn och typer, och returtypen om metoden returnerar något. deal() returnerar till
exempel ett PlayingCard.
Mats tipsar om att inte bara titta på det stora diagrammet. Under varje klass i README:n finns ett diagram för just den klassen och en text som beskriver fälten, egenskaperna och metoderna. Där finns detaljerna.
Startkoden i Deck
Section titled “Startkoden i Deck”Deck är delvis implementerad. Konstruktorn sätter #playingCards till en tom array och fyller den
sedan med 52 kort, med två nästlade loopar över de statiska fälten PlayingCard.suits och
PlayingCard.ranks. Eftersom fälten är statiska går de att använda utan att något
PlayingCard-objekt har skapats.
shuffle() blandar korten med Fisher-Yates-algoritmen: varje varv byter två element i arrayen plats
slumpvis. Semikolonet i början av raden ;[a, b] = [b, a] är där eftersom raden börjar med en
hakparentes. Utan semikolon kan JavaScript tolka raden som en fortsättning på raden före.
Johan påpekar att arrayen är privat av en anledning. Ingen ska kunna stoppa in fingrarna i kortleken och dra vilket kort som helst. Korten hämtas på ett kontrollerat sätt, genom kortlekens metoder.
app.js innehåller exempelkod som skapar en kortlek, skriver ut den, blandar den och skriver ut den
igen. Den koden tar du bort när du har studerat den.
Johan implementerar deal()
Section titled “Johan implementerar deal()”Innan Johan skriver någon kod läser han vad deal() ska göra: ta bort ett kort ur kortleken och
returnera det. Men från vilken ände av arrayen? Det beror på var add() lägger till kort, och det
måste du bestämma själv. Johan tänker sig kortleken liggande på bordet: index 0 är det understa
kortet och det sista elementet är det översta. Han skriver ner beslutet innan han kodar, för att
vara konsekvent.
På MDN hittar Johan metoden pop(), som tar bort det sista elementet i en array och returnerar det.
Det är precis vad han behöver:
/** * Removes the top playing card from the deck and returns it. * * @returns {PlayingCard} The top playing card of the deck. */deal() { return this.#playingCards.pop()}Sedan testar han direkt i app.js: blandar kortleken, skriver ut den, delar ut ett kort och skriver
ut både kortleken och kortet. Det sista kortet i kortleken är borta, och det är det kortet han fick.
Johan påpekar också att det sista kortet i draghögen aldrig får delas ut, så deal() räcker inte
ensam för spelets regler. Var den logiken ska ligga beror på var du lägger den. Deck känner dock
inte till slänghögen, och kan alltså inte blanda om den. Det är spelbordet som har både kortleken
och slänghögen, och CardTable har en egen privat metod deal() som returnerar nästa kort från
kortleken.
Efter det återstår bara count och add() i Deck. Mats poängterar att det sämsta du kan göra är
att skriva allt färdigt och testa sist. Ju mindre kod du skriver mellan varje testkörning, desto
lättare är det att komma framåt.
I vilken ordning du bygger
Section titled “I vilken ordning du bygger”Ett bra sätt är att arbeta nerifrån och upp efter beroendena. CardTable är beroende av både Deck
och Player, och styr hur de samverkar. Därför är det naturligt att göra klart Deck och sedan ge
dig på Player, innan du börjar med CardTable. Ordningen klasserna presenteras i README:n är bara
bokstavsordning, inte en rekommenderad arbetsordning.
Player.js finns inte från början. Skapa filen, skriv klassdeklarationen och dokumentera den. Börja
sedan litet och testa ofta.
Johan förklarar också tanken med det färdiga klassdiagrammet: du ska inte behöva lägga tid på den övergripande designen, utan kan fokusera på hur varje metod ska fungera. Dokumentera metoden innan du kodar den, så att du vet vad den ska göra. Det var därför han redde ut vad som är överst och underst i kortleken först. Hade han börjat koda utan att bestämma sig, hade han lätt kunnat blanda ihop två olika synsätt.
Pilarna och multipliciteten
Section titled “Pilarna och multipliciteten”Pilarna är det som oftast väcker frågor. Johan använder ordet aggregering, men Mats väljer att kalla dem associationer för att inte behöva gå in på olika sorters associationer.
Namnet vid pilspetsen, till exempel -hand mellan Player och PlayingCard, är ett fält i klassen
pilen utgår från. Därför finns hand inte med i mitten av rutan för Player i det stora diagrammet,
bara nickname och standLimit. Fältet syns i stället på pilen. I diagrammet för enbart Player
står det däremot -hand : PlayingCard[] i rutan.
Talen vid pilarna är multiplicitet:
0..5vid-handbetyder att en spelares hand innehåller mellan noll och fem kort. Fler än fem kort kan en hand aldrig ha.1..52vid-playingCardsbetyder att kortleken innehåller mellan ett och 52 kort. Den blir aldrig tom, eftersom det sista kortet aldrig delas ut innan slänghögen blandas om.
Den utmanande varianten
Section titled “Den utmanande varianten”Längst ner i README:n finns ett frivilligt, mer utmanande klassdiagram. Det har två nya klasser,
PlayingCardCollection och Hand, fler beroenden och en ny sorts pil.
Tanken är att du först bygger den grundläggande varianten och gör den helt färdig. Har du tid och vill utmana dig kan du sedan gå vidare. Mats råd: spara undan den färdiga grundläggande lösningen först, så att du inte förstör den. Visar det sig att den utmanande varianten tar för lång tid kan du fortfarande redovisa den grundläggande.
Är den utmanande varianten bättre, eller bara svårare? Den är mer generell, säger Mats, och därför
mer avancerad. PlayingCardCollection är en samling som är expert på att hantera spelkort, och den
används både som kortlekens innehåll i Deck och som slänghög i CardTable. Dessutom kommer arv
in naturligt: Hand ärver från PlayingCardCollection och är en specialiserad sorts samling. I UML
ritas arv med en ofylld triangel som pekar mot basklassen. Ju mer generell lösningen är, desto mer
går att behålla om du vill bygga ett annat kortspel än Tjugoett.
Några skillnader mot den grundläggande varianten som är värda att lägga märke till:
Deckhar ingen egen array längre, utan ett privat fältplayingCardCollection.DeckochPlayingCardCollectionharadd(...playingCards : PlayingCard), en restparameter, i stället föradd(playingCards : PlayingCard[]).- Flera klasser har en
copy()som ska returnera en djup kopia. Var noga med hur du hanterar referenser i parametrar och returvärden, så att du undviker “privacy leaks”. PlayingCardCollectionhar en egen iterator,*[Symbol.iterator](), så att en samling går att använda medfor...ofoch spread-syntax.CardTable.playRounds()returnerar resultaten som en array med objekt, och utskrifter får bara göras iapp.js.
Om du väljer den utmanande varianten ska du också reflektera över privacy leaks i REFLECTION.md
(issue #12).
Testa hela tiden
Section titled “Testa hela tiden”Johan avslutar med att uppmana dig att inte vara rädd för att skriva egen testkod i app.js. Tänk
efter vad resultatet borde bli innan du kör programmet, och kontrollera sedan att det blev så. Blev
det inte som du trodde, fundera på varför. Att förutsäga vad koden gör innan du kör den är en bra
vana att träna på.
- Deadline 2026-11-03 17.00, muntlig examination 2026-11-05 och 2026-11-06 (du bokar själv tid på Moodle).
- Deadline 2026-11-17 17.00, muntlig examination 2026-11-19 (omexamination, du bokar själv tid på Moodle).
- Deadline 2027-06-14 17.00, muntlig examination 2027-06-15 (omexamination, du tilldelas en tid via Zulip).
Betygssättning
Section titled “Betygssättning”Denna uppgift betygssätts med U/G/VG. Betyget är en sammanvägning av genomförande av uppgiften samt prestationen vid det muntliga förhöret.
Muntligt förhör
Section titled “Muntligt förhör”Examination A3 (provmoment 2003) är en muntlig examination — läs och se hur den kan gå till. Du bokar själv tid för den muntliga examinationen på kursens Moodle-webbplats före uppgiftens deadline.
Det muntliga förhöret berör frågor kopplat till delkurs A, det vill säga från och med kursvecka 1 till och med kursvecka 7.
Inlämning av uppgiften
Section titled “Inlämning av uppgiften”Examination A3 (provmoment 2003) lämnas in genom att skapa en Merge Request — se sidan för fullständiga steg-för-steg-instruktioner.
Rapportering av resultat
Section titled “Rapportering av resultat”Detta är ett examinerande, poängsatt moment i kursen. Examinationens resultat rapporteras in i Ladok senast 15 arbetsdagar efter den sista dagen för muntlig examination.