본문으로 건너뛰기

Blackjack

다음 문서는 Sui에서 인기 있는 카지노 게임 Blackjack의 예제 구현을 살펴본다.

이 가이드는 그 구성 요소를 따라가며 module의 함수, 구조체, 상수, 그리고 전체 gameplay 메커니즘에서 그것들이 갖는 의미를 자세히 보여 준다.

배포된 Blackjack 게임 version은 Mysten Blackjack에서 온라인으로 제공된다.

온체인 blackjack 게임을 빌드하는 일은 Coin Flip 게임과 많은 유사점을 가진다.

이 예제는 smart contract(Move module), 백엔드 로직(serverless function 사용), 그리고 frontend 로직을 다룬다.

Click to open

Create Sui account and setup CLI environment

$ sui client

If this is the first time running the sui client CLI tool, it asks you to provide a Sui full node server URL and a meaningful environment alias. It also generates an address with a random key pair in sui.keystore and a config client.yaml.

By default, the client.yaml and sui.keystore files are located in ~/.sui/sui_config. For more information, refer to the Sui client CLI tutorial.

If this is not your first time running sui client, then you already have a client.yaml file in your local environment. If you'd like to create a new address for this tutorial, use the command:

$ sui client new-address ed25519
  • Obtain test tokens.
Click to open

How to obtain tokens

If you are connected to Devnet or Testnet networks, use the Faucet UI to request tokens.

If you are connected to a local full node, learn how to get local network tokens.

정보

백엔드 빌드와 Sui 배포에 대한 자세한 내용은 Coin Flip app example을 확인한다.

full repository for this example here에서도 확인할 수 있다.

Gameplay

이 single-player version의 blackjack에서 플레이어는 시스템에 의해 자동화된 딜러와 경쟁한다.

딜러는 게임 메커니즘에서 중심 역할을 하는 public BLS key를 갖추고 있다.

딜러의 행동은 serverless function으로 보내는 HTTP request에 의해 트리거된다.

플레이어는 게임을 시작하기 위해 bet을 건다.

게임이 시작되면 백엔드(딜러)로 request가 전송되고, 백엔드는 이를 서명하여 처리한 뒤 플레이어에게 카드 두 장을, 자신에게 카드 한 장을 나눠 준다.

플레이어는 'Hit' 또는 'Stand'를 선택할 수 있다.

'Stand'를 선택하면 총합이 17 이상이 될 때까지 딜러가 카드를 뽑게 된다.

딜러가 멈춘 후 smart contract가 개입하여 총합을 비교하고 승자를 선언한다.

반면 'Hit'를 선택하면 딜러가 플레이어를 위해 카드를 한 장 더 뽑게 된다.

분할 같은 더 복잡한 Blackjack 규칙은 이 예제의 범위를 벗어나므로 구현되지 않았다는 점에 유의한다.

Smart contracts

single_player_blackjack module

single_player_blackjack.move module에는 게임 상태를 정의하고 게임 진행 상황을 추적하는 데 도움이 되는 여러 상수가 포함되어 있다:

  • IN_PROGRESS
  • PLAYER_WON_STATUS
  • HOUSE_WON_STATUS
  • TIE_STATUS

강건한 게임 메커니즘을 보장하기 위해 EInvalidBlsSig, EInsufficientBalance 등 오류 처리를 위한 상수도 있다.

GameCreatedEvent, GameOutcomeEvent, HitDoneEvent 같은 struct는 게임 내의 다양한 이벤트와 동작을 포착한다.

HitRequestStandRequest struct는 플레이어가 이미 요청한 경우에만 하우스가 move(hit/stand)를 수행할 수 있도록 보장한다.

HouseAdminCapHouseData는 잔액과 public key를 포함한 하우스 데이터를 유지하는 데 중요하며, Game struct는 플레이어 데이터, 카드, 현재 상태 등 각 게임에 필요한 모든 정보를 담고 있다.

Module의 함수는 크게 초기화, 게임 관리, utility 함수로 분류할 수 있다.

init 함수는 house admin capability를 설정하고, initialize_house_data는 잔액과 public key를 설정하여 게임을 위한 하우스를 준비한다.

place_bet_and_create_game은 플레이어가 새 게임을 시작하는 진입점이며, bet과 random input이 포함된다.

first_deal, hit, stand 함수는 카드 배분과 플레이어 선택을 처리하며 core gameplay를 제어한다.

