Praeitą savaitę pabaigiau vieno mažo projektėlio, kurį kapsčiau prieš porą metų, perdarymą. Ir tai buvo Cardfight!! Vanguard žaidimo online versija.
Nuo pat pradžių buvo problema kaip padaryti serverį ir kaip palaikyti žaidimo statusą atmintyje esant shared hostingui, kuriame neina pasukti nei node serverio, nei javos. Sprendimas buvo: padaryk viską, ką gali, su userio requestu, ir lauk sekančio requesto. Userių notification’ams panaudojau pusher lib’ą, prie kurio prisijungia frontend aplikacija ir laukia pranešimų.
Kadangi nėra kaip gražiai susisteminti visų entities žaidime į db lenteles, nes labai daug kas gali keistis, plėstis, pasinaikinti – visi žaidimo entities yra paprasčiausi masyvų fasadai, kurie iš masyvo gražina reikšmes ir atgal į vidinį masyvą sudeda naujas reikšmes. Loadinant iš duomenų bazės viskas yra išpakuojama iš json string ir saugant yra atgal supakuojama į json string.
class Card
{
public function __construct(
array $data = null
) {
$this->data = $data ?? [];
}
public function setType(string $type = null): self
{
$this->data[self::TYPE] = $type;
return $this;
}
public function getType(): ?string
{
return $this->data[self::TYPE];
}
public function getId(): string
{
return $this->data[self::ID];
}
public function setId(string $id): self
{
$this->data[self::ID] = $id;
return $this;
}
Sekanti problema susijusi su pasirinkta struktūra buvo: kadangi game state objektas nepasikeitė, o pasikeitė kažkoks vidinis objektas, esantis kažkuris giliai child objektuose, doctrine nenorėjo saugoti pasikeitusio entity, nes doctrine požiūriu niekas nepasikeitė. Teko prieš saugojimą liepti visiems objektams save klonuoti, ir klonuoti visus child objektus.
Prieš žaidėjo input apdorojimą išsaugojus pradinę būseną, buvo galima sulyginti struktūrų skirtumus, ir juos papushinti visiems besiklausantiems klientams, kas pasikeitė žaidime, ir nusiųsti sekančią užklausą žaidėjui, iš kurio laukiamas input’as. Tokiu būdu programos veikimas yra:
žaidėjas pasako, kad nori žaisti -> sukuriam žaidimą, processinam būsena iki kol susiduriam su situacija, kai reikia kažką pasirinkti -> pranešam žaidėjui ko iš jo tikimės
žaidėjas atlieką veiksmą -> apdorojam duomenis, ir arba tesiam procesą iki kol vėl prisireikia žaidėjo input’o, arba pranešam atgal, kad blogas pasirinkimas ir vėl iš žaidėjo tikimės duomenų
Taip vykdome tol, kol kažkuris žaidėjas laimi. Jei nė vienas iš žaidėjų nemato pranešimo, kad iš jo kažko reikalaujama, vadinasi kažkas sugedo 🙁
Kadangi kiekviena korta turi savo abilities ir jos visos skirtingos, reikėjo būdo kažkaip visa tai sistematizuoti, kad neimplementinti kiekvienos kortos atskirai.. who’s got time for that?
Solution: More arrays! Yay!
{
"id": "V-TD01/002",
"name": "Stardrive Dragon",
"grade": 3,
"skill": "twin-drive",
"power": 13,
"critical": 1,
"clan": "Royal Paladin",
"image": "https://vignette.wikia.nocookie.net/cardfight/images/a/ad/V-TD01-002EN.png/revision/latest/scale-to-width/273",
"description": "[AUTO](RC):When it attacks a vanguard, if you have three or more rear-guards, this unit gets [Power]+5000 until end of that battle.",
"race": "Cosmo Dragon",
"gift": "force",
"abilities": [
{
"type": "auto",
"event": "before_attack",
"conditions": [
{
"type": "is_card_check",
"options": {
"card": "self",
"location": "rearguard"
}
},
{
"type": "is_card_check",
"options": {
"player": "opponent",
"card": "defender",
"location": "vanguard"
}
},
{
"type": "is_list_check",
"options": {
"zone": "field",
"location": "rearguard",
"min_count": 3
}
}
],
"effects": [
{
"type": "do_mark_card",
"options": {
"card": "self"
}
},
{
"type": "do_modify_buffer",
"options": {
"length": "battle",
"power": 5
}
},
{
"type": "do_clear_buffer"
}
]
}
]
},
Visi abilities žaidime yra 3 rūšių: activated, auto ir continuous. Tačiau iš principo visi jie veikia taip pat: patikrinam situaciją, sumokam kainą jei yra, ir atliekam kažkokius veiksmus. Ir dažniausiai tie tikrinimai ir veiksmai yra tokie patys, arba smarkiai panašūs, todėl sukūriau processorius, kurie apdoroja kažkokį kortos tikrinimą ar veiksmą ir tiesiog pagal parametrus šiek tiek keičiam logiką.
T.y. jei tikrinimo procesoriaus id yra “is_card_check”, privalomas optionas yra “card”, kuris nurodo kuri korta yra tikrinama, ir optional parametras yra “location”, kurio reikšmė yra “rearguard” – patikrinam ar šį abilitį turinti korta šiuo metu randasi rearguard circle.
Aišku, kai kurie abilities yra tokie bizzare, kad teko juos implementinti kaip visai atskirą logiką, nes generic sprendimas buvo arba per daug complicated, arba tiesiog nesugalvojau kaip gudriai išspręsti, ir atidėjau tai vėlesniam laikui.
Application frontendas buvo padarytas su react ir redux, bet čia nežinau ką galima gudraus papasakoti, nes frontendas šiek tiek simplistic logikos prasme, visą logiką permetant backendui.
Jei kam nors norėsis pažiūrėti kaip visa tai veikia realybėje, galit nueiti https://cfv.feral.lt ir prisijungti demo useriu. Prisijungus reikia pasirinkti “training”, jei norima pabandyti žaisti prieš botą, arba kitame tab’e prisijungti su demo2 useriu, jei norima pažaisti prieš save 🙂
Jei norėsis pasikapstyti po kodą, jį galima rasti https://gitlab.com/feral.drood/app-vanguard ir https://gitlab.com/feral.drood/app-vanguard-frontend