Desain game berbasis giliran di mana aksi memiliki efek samping

19

Saya menulis versi komputer dari permainan Dominion . Ini adalah permainan kartu berbasis giliran di mana kartu aksi, kartu harta, dan kartu poin kemenangan diakumulasikan ke dalam dek pribadi pemain. Saya memiliki struktur kelas yang cukup berkembang, dan saya mulai mendesain logika permainan. Saya menggunakan python, dan saya dapat menambahkan GUI sederhana dengan pygame nanti.

Urutan giliran pemain diatur oleh mesin negara yang sangat sederhana. Bergantian berlalu searah jarum jam, dan seorang pemain tidak bisa keluar dari permainan sebelum game berakhir. Permainan satu belokan juga merupakan mesin negara; secara umum, pemain melewati "fase aksi", "fase beli", dan "fase pembersihan" (dalam urutan itu). Berdasarkan jawaban atas pertanyaan Bagaimana menerapkan mesin game berbasis giliran? , mesin negara adalah teknik standar untuk situasi ini.

Masalah saya adalah bahwa selama fase aksi pemain, dia dapat menggunakan kartu aksi yang memiliki efek samping, baik pada dirinya sendiri, atau pada satu atau lebih pemain lain. Misalnya, satu kartu aksi memungkinkan pemain untuk mengambil belokan kedua segera setelah kesimpulan dari belokan saat ini. Kartu aksi lain menyebabkan semua pemain lain membuang dua kartu dari tangan mereka. Namun kartu aksi lain tidak melakukan apa-apa untuk giliran saat ini, tetapi memungkinkan pemain untuk menggambar kartu tambahan pada giliran berikutnya. Untuk membuat segalanya lebih rumit, sering ada ekspansi baru ke permainan yang menambahkan kartu baru. Sepertinya saya bahwa pengkodean keras hasil setiap kartu aksi ke dalam mesin keadaan gim akan jelek dan tidak dapat diadaptasi. Jawaban untuk Loop Strategi Berbasis Turn tidak masuk ke tingkat detail yang membahas desain untuk menyelesaikan masalah ini.

Apa jenis model pemrograman yang harus saya gunakan untuk mencakup fakta bahwa pola umum untuk bergiliran dapat dimodifikasi oleh tindakan yang terjadi dalam belokan? Haruskah objek game melacak efek dari setiap kartu aksi? Atau, jika kartu harus menerapkan efeknya sendiri (misalnya dengan mengimplementasikan antarmuka), pengaturan apa yang diperlukan untuk memberi mereka kekuatan yang cukup? Saya telah memikirkan beberapa solusi untuk masalah ini, tetapi saya bertanya-tanya apakah ada cara standar untuk menyelesaikannya. Secara khusus, saya ingin tahu objek / kelas / apa pun yang bertanggung jawab untuk melacak tindakan yang harus dilakukan setiap pemain sebagai konsekuensi dari kartu aksi yang dimainkan, dan juga bagaimana itu berkaitan dengan perubahan sementara dalam urutan normal dari mesin turn state.

Apis Utilis
sumber
2
Halo Apis Utilis, dan selamat datang di GDSE. Pertanyaan Anda ditulis dengan baik, dan sangat bagus bahwa Anda mereferensikan pertanyaan terkait. Namun, pertanyaan Anda mencakup banyak masalah yang berbeda, dan untuk sepenuhnya mengatasinya, sebuah pertanyaan mungkin harus sangat besar. Anda mungkin masih mendapatkan jawaban yang baik, tetapi diri Anda dan situs akan mendapat manfaat jika Anda memecahkan masalah Anda lagi. Mungkin mulai dengan membuat game yang lebih sederhana dan membangun ke Dominion?
michael.bartnett
1
Saya akan mulai dengan memberikan masing-masing kartu sebuah skrip yang mengubah kondisi permainan, dan jika tidak ada yang aneh terjadi, kembalilah pada aturan belokan default ...
Jari Komppa

Jawaban:

11

Saya setuju dengan Jari Komppa bahwa mendefinisikan efek kartu dengan bahasa scripting yang kuat adalah cara yang harus dilakukan. Tetapi saya percaya bahwa kunci untuk fleksibilitas maksimum adalah penanganan acara yang dapat skrip.

Untuk memungkinkan kartu berinteraksi dengan acara gim nanti, Anda bisa menambahkan skrip API untuk menambahkan "kail skrip" ke acara tertentu, seperti awal dan akhir fase gim, atau tindakan tertentu yang dapat dilakukan pemain. Itu berarti bahwa skrip yang dijalankan ketika kartu dimainkan dapat mendaftarkan fungsi yang disebut pada saat fase tertentu tercapai. Jumlah fungsi yang dapat didaftarkan untuk setiap acara harus tidak terbatas. Ketika ada lebih dari satu, mereka kemudian dipanggil dalam urutan pendaftaran mereka (kecuali tentu saja ada aturan permainan inti yang mengatakan sesuatu yang berbeda).