get_next_random_cardget_card_sum 같은 utility 함수는 게임 메커니즘에 필수적이며, random 카드를 생성하고 hand 값을 계산한다.

Module에는 다양한 게임 데이터와 하우스 데이터를 가져오기 위한 accessor도 포함되어 있다.

테스트 목적을 위해 module은 get_house_admin_cap_for_testing, player_won_post_handling_for_test, house_won_post_handling_for_test 같은 특수 함수도 제공하여 developer가 게임 메커니즘과 하우스 데이터 처리를 철저히 테스트할 수 있게 한다.

Backend

백엔드는 딜러가 실행하는 모든 transaction에 사용된다.

백엔드는 완전히 stateless할 수 있으며, 그 이유로 serverless function이 사용된다.

그 결과 해당 코드는 app/src/app/api/ directory 아래에 있다.

Directories structure

백엔드 코드는 다음 하위 디렉터리로 나뉜다:

  • games/: 백엔드 endpoint의 메인 코드이다. route.ts라는 이름의 각 파일은 project 구조에 정의된 경로에서 endpoint로 제공된다(자세한 내용은 Route Segments on NextJS App Router를 본다).
  • health/: API 가용성을 확인하기 위한 간단한 health check endpoint이다.
  • helpers/: 여러 endpoint에서 사용하는 다양한 helper 함수이다.
  • services/: transaction에 서명하고 Sui 블록체인에서 실행하는 백엔드의 core 로직이다.
  • utils/: transaction을 딜러로서 서명하고 Shinami로 sponsor하여 gas coin equivocation을 피하기 위한 재사용 가능한 method이다.

High-Level endpoints specification

HTTP MethodPathDescriptionRequest Body
GET/api/healthAPI 가용성을 확인하기 위한 간단한 health check endpoint이다.txDigest
POST/api/games/{id}/deal게임 생성 후 초기 deal transaction을 실행한다.txDigest
POST/api/games/{id}/hithit move를 실행한다.txDigest, 해당 HitRequest object의 ID
POST/api/games/{id}/standstand move를 실행한다.txDigest, 해당 StandRequest object의 ID

Need of usage of waitForTransaction

Load balancer 유무와 관계없이 full node를 사용하는 것과 결합되어 developer의 주의가 필요한 Sui 앱 개발의 흥미로운 측면 중 하나는 read-after-write와 write-after-write 사례가 발생한다는 점이다.

Initial deal transaction

예를 들어 Blackjack 게임에서는 사용자가 create game transaction을 실행한 직후 딜러가 initial deal transaction을 실행한다.

이 transaction은 인자를 받고 게임 object를 수정하므로, 방금 생성된 object를 사용하고 있다는 뜻이다.

게임 object가 딜러가 사용하는 full node에서 사용 가능하도록 하려면 create game transaction 이후 waitForTransaction을 호출해야 한다.

Hit and stand transactions

같은 방식으로 frontend에서 게임 object를 다시 가져올 때마다 게임 object를 수정한 이전 transaction이 이미 full node에서 사용 가능한지 확인한다.

이 때문에 frontend와 backend 사이에서 txDigest를 주고받고, 각 write-after-write 또는 read-after-write 사례마다 waitForTransaction을 사용해야 한다.

Frontend

Blackjack 게임 frontend module의 중심 요소인 page component는 상호작용적이고 반응성 있는 게임 경험을 만들도록 구성되어 있다.

React로 작성되었으며, 게임 로직과 사용자 상호작용을 효과적으로 처리하기 위해 여러 기능과 함수를 통합한다.

Directories structure

Frontend는 NextJS App Router project structure를 따르는 NextJS project이다.

Frontend의 메인 코드는 app/src/ directory 아래에 있다.

주요 하위 디렉터리는 다음과 같다:

  • app/: 페이지와 API endpoint의 메인 코드이다.
  • components/: 앱의 재사용 가능한 component이며 하위 디렉터리로 구성된다.
  • hooks/: 앱에서 사용하는 custom hook이다.
  • helpers/, utils/, lib/: 다양한 helper 함수와 utility이다.
  • types/: 앱에서 사용하는 type/interface이다.
  • styles/: 앱 스타일링을 위한 전역 CSS 파일이다.

