Giter Club home page Giter Club logo

0xfable's Introduction

0xFable

See also the README files of the various subpackages:

Installation

  1. Install pre-requisite tooling:
    • Make
    • Foundry
      • Last foundryup: 16 May 2023
    • Node.js & PNPM (npm install -g pnpm)
      • Tested with Node v20.1.0
      • The appropriate pnpm version is listed under the "packageManager key in package.json
      • If you have any issues while installing dependencies with pnpm you can try to use corepack to make sure you use the correct version of pnpm.
        • corepack enable
        • corepack pnpm install
    • Circom
      • Needed to build circuits package
      • Tested with version 2.1.4
  2. Run make setup
  3. Run contract tests for basic sanity testing:
    • (cd packages/contracts && make test)
  4. Install Circom

IDEs

If you're using Visual Studio Code, the contract remappings will only be picked up if you set the root of the project to the contracts package. Otherwise, you'll have to manually add the remappings (from remappings.txt) to the Solidity plugin configuration.

Running

To deploy and try out the app run the following commands (anvil and webdev will keep running and must be run in their own terminal):

make anvil
make webdev
make deploy
make circuits

This will do the following:

  • Run anvil (local EVM node) at localhost:8545 with chain ID 1337 (this chain comes preconfigured in Metamask and other wallets as "Localhost")
  • Run the NextJS dev command (web server + live reload)
  • Deploy the contracts to the local node
  • Build the zk circuits (the first time, you will need to a 300MB trusted setup file). Make sure you have circom installed.

After that, you can visit the app at http://localhost:3000/ (if that port is already occupied, NextJS might affect another one).

Common caveat: Every time you restart Anvil, you might have to perform some kind of reset in your wallet. With Metamask, that's "..." > "Settings" > "Advanced" > "Clear activity and nonce data".

Testing The Game

If you access the app via http://localhost:3000/, you can interact with it with your wallet. You'll need to "Claim Airdrop" in order to get a deck to play with.

However, for testing, it's expected you'll use the Anvil ("test ... junk" mnemonic) accounts. These come preloaded with local testnet ETH, and as part of our deploy script, we pre-airdrop a deck to accounts 0 and 1.

You can import those accounts in your wallet, but we recommend instead accessing the app via:

This will load the test private key locally, and avoid the need for you to click your wallet every time a transaction is made!

IMPORTANT: You will need to use two separate browsers (or two separate profiles) to test the game with two different accounts. It may work to some extent with two different tabs, but any reload will reset the address.

If you would like to skip proof generation and verification, you can replace make deploy and make dev with the following commands:

  • cd packages/contracts && make deploy-noproofs
  • cd packages/webapp && make dev-noproofs

If you would like to test with deterministic randomness and avoid timeouts when a player takes too long to perform an action, you can replace make deploy with:

  • cd packages/contracts && make deploy-norandom

To both skip proofs and use deterministic randomness, you can replace make deploy and make dev with the following commands:

  • cd packages/contracts && make deploy-noproofs
  • cd packages/webapp && make dev-noproofs-norandom

Commands

See the Makefile for a description of all top-level make commands.

0xfable's People

Contributors

0xnonso avatar blazing-mike avatar boffee avatar eerkaijun avatar eviterin avatar faytey avatar geniusgarlic avatar kevincharm avatar nab5 avatar nezouse avatar norswap avatar omahs avatar ptisserand avatar sasha-k-cp avatar tudorpintea999 avatar ultraviolet10 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

0xfable's Issues

Enable archiving game data

(This will only be relevant in the far future.)

Currently, game data accumulates in the game contract, with a minimal amount of cleanup done. In the future we might wish to archive it somewhere so that appchain validators do not need to know about the old game data.