Seharusnya dimungkinkan untuk mendaftarkan kait ini untuk semua pemain atau hanya untuk pemain tertentu. Saya juga menyarankan untuk menambahkan kemungkinan kait untuk memutuskan sendiri apakah mereka harus tetap dipanggil atau tidak. Dalam contoh-contoh ini nilai balik dari fungsi kait (benar atau salah) digunakan untuk menyatakan ini.

Kartu giliran ganda Anda kemudian akan melakukan sesuatu seperti ini:

add_event_hook('cleanup_phase_end', current_player, function {
     setNextPlayer(current_player); // make the player take another turn
     return false; // unregister this hook afterwards
});

(Saya tidak tahu apakah Dominion bahkan memiliki sesuatu seperti "fase pembersihan" - dalam contoh ini adalah fase terakhir hipotetis dari para pemain berubah)

Kartu yang memungkinkan setiap pemain untuk menggambar kartu tambahan di awal fase pengundian mereka akan terlihat seperti ini:

add_event_hook('draw_phase_begin', NULL, function {
    drawCard(current_player); // draw a card
    return true; // keep doing this until the hook is removed explicitely
});

Kartu yang membuat pemain target kehilangan titik hit setiap kali mereka memainkan kartu akan terlihat seperti ini:

add_event_hook('play_card', target_player, function {
    changeHitPoints(target_player, -1); // remove a hit point
    return true; 
});

Anda tidak akan menemukan kode-aksi permainan seperti menggambar kartu atau kehilangan hit poin, karena definisi lengkapnya - apa sebenarnya artinya "menggambar kartu" - adalah bagian dari mekanisme permainan inti. Sebagai contoh, saya tahu beberapa TCG di mana ketika Anda harus menggambar kartu untuk alasan apa pun dan deck Anda kosong, Anda kehilangan permainan. Aturan ini tidak dicetak pada setiap kartu yang membuat Anda menggambar kartu, karena itu ada di buku aturan. Jadi, Anda tidak harus memeriksa kondisi yang hilang dalam skrip setiap kartu juga. Memeriksa hal-hal seperti itu harus menjadi bagian dari fungsi hard-coded drawCard()(yang, omong-omong, juga akan menjadi kandidat yang baik untuk acara hookable).

Ngomong-ngomong: Tidak mungkin Anda akan dapat merencanakan ke depan untuk setiap edisi mekanik masa depan yang tidak jelas dapat muncul , jadi apa pun yang Anda lakukan, Anda masih harus menambahkan fungsionalitas baru untuk edisi mendatang sesekali (dalam hal ini kasing, sebuah confetti melemparkan minigame).

Philipp
sumber
1
Wow. Confetti kekacauan itu.
Jari Komppa
Jawaban yang sangat bagus, @Philipp, dan ini menangani banyak hal yang dilakukan di Dominion. Namun, ada tindakan yang harus terjadi segera ketika kartu dimainkan, yaitu kartu yang dimainkan yang memaksa pemain lain untuk membalik kartu teratas perpustakaannya dan memungkinkan pemain saat ini untuk mengatakan "Simpan" atau "Buang". Apakah Anda akan menulis kait peristiwa untuk menangani tindakan segera seperti itu, atau apakah Anda perlu membuat metode tambahan untuk membuat skrip kartu?
fnord
2
Ketika sesuatu perlu segera terjadi, skrip harus memanggil fungsi yang sesuai secara langsung dan tidak mendaftarkan fungsi hook.
Philipp
@JariKomppa: Set Unglued sengaja tidak masuk akal dan penuh kartu gila yang tidak masuk akal. Favorit saya adalah kartu yang membuat semua orang mengambil titik kerusakan ketika mereka mengucapkan kata tertentu. Saya memilih 'itu'.
Jack Aidley
9

Saya memberikan masalah ini - mesin permainan kartu komputer yang fleksibel - beberapa orang berpikir beberapa waktu lalu.

Pertama-tama, permainan kartu yang rumit seperti Chez Geek atau Fluxx (dan, saya percaya, Dominion) akan membutuhkan kartu yang dapat dituliskan skrip. Pada dasarnya setiap kartu akan datang dengan sekelompok skripnya sendiri yang dapat mengubah keadaan permainan dengan berbagai cara. Ini akan membuat Anda memberikan sistem beberapa pemeriksaan di masa depan, karena skrip mungkin dapat melakukan hal-hal yang tidak dapat Anda pikirkan saat ini, tetapi mungkin datang dalam ekspansi di masa depan.

Kedua, "belokan" yang kaku dapat menyebabkan masalah.