Components and custom hooks for state management

  • Custom hooks: 코드를 가능한 한 구조적으로 유지하기 위해 각 단계에서 게임 보드의 복잡한 상태를 관리하는 여러 custom hook이 사용된다. useBlackjackGame custom hook은 게임 상태와 로직을 캡슐화하여 게임을 표시하고 플레이하는 데 필요한 모든 정보(game, isInitialDealLoading 같은 field 포함)와 필요한 기능(handleCreateGame, handleHit 같은 method 포함)을 노출한다. useCreateBlackjackGame, useMakeMoveInBlackjackGame 같은 여러 추가 custom hook은 코드의 가독성과 유지 보수성을 위해 자체 상태와 로직 조각을 캡슐화한다.

  • Component for game initialization: StartGame component는 새 게임 생성을 쉽게 하기 위해 구현되었다. 이 component는 create-game transaction을 실행하기 위해 useBlackjackGame hook의 handleCreateGame 함수를 사용한다.

  • Card displaying and management: DealerCardsPlayerCards component는 각각 딜러와 플레이어가 소유한 카드와 총점을 표시하는 데 사용된다.

  • Game actions: GameActions component는 HitStand 버튼을 표시하고 해당 action을 트리거하는 데 사용되며, 이 action은 해당 transaction을 실행하기 위해 useBlackjackGame hook에서 export된다.

  • BlackjackBanner: BlackjackBanner component는 플레이어가 blackjack으로 이겼을 때 표시하는 custom view로 사용된다.

Comparison: Blackjack and Coin Flip

Similarities

  • Blockchain-based logic: 두 게임 모두 탈중앙화 애플리케이션을 위한 기능을 활용하면서 Sui 위에 구축된다. 각 게임의 core 로직은 Move module에 있으며, 안전하고 검증 가능한 gameplay를 보장한다.

  • State management: 두 게임 모두에서 상태 관리는 중요하다. Blackjack에서는 React state hook을 사용해 플레이어와 딜러의 hand 및 점수를 관리하는 일이 여기에 해당한다. Coin Flip에서는 하우스 잔액과 기타 게임 관련 세부 정보를 추적하는 HouseData 같은 Move struct를 통해 상태를 관리한다.

  • Randomness and fair play: 두 게임 모두 공정성을 위해 randomness를 강조한다. 두 게임 모두 Sui의 온체인 랜덤니스를 사용하여 모든 게임에 대해 검증 가능하고 진정으로 random한 결과를 보장한다.

  • Smart contract interactions: 각 게임에는 bet 배치, 카드 배분(Blackjack), 추측 수행(Coin Flip) 같은 게임 action을 위한 smart contract 상호작용이 포함된다. 이러한 상호작용은 블록체인에서 게임 로직을 실행하는 데 중요하다.

Differences

  • Game mechanics and complexity: Blackjack은 더 동적인 frontend를 요구하는, 여러 action(hit, stand, deal)과 상태 업데이트가 있는 더 복잡한 게임이다. 반면 Coin Flip은 단일 bet과 추측 결과를 중심으로 하는 더 단순한 메커니즘을 가진다.

  • User interface (UI) complexity: Blackjack 게임은 카드 표시, 게임 상태 관리, 플레이어 상호작용을 위해 더 복잡한 UI를 포함한다. Gameplay가 더 단순한 Coin Flip은 덜 복잡한 UI를 요구한다.

  • Backend processing: Blackjack에서는 딜러가 자동화되어 있으며(기계), 플레이어의 action이 게임 결과에 직접 영향을 준다. Coin Flip 게임에서는 하우스(smart contract)가 더 수동적인 역할을 하며, 주로 플레이어의 추측에 따라 게임을 초기화하고 마무리한다.

  • Module structure and focus: Blackjack 게임은 frontend 상호작용과 실시간 업데이트에 더 초점을 맞춘다. Coin Flip 게임은 블록체인에서 게임 데이터를 안전하게 초기화하고 관리하기 위해 HouseCap, house_data 같은 구조로 백엔드 로직을 더 깊게 다룬다.

  • Multi-version implementation: Coin Flip 게임은 MEV 공격에 취약한 version과 이에 저항하는 version 두 가지를 언급하며, 이는 보안과 사용자 경험 변화에 초점을 맞추고 있음을 보여 준다. 이러한 변형은 Blackjack에는 구현되어 있지 않다.

정보

완전한 app example은 the blackjack-sui repo에서 찾을 수 있다.