Hvordan man designer en database, der er fleksibel nok til at rumme flere meget forskellige kortspil.
For nylig viste vi, hvordan en database kunne bruges til at gemme brætspilsresultater. Brætspil er sjove, men de er ikke den eneste onlineversion af klassiske spil, der kører. Kortspil er også meget populære. De introducerer et element af held i gameplayet, og der er meget mere end held involveret i et godt kortspil!
I denne artikel vil vi fokusere på at opbygge en datamodel til at gemme spilkampe, resultater, spillere og scoringer. Den største udfordring her er at gemme data relateret til mange forskellige kortspil. Vi kunne også overveje at analysere disse data for at bestemme vinderstrategier, forbedre vores egne spillefærdigheder eller opbygge en bedre AI-modstander.
De fire kortspil, vi vil bruge i vores database
Fordi spillere ikke kan kontrollere den hånd, de får uddelt, kombinerer kortspil strategi, færdigheder og held. Den held-faktor giver en nybegynder chancen for at slå en erfaren spiller, og det gør kortspil vanedannende. (Dette adskiller sig fra spil som skak, der er stærkt afhængige af logik og strategi. Jeg har hørt fra mange spillere, at de ikke er interesserede i at spille skak, fordi de ikke kan finde modstandere på deres færdighedsniveau.)
Vi vil fokusere på fire velkendte kortspil:poker, blackjack, belot (eller belote) og préférence. Hver af dem har relativt komplekse regler og kræver lidt tid at mestre. Forholdet mellem held og viden er også forskelligt for hvert spil.
Vi tager et hurtigt kig på de forenklede regler og detaljer for alle fire spil nedenfor. Spilbeskrivelser er temmelig sparsomme, men vi har inkluderet nok til at vise de forskellige spiltilstande og de forskellige regler, vi vil støde på under databasedesignprocessen.
Blackjack:
- Dæk: Et til otte spil med hver 52 kort; ingen joker-kort
- Spillere: Dealer og 1 eller flere modstandere
- Anvendt enhed: Normalt penge
- Grundlæggende regler: Spillere får 2 kort, som kun de kan se; dealeren får to kort, det ene med billedsiden opad og det andet med billedsiden nedad; hver spiller beslutter sig for at trække flere kort (eller ej); forhandleren trækker sidst. Kort har tildelt pointværdier fra 1 til 11.
- Mulige spillerhandlinger: Hit, Stand, Split, Surrender
- Mål og sejrstilstand: Summen af en spillers kort er større end dealerens; hvis en spiller går over 21, taber den spiller.
Poker (Texas Hold'Em):
- Dæk: Standard (også kendt som fransk farve) 52-korts kortspil; ingen joker-kort. Kort er oftest røde og sorte i farven.
- Spillere: To til ni; spillere skiftes til at dele
- Anvendt enhed:Normalt chips
- Grundlæggende regler: Hver spiller starter med at få tildelt to kort; spillere placerer deres indsatser; tre kort uddeles med billedsiden opad i midten af bordet; spillere placerer igen deres indsatser; et fjerde kort placeres i midten, og spillerne satser igen; derefter placeres det femte og sidste kort, og den sidste indsatsrunde er afsluttet.
- Mulige spillerhandlinger: Fold, Call, Raise, Small Blind, Big Blind, Genraise
- Mål: Kombiner den bedst mulige hånd af fem kort (fra de to kort i spillerens hånd og de fem kort i midten af bordet)
- Sejrsbetingelse:Normalt for at vinde alle jetonerne på bordet
Belot (kroatisk variant af Belote):
- Dæk: Normalt det traditionelle tyske eller ungarske 32-korts kortspil; ingen joker-kort
- Spillere: To til fire; normalt fire spillere i par af to
- Anvendt enhed: Point
- Grundlæggende regler: For et spil med fire spillere får hver spiller seks kort på hånden og to kort med billedsiden nedad; spillere byder først på trumffarve; efter at trumf er bestemt, tager de de to kort med billedsiden nedad og lægger dem i deres hånd; en erklæringsrunde følger, hvorunder visse kortkombinationer annonceres for yderligere point; spillet fortsætter, indtil alle kort er brugt.
- Mulige spillerhandlinger: Beståelse, budfarve, erklæring, kastekort
- Mål for hånden: At vinde mere end halvdelen af pointene
- Sejrstilstand: Vær det første hold, der scorer 1001 point eller mere
Préférence:
- Dæk: Oftest et traditionelt tysk eller ungarsk spil med 32 kort; ingen joker-kort
- Spillere: Tre
- Enheder: Point
- Grundlæggende regler: Alle spillere får 10 kort; to "kitty" eller "talon" kort placeres midt på bordet; spillere bestemmer, om de vil byde på en kulør; spillere beslutter sig for at spille eller ej.
- Mulige spillerhandlinger: Pass, Byd Farbe, Spil, Spil ikke, Kast kort
- Mål: Afhænger af den variant af Préférence, der spilles; i standardversionen skal budgiveren vinde i alt seks stik.
- Sejrstilstand: Når summen af alle tre spilleres score er 0, vinder spilleren med det laveste antal point.
Hvorfor kombinere databaser og kortspil?
Vores mål her er at designe en databasemodel, der kan gemme alle relevante data for disse fire kortspil. Databasen kunne bruges af en webapplikation som et sted til at gemme alle relevante data. Vi ønsker at gemme indledende spilindstillinger, spildeltagere, handlinger udført under spillet og resultatet af en enkelt handel, hånd eller trick. Vi skal også huske på, at en kamp kan have en eller flere aftaler forbundet med sig.
Ud fra det, vi gemmer i vores database, burde vi være i stand til at genskabe alle de handlinger, der fandt sted under spillet. Vi bruger tekstfelter til at beskrive sejrsbetingelser, spilhandlinger og deres resultater. Disse er specifikke for hvert spil, og webapplikationslogikken vil fortolke teksten og transformere dem efter behov.
En hurtig introduktion til modellen
Denne model gør det muligt for os at gemme alle relevante spildata, herunder:
- Spilegenskaber
- Liste over spil og kampe
- Deltagere
- Handlinger i spillet
Da spil adskiller sig på mange måder, vil vi ofte bruge varchar(256) datatype til at beskrive egenskaber, bevægelser og resultater.
Spillere, kampe og deltagere
Denne del af modellen består af tre borde og bruges til at gemme data om registrerede spillere, de spillede kampe og de spillere, der deltog.
player
tabellen gemmer data om registrerede spillere. username
og email
attributter er unikke værdier. nick_name
attribut gemmer spillernes skærmnavne.
match
tabel indeholder alle relevante matchdata. Generelt er en kamp sammensat af en eller flere kortuddelinger (også kendt som runder, hænder eller tricks). Alle kampe har fastlagte regler før spillet begynder. Attributterne er som følger:
game_id
– henviser til tabellen, der indeholder listen over spil (poker, blackjack, belot og préférence, i dette tilfælde).start_time
ogend_time
er de faktiske tidspunkter, hvor en kamp starter og slutter. Bemærk, atend_time
kan være NULL; vi vil ikke have dens værdi, før spillet slutter. Hvis en kamp afbrydes, før den er afsluttet, erend_time
værdien kan forblive NULL.number_of_players
– er antallet af deltagere, der kræves for at starte spilletdeck_id
– refererer til det spil, der bruges i spillet.decks_used
– er antallet af dæk, der bruges til at spille spillet. Normalt vil denne værdi være 1, men nogle spil bruger flere dæk.unit_id
– er den enhed (point, jetoner, penge osv.), der bruges til at score spillet.entrance_fee
– er antallet af nødvendige enheder for at deltage i spillet; dette kan være NULL, hvis spillet ikke kræver, at hver spiller starter med et bestemt antal enheder.victory_conditions
– bestemmer, hvilken spiller der vandt kampen. Vi bruger varchar datatype for at beskrive hvert spils sejrstilstand (dvs. det første hold, der når 100 point), og lad applikationen fortolke den. Denne fleksibilitet giver plads til, at masser af spil kan tilføjes.match_result
– gemmer resultatet af kampen i tekstformat. Som medvictory_conditions
, lader vi applikationen fortolke værdien. Denne attribut kan være NULL, fordi vi udfylder denne værdi samtidig med, at vi indsætterend_time
værdi.
participant
tabel gemmer data om alle deltagerne i en kamp. match_id
og player_id
attributter er referencer til match
og player
tabeller. Tilsammen danner disse værdier tabellens alternative nøgle.
De fleste af spillene roterer, hvilken spiller der byder eller spiller først. Normalt i første runde bestemmes den spiller, der spiller først (åbningsspilleren), af spillereglerne. I næste runde vil spilleren til venstre (eller nogle gange til højre) for den oprindelige åbningsspiller gå først. Vi bruger initial_player_order
attribut for at gemme ordensnummeret for første rundes åbningsspiller. match_id
og initial_player_order
attributter danner en anden alternativ nøgle, fordi to spillere ikke kan spille på samme tid.
score
egenskaben opdateres, når en spiller afslutter en kamp. Nogle gange vil dette være på samme tidspunkt for alle spillere (f.eks. i belot eller préférence) og nogle gange mens kampen stadig er i gang (f.eks. poker eller blackjack).
Handlinger og handlingstyper
Når vi tænker på handlinger, som spillere kan foretage i et kortspil, indser vi, at vi skal gemme:
- Hvad handlingen var
- Hvem udførte den handling
- Hvornår (i hvilken aftale) handlingen fandt sted
- Hvilke kort(er) blev brugt i den handling
action_type
table er en simpel ordbog, der indeholder navnene på spillerens handlinger. Nogle mulige værdier inkluderer trække kort, spille kort, give kort til en anden spiller, check og raise.
I action
tabel, gemmer vi alle de begivenheder, der skete under en aftale. deal_id
, card_id
, participant_id
og action_type_id
er referencer til tabellerne, der indeholder værdier for deal, kortdeltager og action_type. Bemærk, at participant_id
og card_id
kan være NULL-værdier. Dette skyldes det faktum, at nogle handlinger ikke udføres af spillere (f.eks. trækker dealeren et kort og lægger det med billedsiden opad), mens nogle ikke inkluderer kort (f.eks. et raise i poker). Vi skal gemme alle disse handlinger for at kunne genskabe hele kampen.
action_order
attribut gemmer ordenstallet for en handling i spillet. For eksempel vil et åbningsbud modtage en 1-værdi; det næste bud ville have en 2-værdi osv. Der kan ikke ske mere end én handling på samme tid. Derfor er deal_id
og action_order
attributter danner sammen den alternative nøgle.
action_notation
attribut indeholder en detaljeret beskrivelse af en handling. I poker kan vi for eksempel gemme en forhøjelse handling og et vilkårligt beløb. Nogle handlinger kan være mere komplicerede, så det er klogt at gemme disse værdier som tekst og lade programmet fortolke det.
Tilbud og aftaleordre
En kamp er sammensat af en eller flere kortuddelinger. Vi har allerede diskuteret participant
og match
tabeller, men vi har inkluderet dem i billedet for at vise deres relation til deal
og deal_order
tabeller.
deal
tabel gemmer alle de data, vi har brug for om en enkelt match-instans.
match_id
attribut relaterer den instans til det relevante match, mens start_time
og end_time
angiv det nøjagtige tidspunkt, hvor den instans startede, og hvornår den var færdig.
move_time_limit
og deal_result
attributter er både tekstfelter, der bruges til at gemme tidsgrænser (hvis relevant) og en beskrivelse af resultatet af den pågældende aftale.
I participant
tabellen, initial_player_order
attribut gemmer spillerrækkefølgen for åbningskampen. Lagring af ordrer til efterfølgende ture kræver et helt nyt bord – deal_order
tabel.
Det er klart, deal_id
og participant_id
er referencer til en kampinstans og en deltager. Sammen danner de den første alternative nøgle i deal_order
bord. player_order
attribut indeholder værdier, der angiver de ordrer, spillere deltog i den pågældende kampinstans. Sammen med deal_id
, den danner den anden alternative nøgle i denne tabel. deal_result
attribut er et tekstfelt, der beskriver resultatet af kampen for en individuel spiller. score
attribut gemmer en numerisk værdi relateret til aftaleresultatet.
Suits, Rangs and Cards
Denne sektion af modellen beskriver de kort, vi vil bruge i alle de understøttede spil. Hvert kort har en kulør og rang.
suit_type
table er en ordbog, der indeholder alle de kulørtyper, vi vil bruge. For suit_type_name
, vil vi bruge værdier som "franske jakkesæt", "tyske jakkesæt", "schweizisk-tyske jakkesæt" og "latinske jakkesæt".
suit
tabellen indeholder navnene på alle de dragter, der er indeholdt af specifikke dæktyper. For eksempel har det franske dæk dragter kaldet "Spades", "Hearts", "Diamonds" og "Clubs".
I rank
ordbog, finder vi velkendte kortværdier som "Es", "Konge", "Dronning" og "Knægt".
card
tabellen indeholder en liste over alle mulige kort. Hvert kort vises kun én gang i denne tabel. Det er grunden til, at suit_id
og rank_id
attributter danner denne tabels alternative nøgle. Begge attributters værdier kan være NULL, fordi nogle kort ikke har en kulør eller en rang (f.eks. joker-kort). is_joker_card
er en selvforklarende boolsk værdi. card_name
attribut beskriver et kort med tekst:"Spades es".
Kort og kortspil
Kortene hører til dæk. Fordi et kort kan optræde i flere bunker, skal vi bruge et n:n forholdet mellem card
og deck
tabeller.
I deck
bordet, gemmer vi navnene på alle de kortspil, vi vil bruge. Et eksempel på værdier gemt i deck_name
egenskaber er:"Standard 52-korts kort (fransk)" eller "32-korts kortspil (tysk)".
card_in_deck
relation bruges til at tildele kort til passende bunker. card_id
– deck_id
par er den alternative nøgle til deck
bord.
Match-egenskaber, dæk og brugte enheder
Denne sektion af modellen indeholder nogle grundlæggende parametre for at starte et nyt spil.
Hoveddelen af denne sektion er game
bord. Denne tabel gemmer data om applikationsunderstøttede spil. game_name
attribut indeholder værdier som "poker", "blackjack", "belot" og "préférence".
min_number_of_players
og max_number_of_players
er det minimale og maksimale antal deltagere i en kamp. Disse egenskaber tjener som grænser for spillet, og de vises på skærmen i starten af en kamp. Den person, der starter kampen, skal vælge en værdi fra dette interval.
min_entrance_fee
og max_entrance_fee
attributter angiver adgangsgebyrintervallet. Igen er dette baseret på det spil, der spilles.
I possible_victory_condition
, vi gemmer alle de sejrsbetingelser, der kunne tildeles en kamp. Værdier er adskilt af en afgrænsning.
unit
ordbogen bruges til at gemme hver enhed, der bruges i alle vores spil. unit_name
attribut vil indeholde værdier som "point", "dollar", "euro" og "chip".
game_deck
og game_unit
tabeller bruger den samme logik. De indeholder lister over alle dæk og enheder, der kan bruges i en kamp. Derfor er game_id
– deck_id
parret og game_id
– unit_id
par danner alternative nøgler i deres respektive tabeller.
Score
I vores applikation vil vi gerne gemme scoringerne for alle spillere, der deltog i vores kortspil. For hvert spil beregnes og gemmes en enkelt numerisk værdi. (Beregningen er baseret på spillerens resultater i alle spil af en enkelt type.) Denne spillerscore svarer til en rang; det lader brugerne vide, hvor god en spiller er.
Tilbage til beregningsprocessen. Vi opretter en n:n forholdet mellem player
og game
tabeller. Det er player_score
bord i vores model. player_id
og score_id
” danner sammen tabellens alternative nøgle. "score
attribut bruges til at gemme den tidligere nævnte numeriske værdi.
Der er mange forskellige kortspil, der bruger meget forskellige regler, kort og dæk. For at skabe en database, der gemmer data for mere end ét kortspil, skal vi lave nogle generaliseringer. En måde vi gør dette på er ved at bruge beskrivende tekstfelter og lade applikationen interpetere dem. Vi kunne finde på måder at dække de mest almindelige situationer på, men det ville eksponentielt komplicere databasedesignet.
Som denne artikel har vist, kan du bruge én database til mange spil. Hvorfor ville du gøre dette? Tre grunde:1) du kan genbruge den samme database; 2) det ville forenkle analyser; og dette ville føre til 3) opbygningen af bedre AI-modstandere.