Anda memerlukan semacam "tumpukan tumpukan" yang berisi "putaran khusus", seperti "buang 2 kartu". Ketika tumpukan kosong, putaran normal standar berlanjut.

Di Fluxx, sangat mungkin bahwa satu putaran berjalan seperti:

  • Pilih kartu N (sebagaimana dinyatakan oleh aturan saat ini, dapat diubah melalui kartu)
  • Mainkan kartu N (sebagaimana dinyatakan oleh aturan saat ini, dapat diubah melalui kartu)
    • Salah satu kartu mungkin "ambil 3, mainkan 2"
      • Salah satu dari kartu-kartu itu mungkin "berbelok lagi"
    • Salah satu kartu mungkin "membuang dan menggambar"
  • Jika Anda mengubah aturan untuk mengambil lebih banyak kartu daripada yang Anda lakukan saat giliran Anda dimulai, pilih lebih banyak kartu
  • Jika Anda mengubah aturan untuk lebih sedikit kartu yang ada, semua orang harus segera membuang kartu
  • Ketika giliran Anda berakhir, buang kartu hingga Anda memiliki kartu N (dapat diubah melalui kartu, lagi), lalu belok lagi (jika Anda memainkan kartu "ambil putaran lain" kadang-kadang dalam kekacauan di atas).

..dan seterusnya, dan sebagainya. Jadi merancang struktur belokan yang dapat menangani penyalahgunaan di atas bisa agak rumit. Tambahkan pula banyak permainan dengan kartu "kapan saja" (seperti dalam "chez geek") di mana kartu "kapan saja" dapat mengganggu aliran normal dengan, misalnya, membatalkan kartu apa pun yang terakhir dimainkan.

Jadi pada dasarnya saya akan mulai dari mendesain struktur belokan yang sangat fleksibel, mendesainnya sehingga dapat digambarkan sebagai skrip (karena setiap gim akan membutuhkan "skrip master" sendiri yang menangani struktur gim dasar). Kemudian, kartu apa pun harus dapat dituliskan; sebagian besar kartu mungkin tidak melakukan sesuatu yang aneh, tetapi yang lain melakukannya. Kartu juga dapat memiliki berbagai atribut - apakah dapat disimpan di tangan, dimainkan "kapan saja", apakah dapat disimpan sebagai aset (seperti 'penjaga' fluks, atau berbagai hal dalam 'chez geek' seperti makanan) ...

Saya tidak pernah benar-benar mulai menerapkan semua ini, jadi dalam praktiknya Anda mungkin menemukan banyak tantangan lain. Cara termudah untuk memulai adalah memulai dengan apa pun yang Anda ketahui tentang sistem yang ingin Anda implementasikan, dan mengimplementasikannya dengan cara yang dapat dituliskan, mengatur sesedikit mungkin, sehingga ketika ekspansi dilakukan, Anda tidak perlu merevisi sistem dasar - banyak. =)

Jari Komppa
sumber
Ini adalah jawaban yang bagus, dan saya akan menerima keduanya jika saya bisa. Saya memutuskan hubungan dengan menerima jawaban dari orang yang memiliki reputasi lebih rendah :)
Apis Utilis
Tidak masalah, saya sudah terbiasa sekarang ... =)
Jari Komppa
0

Hearthstone tampaknya melakukan hal-hal yang berhubungan dan jujur ​​saya pikir cara terbaik untuk mencapai fleksibilitas adalah melalui mesin ECS dengan desain berorientasi data. Sudah mencoba membuat tiruan batu perapian dan terbukti tidak mungkin sebaliknya. Semua kasing tepi. Jika Anda menghadapi banyak kasus tepi aneh ini maka itu mungkin cara terbaik untuk menyelesaikannya. Saya cukup bias meskipun dari pengalaman baru-baru ini mencoba teknik ini.

Sunting: ECS bahkan mungkin tidak diperlukan tergantung pada jenis fleksibilitas dan optimalisasi yang Anda inginkan. Itu hanya satu cara untuk mencapai ini. DOD Saya keliru menganggap pemrograman prosedural meskipun mereka banyak berhubungan. Yang saya maksud. Bahwa Anda harus mempertimbangkan untuk menghilangkan OOP seluruhnya atau sebagian besar setidaknya dan alih-alih memusatkan perhatian Anda pada data dan bagaimana OOP. Hindari warisan dan metode. Alih-alih fokus pada fungsi publik (sistem) untuk memanipulasi data kartu Anda. Setiap tindakan bukanlah hal templated atau logika dalam bentuk apa pun, melainkan data mentah. Di mana sistem Anda kemudian menggunakannya untuk melakukan logika. Kasing sakelar integer atau menggunakan integer untuk mengakses array pointer fungsi membantu mengetahui logika yang diinginkan dari data input secara efisien.