(Note regarding cleanup: we can safely erase the final game state, but we need to keep at least the list of participants, the winner(s), and the listing of cards used in each player's deck.)

One option would be to (1) compress the game data (e.g. using something like zipped-contracts) and (2) send it to contract storage (e.g. using SSTORE2)

Writing to contract storage not only cost less gas when using the Ethereum gas schedule (though we could depart from that), but also means that validators would only need a single hash for the game assuming they do not care about historical games. To achieve this, we would need to guarantee that either tx cannot call these storage contracts (i.e. it is illegal).

For now this is way way premature optimization, but I think it's pretty neat and wanted to write about it.

Deck creator

Small project that can be tackled in parallel with more pressing stuff.

Create a "deck creator" interface that enables people to pick cards from their collection and assemble them in a deck.

The contract-side calls (in the Inventory.sol) contracts are already implemented, though it's very possible it would be convenient to retool them somehow.

The current collection page display could be used as a starting template. Though there is a significant amount of UX work to be done for this.

Implement more stringent timeouts

Currently there is a technical timeout after 256 blocks (512s) because we can't get the public randomness blockhash. This is 8 minutes, which is too much, we should lower this.

The code is mostly in place already, though it's really hacky on the frontend side (probably the worst code in the codebase right now in terms of how integrated it is).

Possibly use a chess clock?

Investigate SolidJS

From having a cursory look at SolidJS, it seems more "correct" than React. The reactivity model seems to have a lot less footguns and opportunities for wasteful re-rendering than React.

If switching frameworks is in the cards, earlier is better than later, so we should have a look soon-ish.

The main issue I am seeing is that there are no SolidJS-compatible wallet libraries. One option is to do a friendly fork of an existing one, and port over their React components and React hooks to SolidJS. This being feasible and relatively easy is a prime requirement to consider this migration at all.

Build failed on macOS

uname -a

Darwin Kais-MacBook-Pro.local 22.5.0 Darwin Kernel Version 22.5.0: Mon Apr 24 20:51:50 PDT 2023; root:xnu-8796.121.2~5/RELEASE_X86_64 x86_64

node -v 
v20.1.0

pnpm -v
8.6.2
node_modules/.pnpm/[email protected]/node_modules/better-sqlite3: Running install script, failed in 32.3s
.../node_modules/better-sqlite3 install$ prebuild-install || npm run build-release
│ prebuild-install warn install No prebuilt binaries found (target=20.1.0 runtime=node arch=x64 libc= platform=darw
│ > [email protected] build-release
│ > node-gyp rebuild --release
│ gyp info it worked if it ends with ok
│ gyp info using [email protected]
│ gyp info using [email protected] | darwin | x64
│ gyp info find Python using Python version 3.8.10 found at "/usr/local/bin/python3"
│ gyp info spawn /usr/local/bin/python3
│ gyp info spawn args [
│ gyp info spawn args   '/Users/kaichen/.nvm/versions/node/v20.1.0/lib/node_modules/npm/node_modules/node-gyp/gyp/g
│ gyp info spawn args   'binding.gyp',
│ gyp info spawn args   '-f',
│ gyp info spawn args   'make',
│ gyp info spawn args   '-I',
│ gyp info spawn args   '/Users/kaichen/Documents/projects/0xFable/node_modules/.pnpm/[email protected]/node_mod
│ gyp info spawn args   '-I',
│ gyp info spawn args   '/Users/kaichen/.nvm/versions/node/v20.1.0/lib/node_modules/npm/node_modules/node-gyp/addon
│ gyp info spawn args   '-I',
│ gyp info spawn args   '/Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/common.gypi',
│ gyp info spawn args   '-Dlibrary=shared_library',
│ gyp info spawn args   '-Dvisibility=default',
│ gyp info spawn args   '-Dnode_root_dir=/Users/kaichen/Library/Caches/node-gyp/20.1.0',
│ gyp info spawn args   '-Dnode_gyp_dir=/Users/kaichen/.nvm/versions/node/v20.1.0/lib/node_modules/npm/node_modules
│ gyp info spawn args   '-Dnode_lib_file=/Users/kaichen/Library/Caches/node-gyp/20.1.0/<(target_arch)/node.lib',
│ gyp info spawn args   '-Dmodule_root_dir=/Users/kaichen/Documents/projects/0xFable/node_modules/.pnpm/better-sqli
│ gyp info spawn args   '-Dnode_engine=v8',
│ gyp info spawn args   '--depth=.',
│ gyp info spawn args   '--no-parallel',
│ gyp info spawn args   '--generator-output',
│ gyp info spawn args   'build',
│ gyp info spawn args   '-Goutput_dir=.'
│ gyp info spawn args ]
│ gyp info spawn make
│ gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
│   TOUCH ba23eeee118cd63e16015df367567cb043fed872.intermediate
│   ACTION deps_sqlite3_gyp_locate_sqlite3_target_copy_builtin_sqlite3 ba23eeee118cd63e16015df367567cb043fed872.int
│   TOUCH Release/obj.target/deps/locate_sqlite3.stamp
│   CC(target) Release/obj.target/sqlite3/gen/sqlite3/sqlite3.o
│   LIBTOOL-STATIC Release/sqlite3.a
│   CXX(target) Release/obj.target/better_sqlite3/src/better_sqlite3.o
│ In file included from ../src/better_sqlite3.cpp:4:
│ In file included from ./src/better_sqlite3.lzz:11:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/node.h:73:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8.h:24:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-array-buffer.h:12:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-local-handle.h:12:
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:465:30: warning: 'static_assert' with no
│ SHARED_EXTERNAL_POINTER_TAGS(CHECK_SHARED_EXTERNAL_POINTER_TAGS)
│                              ^
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:465:30: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:465:30: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:465:30: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:465:30: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ PER_ISOLATE_EXTERNAL_POINTER_TAGS(CHECK_NON_SHARED_EXTERNAL_POINTER_TAGS)
│                                   ^
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:466:35: warning: 'static_assert' with no
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:693:61: warning: 'static_assert' with no
│     static_assert(kJSObjectType + 1 == kFirstJSApiObjectType);
│                                                             ^
│                                                             , ""
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:694:55: warning: 'static_assert' with no
│     static_assert(kJSObjectType < kLastJSApiObjectType);
│                                                       ^
│                                                       , ""
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-internal.h:695:63: warning: 'static_assert' with no
│     static_assert(kFirstJSApiObjectType < kLastJSApiObjectType);
│                                                               ^
│                                                               , ""
│ In file included from ../src/better_sqlite3.cpp:4:
│ In file included from ./src/better_sqlite3.lzz:11:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/node.h:73:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8.h:24:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-array-buffer.h:13:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-object.h:9:
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-maybe.h:106:45: error: no template named 'is_lvalue
│   template <class U, std::enable_if_t<!std::is_lvalue_reference_v<U>>*>~~~~~^~~~~~~~~~~~~~~~~~~~~
│                                             is_lvalue_reference
│ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1
│ struct _LIBCPP_TEMPLATE_VIS is_lvalue_reference : _BoolConstant<__is_lvalue_reference(_Tp)> { };
│                             ^
│ In file included from ../src/better_sqlite3.cpp:4:
│ In file included from ./src/better_sqlite3.lzz:11:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/node.h:73:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8.h:24:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-array-buffer.h:13:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-object.h:9:
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-maybe.h:106:69: error: expected '(' for function-st
│   template <class U, std::enable_if_t<!std::is_lvalue_reference_v<U>>*>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-maybe.h:123:43: error: no template named 'is_lvalue
│ template <class T, std::enable_if_t<!std::is_lvalue_reference_v<T>>* = nullptr>
│                                      ~~~~~^~~~~~~~~~~~~~~~~~~~~
│                                           is_lvalue_reference
│ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1
│ struct _LIBCPP_TEMPLATE_VIS is_lvalue_reference : _BoolConstant<__is_lvalue_reference(_Tp)> { };
│                             ^
│ In file included from ../src/better_sqlite3.cpp:4:
│ In file included from ./src/better_sqlite3.lzz:11:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/node.h:73:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8.h:24:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-array-buffer.h:13:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-object.h:9:
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-maybe.h:123:67: error: expected '(' for function-st
│ template <class T, std::enable_if_t<!std::is_lvalue_reference_v<T>>* = nullptr>
│                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
│ In file included from ../src/better_sqlite3.cpp:4:
│ In file included from ./src/better_sqlite3.lzz:11:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/node.h:73:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8.h:33:
│ In file included from /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-function.h:11:
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-function-callback.h:151:66: warning: 'static_assert
│                 kReturnValueDefaultValueIndex - kReturnValueIndex);
│                                                                  ^
│                                                                  , ""
│ /Users/kaichen/Library/Caches/node-gyp/20.1.0/include/node/v8-function-callback.h:153:50: warning: 'static_assert
│                 kIsolateIndex - kReturnValueIndex);
│                                                  ^
│                                                  , ""
│ ./src/util/macros.lzz:157:21: error: no member named 'AccessorSignature' in namespace 'v8'
│                 v8::AccessorSignature::New(isolate, recv)
│                 ~~~~^
│ ./src/objects/database.lzz:180:21: warning: variable 'status' set but not used [-Wunused-but-set-variable]
│                 int status = sqlite3_db_config(db_handle, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL);
│                     ^
│ ./src/util/binder.lzz:37:51: error: no member named 'CreationContext' in 'v8::Object'
│                 v8::Local<v8::Context> ctx = obj->CreationContext();
│                                              ~~~~~^
│ 23 warnings and 6 errors generated.
│ make: *** [Release/obj.target/better_sqlite3/src/better_sqlite3.o] Error 1
│ rm ba23eeee118cd63e16015df367567cb043fed872.intermediate
│ gyp ERR! build error
│ gyp ERR! stack Error: `make` failed with exit code: 2
│ gyp ERR! stack     at ChildProcess.onExit (/Users/kaichen/.nvm/versions/node/v20.1.0/lib/node_modules/npm/node_mo
│ gyp ERR! stack     at ChildProcess.emit (node:events:511:28)
│ gyp ERR! stack     at ChildProcess._handle.onexit (node:internal/child_process:293:12)
│ gyp ERR! System Darwin 22.5.0
│ gyp ERR! command "/Users/kaichen/.nvm/versions/node/v20.1.0/bin/node" "/Users/kaichen/.nvm/versions/node/v20.1.0/
│ gyp ERR! cwd /Users/kaichen/Documents/projects/0xFable/node_modules/.pnpm/[email protected]/node_modules/bette
│ gyp ERR! node -v v20.1.0
│ gyp ERR! node-gyp -v v9.3.1
│ gyp ERR! not ok
└─ Failed in 32.3s at /Users/kaichen/Documents/projects/0xFable/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3
 ELIFECYCLE  Command failed with exit code 1.

Ensure the same card cannot figure in the same deck multiple time

This should be checked as part of checkDeck in Inventory.sol.

We could modify the sorting doing there to preserve the unique NFT ID, then we only have to check all the instances of a similar card for similarity.

Once we have a frontend deck builder, we should also check this constraints (and others) there at add-time — although they won't be enforced contract-side when adding.

Alternatively, we could get rid of the notion of adding and removing cards from a deck and only mandate that decks are updated wholesale. This is potentially costly in terms of calldata, which is the main cost for rollups, so I do not think it's the way to go.

Implement blocker assignment

Blocker selection resembles attacker selection but with the added difficulty that each blocker must be matched to an attacker.

To assign a defender, drag from the defender to the attacker to block (these should be highlighted with a glow). The dragging should create an arrow whose head follows the mouse cursor. Upon making a valid assignment (dragging from non-previously attacking creatures on your side to an attacking creature on the other side, then releasing), the arrow settles in place between the center of the defender and the center of the attacker and becomes permanent. If you start re-dragging from the same creature, the old arrow vanishes and you can make another assignment instead.

This kind of thing is implement in another card game called alketh (github, frontend), which can be used as inspiration (with the blessing of the creator).

Once satisfied with the blocker assignment, a "start fight" button can be pressed.

Modals: use dialog + react portal

Mostly about accessibility / best practices, use the HTML <dialog> element and use a React Portal to move the modal outside of the normal tree.

Implement a horizontal card drawer component

This component would display a list of cards side by side, centered vertically when not taking up the whole screen.

This component can be used for displaying the cards in our hand, the cards in play under our control, and the cards in play under the opponent's control.

There should be some kind of effect when hovering a card (glow around the border, slight zoom), and a click on a card would expand the card detail (just a smaller version of the card picture is displayed in this "drawer"). Probably a good idea to include attack and defense for the creatures too.

If there are too many cards to display in the space available, there should be arrows icons on the left and right of the drawer to enable scrolling. If at all possible, keeping the arrow clicked should smooth scroll through the cards in the desired direction (to be tested for feel). We should also enable the mouse wheel to scroll in the selected area, maybe the arrow keys also (though maybe we should reserve this for keyboard card navigation?). Ideally, drag and drop in empty spaces should also enable us to to pan the drawer.

Develop a mobile version

Clearly, the current UX is not ideal for mobile. We could force landscape mode and scale the UI and things would be usable (similar to how other card games tend to be played on mobile, e.g. Hearthstone or Gwent), but I'd like to do something more natural.

My strawman proposal is a list of vertical "drawers" each with a heading that collapses/expands the drawer when clicked. Each drawer represents a game area (hand, our side of the battlefield, the opponent side of the battlefields, the player's graveyards...) and within each drawer the cards in the corresponding area are listed. This could be as icons or just as a bunch of lines listing the name of the cards (or this could even be configurable).

The UX flow would generally be fairly similar to the desktop version (see the relevant epic), though it obviously need adaptations. Drag and drop should be removed in favour of buttons. For assigning blockers, deciding to block should pop a list of valid blocking targets to select from.

  • post a wireframe to clarify the proposed drawers UI

Transition Circom tests to WASM

Currently we test the Circom circuits using circom-helper which generates the proofs in C++.

circom-helper doesn't seem to work with Mac (I tried hacking this to work a whole lot to no avail). While it's certainly ultimately possible, it's not really worth the effort.

Instead, we should use the wasm provers, which works cross-platform, and is ultimately what we will use in production. Also an important part of designing/testing zk circuits is ascertaining their performance, and circom-helper goes a lot faster than WASM, hence the need to run them with WASM anyway.

Use a burner wallet with account abstraction

Currently, every action in the game requires signing a transaction in the wallet, this is of course not acceptable.

There are multiple solutions to that issue, notably involving some form of account abstraction.

But the simplest solution by far is to set up a burner wallet: a private key derived locally (maybe from a single wallet signature) and kept in the browser storage, used to sign actions as players do them.

There is, to the best of my knowledge, no burner wallet library, however it's clear that plenty of games use them: Dark Forest, Conquest.eth, ... MUD comes bundled with one. So there are plenty of space to get inspiration from.

(It could be interesting to bundle whatever we end up implementing as an open-source library.)

Regarding practicality, this will need to be coupled with a faucet implementation for the local devnet. This can be fairly trivial: just bundle the standard devnet private keys and have them send some funds to the generated burner wallet.

For the eventual testnet, it's more difficult, and ties in to the question of how we will host the testnet. My preference would be set up a simple PoA chain, since that seems like the cheapest & simplest option for a no-stakes demo.

I think we could optimistically assume nobody would spam such a devnet. In any case, the spamming should not be too costly, it simply would make the game unusable (as the amounts dropped by the faucet would not be sufficient to play it).

The faucet implementation needs to be a bit more involved on such a devnet as well: a call to a separately hosted server holding the keys to an account with a huge gas token balance.

  • Implement local devnet burner wallet
  • Implement hosted testnet faucet

FE: Implement draw and drawForJoin

Draw 5 random cards when joinGame
from the player's deck -> hand

  1. getDeck - once at the start of the game
  2. getRandomness - to do @norswap
  3. draw deck[rand] % deck.size
  4. update the deck - copy and pop, order will change
function drawForJoin(initialRandomness) {
    randomness = initialRandomness;
    for (i = 1 to 5) {
        draw(randomness);
        randomness = sha256(randomness);
    }
}

function draw(randomness) {
    // todo
}

Detect offline client

Display some info if we detect we can't get online.

The simplest thing is to use navigator.online however this is only false if the LAN is unreachable. Meaning the internet might be down but local networking up and this would still be true.

Another option to improve on this is to detect a RPC query that fails all its allotted retries.

This should be implemented as a modal on the home page and play page.

Explore re-writing circuits in Noir

Similar to #48, this issue would be to investigate if we can do better with Noir, which uses "the UltraPlonk arithmetization with Aztek's Barretenberg backend" instead of vanilla Plonk/groth16 used by Circom.

I think @nizhunt is interested in doing this.

Enabling replay/spectator mode

Currently, only the players can really meaningfully participate to a game.

It would be neat to be able to replay games, or even to spectate games.

Spectating, given no private info (like players' hand), is simply a frontend concern where a third-party can follow an ongoing game between two players (but cannot see any non-public info such as their hands).

Replay mode goes a step further and lets people replay a game that has ended, or rewind and fast-forward in an ongoing game. This is a little bit more difficult because intermediate game states are not preserved on the blockchain, as such, there is a need to replay historical events. But we can easily query a historical RPC node for these events.

If one wants to get to see private information in these replays, it's necessary for the players to willingly contribute that information. That could be done after the game is over, via an on-chain call. Ideally, this would also get compressed somewhere out of the way as proposed here.

The first annoying bit is that we'd have to temporarily store the data on-chain, which is expensive given the usual gas schedule, but maybe some kind of rebate can be implemented on a dedicated chain.

The second annoying bit is we cannot validate that the information provided by players is valid on-chain (doing so would require re-processing the whole game). But since the availability of public info is only dependent on the willingness of players to share, that doesn't matter too much — we just need to make sure we can ignore garbage data submitted by players (size-bounding the data seems particularly important to avoid griefing in the presence of a rebate.)

Spectating with private info is a lot harder, as private info cannot hit the blockchain (or the opponents could see it). We could enable the frontend to plug-in a trusted data stream sent by the player. But this is so far down the list of priority to be almost laughable.

Try using Vite instead of Next.js

Currently we use Next.js for building and routing. Next has been rather easy to work with, but it has two disadvantages:

  • The dev source maps are super shitty (mapping into big js bundles, and poorly), making debugging harder than it needs to be.
    - To make matters worse, Next aggressively prevents you from changing the source map provider.
  • We don't need server-side rendering (we have no server outside the blockchain, and I don't foresee this would be a big need) but need to worry about hydration issues nonetheless (this is rather easy to sidestep with the custom useIsHydrated hook, but it's still a bit janky). Pretty minor issue though.

I've heard that Vite has clean source maps, so I'm interested to try it out and compare.

Swapping out Next also means we need an alternative routing solution — something like react-router maybe? Open to alternatives as long as it's fairly lightweight and doesn't pull in a bunch of stuff we don't need.

Overhaul card collection display

The current display is a placeholder that doesn't pull on-chain data, and doesn't look great.

UI

Here's a suggestion of how it might look like. Open to more suggestions.

Untitled-2023-12-18-0038

Nevertheless, the picture illustrates the essential features we need:

  • List decks and deck contents
  • List cards staked in inventory
  • List all cards owned (optionally also those in inventory, should be present by default, and inventory card should be marked with a small icon overlayed on the card)
  • We probably don't need a checkbox at first.
  • Search box to search in the current category.

Data Architecture

We'll also need to think about the data architecture for this. Fetching the data is easy enough, we already have a function for fetching the content of a deck, as well as the list of decks (though they don't have names at present).

It's not hard to get all cards, but soon players' collections might be a little bit too big for that to be efficient or even possible.

It wouldn't be hard to add a paginated way to get cards. However the list of owned cards is not sorted, and that's probably not how we would like to display cards on the UI. We could do the sorting in the RPC call, but when collections get large that would be a helluvah lot of storage access. At this point, the only solution would be indexing (which we currently don't have).

I think for now we just get the full collections, do the sorting locally, and we will add indexing later (which can support pagination).

Then the question is how to deal with changes in the collection. It doesn't cost much to listen to events while the page (or even app?) is loaded. Then simply refresh when the page itself refresh (we need to investigate what happens if the client goes offline for a while, but this probably requires a similar refresh).

Check that players aren't in a game before removing cards from the inventory

Players must not be allowed to remove cards from their inventory while they're in a game, because they could potentially be using that card in the game. Letting this happen would make it easy to "flashloan" cards at game join time, creating an economic attack on the game.

This should be a pretty straightforward thing to implement, as the Game contract already has an inGame mapping that gives us exactly the information we need here.

[Epic] Implement the PoC core game loop (frontend)

So far, I've focused on laying strong foundations in the frontend, but now it's actually time to implement the core game loop that is already implemented and cursorily tested in the contracts.

This simple game loop just consists in the ability of playing creatures and having them attack magic-style: the attacker selects attacking creatures, the defender selects which creatures to block, and which creatures he controls blocks which attacking creatures (only one-on-one blocking).

This is the critical path to the tech demo, which I'm likely to tackle myself, but help is nevertheless welcome, especially preparing components and logic that could be used at a later stage. In particular:

  • all the logic to scroll in the drawer components (I might skip most of it at first)
    • similarly, smooth scrolling if that turns out to be difficult
  • drag-and-drop logic (for playing cards)
  • arrow-drag-and-drop-logic (for defending)
  • pretty glow effects & other graphical niceties

Aggregate event subscriptions

Currently, we're subscribing to each event separately. This was a limitation of viem at the time I set that up, and maybe it still is.

This needs to change, even if we have to bypass Viem, because we're listening for A LOT of events and currently we're positively hammering the RPC with eth_getFilterChanges requests.

Refactor the frontend store

I don't love how I structured the store logic (split between public atoms, private atoms, update, subscriptions), it's a little bit tangled right now.

The new structure should have:

  • custom react hooks that let us abstract over the store library (store/hooks.ts)
  • actions that let us mutate the store and can be called from the frontend (store/actions.ts)
  • the underlying Jotai atoms that implement the store (store/atoms.ts)

Subscriptions and updates can mostly stay as-is, but there may need to be an additional "logic" layer between the hooks and the atoms, such that the update logic may be able to query it directly.

Generate PoC assets / cards

We need a variety of assets for cards for the PoC, not only goblins and dwarves and one witch like we have now.

These cards will also need names and attack / defense score. The specification here isn't too terribly important.

Maybe we could come up with a fun theme for the PoC? Something themed after crypto projects / personalities?

Add a lightweight query abstraction for throttling, avoiding stale updates, and retrying

I currently have ad-hoc logic for throttling and stale update avoidance in update.ts. It would be good to abstract this away so it can be used with any kind of query.

I should also add retry logic, in case the query fail for reason outside of our control (determining this eventually ties in with error-handling).

Once we have this abstraction, we could split the fetchGameData function to separately handle fetching the FetchedGameData, fetching the cards, and fetching the on-chain randomness. Though note that this introduces new issues where we could have some of those things but not the others. If something ends up missing despite retries, we should notify the user.

Some good inspiration from tanstack-query for API design.

Implement playing cards

Playing a card should be possible in two ways:

  1. clicking the card to show its full details and then clicking "play"
  2. drag and dropping the card onto your area of the battlefield
    • the card should go back to the hand if the mouse is release when the card is over another area

When in the "playing card" step of the game, buttons should let you attack or finish your turn.

Enable cancelling some actions

Right now, some actions taken, and in particular creating / joining a game, display a loading modal that displays the current status and have a "cancel" button.

However that cancel button simply dismisses the modal, but does not stop the action from taking its course. It's still useful in quite a few scenarios (stuck actions, denied transactions when using a wallet, etc), but it's very misleading.

The only exception at the moment is that we support cancelling proof generation as of #80.

Ideally, we also need to be able to cancel at other times. Here's a roadmap of how we could do this:

  • add a global cancel flag to the store
  • (maybe?) add a global action lock to the store (so that you can't have two actions ongoing at the same time, at least for now — this should already be the case for now, but making this extra check ensures that cancel flag doesn't end up befuddling things and enforces the discipline that there are no "stuck" actions waiting for a promise that is never resolved
  • at the start of each action
    • set/check the global action lock
    • using CancellationHandler, register a cancel function to set the cancel flag
    • create a pair of checkFresh / freshWrap wrappers
    • in the checkFresh wrapper, check the cancel flag and throw a new type of exception (e.g. CancelledException) if it is set (then clear it)
    • (maybe?) save the action context in the checkFresh wrapper, and verify it in the freshWrap wrapper (this could be an alternative to the global action lock, we could also give each action an id and make cancellation a map from ID to cancellation status)
  • initially, don't cancel wagmi/viem awaits, but instead make sure the cancellation flag is set, release the log and let the action pending, make sure it will always unwind on checkFresh wrapper at some point (or complete)
  • down the line, we could wrap every wagmi/viem promise in cancellable promises

Note that right now, all actions use the modal in this way, but this is not going to stay so for "game actions", like playing a card or drawing a card. These should probably load in the background and not be cancellable.

Switch to Groth16

Currently we use Plonk, which is nice because it doesn't require a circuit-specific trusted setup.

However, after all our optimizations, the DrawHand proof is still ~22s of client-side proving, and that's on a maxed-out M1 Mac. That's probably too slow for comfort, and it's a fact that the Groth16 implementation in snarkjs should be a lot faster.

The change should be relatively trivial — just change the Makefile & various calls to snarkjs to reflect Groth16 usage instead of Plonk. The major change might the need to add the generation of the circuit-specific trusted setup to the zk package Makefile.

When going to prod, we'll need to run a credibly decentralized trusted ceremony, we can use p0tion from PSE for that prupose.

Implement attacker selection

When attacking, we need to select the attacking creatures. This is done by clicking the creatures on your side of the battleground. One click selects a creature (a border glow signals the creature is selected) and another click deselects it). Finally, a click on the "launch attack" button confirms your selection. There is also a button to end your turn if you decide not to attack.

If creatures are selected and you click "skip turn", a modal should ask you to confirm. If no creatures are select and you click "launch attack", a modal should ask you if you want to end your turn without attacking.

Add player-specific salt

Currently, we pick a random number (which is based on blockhash of the previous block) and use that to draw cards. The problem is that the opponent can see this random number too, and thus predict drawed cards.

To avoid this, we need to let each player pick a random salt in advance and commit to it on-chain by posting its hash. Henceforth, the randomness used to pick their card is a combination of the blockhash (which they can't predict) and the salt (which the opponent can't see). The zk circuits take the salt as private input and the salt hash a public input, verifying that the two match, and deriving randomness by combining the salt with the public randomness.

Investigate the possibility of migrating to MUD

This is for after the tech demo.

MUD simplifies development by synchronizing the frontend and the backend easily (amongst other things).

We can't really use MUD today because the game is "session-based": each player only really needs the data about the game he is currently in, as well as the cards he owns. MUD, on the other hand, synchronizes the whole "world" state, which in our case would be every game and global card ownership mappings. This is way overkill and wasteful.

I heard from Justin at Lattice (who develops MUD) that they intend to enable selective sync. We can consider using MUD once this is in, or we could try to contribute this feature upstream.

Another concern I have is that currently, 0xFable works without indexer, but MUD requires an indexer (it is possible to use a frontend indexer, but the necessity to reprocess the whole blockchain history makes this an impractical solutions outside of demos). The reason why an indexer is required in MUD is that everything is stores in tables, which are essentially mapping of keys to structures. Without storing the list of keys on the blockchain, it is impossible to know the extent of a table. There is talk about enabling a mode where keys are stored, and maybe this is can even be implemented independently as a "MUD module" (although the synchronization part on the frontend will require more invasive changes).

I'm torn on the issue. Besides the missing features outlined above, using a framework means less low-level control, and I feel like we have already figured out a bunch of the difficulties that MUD is supposed to alleviate. On the other hand, it means we get to benefit from all the new MUD features for free.

A simple way to figure it out would be to try and do a rewrite (maybe limiting ourselves to the tech demo features) to figure out how we feel about this in practice, and where the practical pain points end up being.

Spin up own chain infrastructure

For the tech demo, the game has to run somewhere. The long-term plan is to make it a rollup (settlement layer TBD), but in the short term, we want an easier solution.

For cost considerations, the easiest thing to do would be to spin our own Proof-of-Authoriy (PoA) chain. This would also enable to very easily make the game totally free via a burner wallet.

Make sure every revert-able error is in the ABI

For Wagmi to be able to parse contract reverts, the error must included in the contract that is initially called to cause that revert.

This means that if a transaction sent to contract A calls contract B which reverts with an error E, then E needs to be declared in contract A.

I don't think imports will solve the issue here (maybe if we can import inside a contract?). What doesn't work is copying the error in the contract file but outside the contract definition. For it to be included in the ABI, the error needs to be declared inside the contract definition.

Copying declaration is obviously very jank / prompt to being forgotten, so if we can't use imports, one thing we might do is patch the ABI on the JS side after.

Another solution that might work: bogus inheritance of a contract that defines the relevant errors.

Re-instate game join predicate

The initial design had the game creator able to specify a function (contract + selector) to be used as a callback to validate that a user is able to join a game. This enables for a variety of mechanisms to gate game access to be implemented (e.g. password-protected games, games setup for particular players, min/max requirement on ladder level, etc...).

Unfortunately, encoding the selector was a pain in Ethers.js so I just commented out that logic. We should look at reinstating it, and see if viem / wagmi make things a little bit easier.

In particular, the problem with Ethers.js was that even though I encoded the function value, it would reject it — I don't really remember why it didn't work though. I'm hoping the issue wasn't that the parameter was somehow missing from the ABI.

Anyhow, here's the logic that should be used to encode the function value (which you can see is correct by checking the spec — search for function saying it's encoded like a bytes24 and then search bytes<M> to see it gets extended to 32 bytes).

Refetch game state periodically as a failsafe

Currently, we fetch the game state on demand when we read a game event from the blockchain that we subscribed to.

Subscriptions are handled here.
Fetching and updating from the state is handled here.

This is good, but if we were to go offline, or miss an event for a reason or another we would have not way to update short of refreshing the page. As a failsafe, we should include a job that runs periodically that refetches and resynchronizes the state.

Currently, fetching the state already includes a throttling mechanism, so these scheduled refetchs could even be skipped entirely if there was a recent refetch.

Update architecture document with new circuits description

The docs/architecture.md document currently describes the old version of the circuits that still use Merkle roots. Circuits were refactored in #58, and the document should reflect that. Note that only the sections explicitly marked as outdated (the one titled after the circuit file names) need to be updated.

Rethink the zk circuits architecture

Our previous approach to circuits works, but has really high proving time when compiled to wasm (proving a Merkle root computed over a 64 field element arrays takes 60+s and requires downloading a 3GB zkey to the client).

Fortunately, we think it's possible to do much better:

  • instead of computing a Merkle tree, we can just hash the whole array
  • moreover, each element of the array can be encoded in a single byte (8 bits) and so we only need 3 fields elements to represent the whole array

However, doing this means we need to change the circuits to do a combination of indexing and shifts instead of straightforward indexing.

We're also investigating some other optimizations.

Assigning this to Kai Jun as he's likely to rewrite this in Circom, but the investigation here will be relevant for the direction of #48 and #49

Add a solo experience (bots?)

At first, especially for the tech demo, there will be very few players. To demonstrate the tech effectively, we need a solo experience. This could come in two forms:

  • Implementing a bot that will join player's games and play against them using a dummy algorithm.
    • The game right now has no gameplay to speak of, so this bot need not be difficult.
  • Implementing a "solo" version of the game contracts that automatically performs the bot's action.
    • We can make this generic by using a bot interface that enable other bots to be written.
    • Maybe the main contract should be modified so that it supports both dual-players and bot modes?

Switch to ConnectKit?

I've been less than super impressed with WalletConnect, and I've noticed a lot of people that seem to know what's up using ConnectKit. So could be worth trying the integration.

Refresh game state on a timer as a failsafe

Currently, we fetch manually when performing actions or when receiving events. As events delivery might be unreliable and we might get offline, etc... we should build some redundancy in.

This would be in the form of a setInterval job that refetches and resynchronizes the game data every so often (5s?)
Given the throttling query abstraction, this would not necessarily even need to refetch all that often if there was something pulled recently (maybe we add an extra condition that when setInterval fires, the latest fetch must have been older than the interval?).

There should also be a switch to enable/disable this, as it's probably very valuable to test both with and without this enabled.

Circuit Cleanup

  • rename deck predicate, cf this comment
  • fix some issues I noticed in the earlier proofs (draw or play proof) where some checks were missing
  • investigate updating the version pragma

Investigate using zkShuffle

Our current scheme for private information involves committing to player hands and the card remaining in player decks (via a (to be salted) Merkle root) and then proving changes to these roots via zk proofs. We also use on-chain randomness to select random cards and prove in the zk proof that the correct cards were drawn from the deck.

This has the advantage of being relatively simple and not require extra infrastructure. The disadvantage is that it limits us in terms of gameplay: game often have mechanism that manipulate the deck, such as "look at the three top cards of your deck and put them back in any order". The current zk scheme does not allow us to do this (or we need to special case for every potential effect, and it gets really complicated and expensive really fast).

An alternative exists in the form of zkShuffle, an implementation of "mental poker" by the team at p0x Labs.

I wrote some twitter threads on both system:

But in brief, mental poker thresholds encrypt every card by all players (both in our case). I encrypt then shuffle, pass the shuffled deck to you, then you encrypt and shuffle. The result is a deck where nobody knows what card is where. To reveal a card publicly, all parties need to collaborate. To reveal a card only to player X, all players except X decrypt publicly, and X decrypts privately.

This system allows any card system we want BUT comes with the problem that both players need to collaborate when revealing a card. This could mean extra on-chain latency or an off-chain component (server or p2p) to aggregate the descriptions. In practice though, the El Gamal encryption scheme is expensive to compute on-chain and so zkShuffle uses zero-knowledge proof to prove the El Gamal decryption on-chain instead (also maybe there is no way to tell that a ciphertext was decrypted with the right private key? I really have no idea how El Gamal works).

But anyway, all that work has been by the good folks at p0x Labs, who are also making their system available as open source software. The tech was used to implement https://zkholdem.xyz. Some resources:

The goal of this issue is to investigate the use of zkShuffle as applied to 0xFable. No better way to learn than by doing, so basically, reimplement the system with zkShuffle — in particular, its current state with the demo game loop (draw, play creatures, attack, defend) which only needs zk for drawing & playing.

Implement robust error handling for blockchain interactions

For this, we will need to collect all errors that can be thrown by viem, wagmi (including things inherited from tanstack-query? not sure it throws its own errors), and maybe even the wallet framework.

We'll need to make sure all of those are handled appropriately.

For errors that signal an on-chain revert, we'll need to distinguish between reverts during simulation and reverts during execution, and we'll need to parse the custom error codes to display some appropriate information for every type of error.

Explore re-writing and optimizing circuits in halo2-lib

The current circuits are written in Circom, but it would be interesting and potentially very useful to explore re-writing them in Rust with halo2-lib, especially with the arrival of new primitives like Hypernova. As a first step, we could start looking at a vanilla halo2 implementation of the circuits and then optimizations using Hypernova.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.