Aturan dasar yang harus diikuti adalah bahwa Anda harus menghindari mengikat logika secara langsung bersama-sama dengan data, Anda harus menghindari membuat data bergantung satu sama lain sebanyak mungkin (pengecualian mungkin berlaku), dan ketika Anda menginginkan logika fleksibel yang terasa di luar jangkauan ... Pertimbangkan untuk mengubahnya menjadi data.

Ada manfaat yang bisa didapat dari melakukan ini. Setiap kartu dapat memiliki nilai enum atau string untuk mewakili tindakan mereka. Magang ini memungkinkan Anda untuk mendesain kartu melalui teks atau file json dan memungkinkan program untuk mengimpornya secara otomatis. Jika Anda membuat aksi pemain daftar data ini memberikan fleksibilitas bahkan lebih terutama jika kartu tergantung pada logika masa lalu seperti halnya hearthstone, atau jika Anda ingin menyimpan permainan atau memutar ulang permainan di titik mana pun. Ada potensi untuk membuat AI lebih mudah. Terutama ketika menggunakan "sistem utilitas" alih-alih "pohon perilaku". Jaringan juga menjadi lebih mudah karena daripada perlu mencari cara untuk mendapatkan seluruh objek polimorfik yang mungkin ditransfer melalui kabel dan bagaimana serialisasi akan diatur setelah faktanya, Anda sudah memiliki objek game Anda tidak lebih dari data sederhana yang akhirnya sangat mudah untuk bergerak. Dan yang terakhir tapi pasti ini memungkinkan Anda untuk mengoptimalkan lebih mudah karena alih-alih membuang-buang waktu mengkhawatirkan kode Anda dapat mengatur data Anda dengan lebih baik sehingga prosesor akan memiliki waktu yang lebih mudah mengatasinya. Python mungkin memiliki masalah di sini, tetapi cari "cache line" dan bagaimana hubungannya dengan game dev. Mungkin tidak penting untuk membuat prototipe, tetapi pada akhirnya akan sangat berguna.

Beberapa tautan bermanfaat.

Catatan: ECS memungkinkan seseorang untuk secara dinamis menambah / menghapus variabel (disebut komponen) saat runtime. Contoh program c tentang bagaimana ECS "mungkin" terlihat (ada banyak cara untuk melakukannya).

unsigned int textureID = ECSRegisterComponent("texture", sizeof(struct Texture));
unsigned int positionID = ECSRegisterComponent("position", sizeof(struct Point2DI));
for (unsigned int i = 0; i < 10; i++) {
    void *newEnt = ECSGetNewEntity();
    struct Point2DI pos = { 0 + i * 64, 0 };
    struct Texture tex;
    getTexture("test.png", &tex);
    ECSAddComponentToEntity(newEnt, &pos, positionID);
    ECSAddComponentToEntity(newEnt, &tex, textureID);
}
void *ent = ECSGetParentEntity(textureID, 3);
ECSDestroyEntity(ent);

Membuat sekelompok entitas dengan data tekstur dan posisi dan pada akhirnya menghancurkan entitas yang memiliki komponen tekstur yang berada pada indeks ketiga dari array komponen tekstur. Terlihat aneh tetapi merupakan salah satu cara dalam melakukan sesuatu. Berikut adalah contoh bagaimana Anda akan merender segala sesuatu yang memiliki komponen tekstur.

unsigned int textureCount;
unsigned int positionID = ECSGetComponentTypeFromName("position");
unsigned int textureID = ECSGetComponentTypeFromName("texture");
struct Texture *textures = ECSGetAllComponentsOfType(textureID, &textureCount);
for (unsigned int i = 0; i < textureCount; i++) {
    void *parentEntity = ECSGetParentEntity(textureID, i);
    struct Point2DI *drawPos = ECSGetComponentFromEntity(positionID, parentEntity);
    if (drawPos) {
        struct Texture *t = &textures[i];
        drawTexture(t, drawPos->x, drawPos->y);
    }
}
Blue_Pyro
sumber
1
Jawaban ini akan lebih baik jika masuk ke beberapa detail tentang bagaimana Anda akan merekomendasikan pengaturan ECS berorientasi data Anda dan menerapkannya untuk memecahkan masalah khusus ini.
DMGregory
Diperbarui terima kasih telah menunjukkan itu.
Blue_Pyro
Secara umum, saya pikir itu buruk untuk memberi tahu seseorang "bagaimana" untuk mengatur pendekatan semacam ini tetapi biarkan mereka merancang solusi mereka sendiri. Ini terbukti sebagai cara yang baik untuk berlatih dan memungkinkan solusi yang berpotensi lebih baik untuk masalah ini. Ketika berpikir tentang data lebih dari logika dengan cara ini, pada akhirnya ada banyak cara untuk mencapai hal yang sama dan semuanya tergantung pada kebutuhan aplikasi. Serta waktu programmer / pengetahuan.
Blue_Pyro