diff --git a/.github/workflows/flying_ninja.yml b/.github/workflows/flying_ninja.yml new file mode 100644 index 000000000..0d9a77b56 --- /dev/null +++ b/.github/workflows/flying_ninja.yml @@ -0,0 +1,43 @@ +name: flying_ninja + +on: + push: + branches: [master] + pull_request: + paths: + - motoko/flying_ninja/** + - rust/flying_ninja/** + - .github/workflows/flying_ninja.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + motoko-flying_ninja: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.3.2 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: motoko/flying_ninja + run: | + icp network start -d + icp deploy + make test + + rust-flying_ninja: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-rust:0.3.2 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: rust/flying_ninja + run: | + icp network start -d + icp deploy + make test diff --git a/motoko/flying_ninja/.devcontainer/devcontainer.json b/motoko/flying_ninja/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/motoko/flying_ninja/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/motoko/flying_ninja/BUILD.md b/motoko/flying_ninja/BUILD.md deleted file mode 100644 index 8877d3ed2..000000000 --- a/motoko/flying_ninja/BUILD.md +++ /dev/null @@ -1,26 +0,0 @@ -# Continue building locally - -Projects deployed through ICP Ninja are temporary; they will only be live for 30 minutes before they are removed. To continue building locally, follow these steps. - -### 1. Install developer tools - -Install [Node.js](https://nodejs.org/en/download/) and [icp-cli](https://cli.internetcomputer.org): - -```bash -npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm -``` - -Then navigate into your project's directory that you downloaded from ICP Ninja. - -### 2. Deploy locally - -Start the local network and deploy the project: - -```bash -icp network start -d -icp deploy -``` - -## Additional examples - -Additional code examples and sample applications can be found in the [DFINITY examples repo](https://github.com/dfinity/examples). diff --git a/motoko/flying_ninja/Makefile b/motoko/flying_ninja/Makefile new file mode 100644 index 000000000..c5f5b3e90 --- /dev/null +++ b/motoko/flying_ninja/Makefile @@ -0,0 +1,28 @@ +.PHONY: test + +test: + @echo "=== Test 1: isHighScore returns true when leaderboard is empty ===" + @result=$$(icp canister call backend isHighScore '(42)') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'true' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 2: addLeaderboardEntry returns entry in leaderboard ===" + @result=$$(icp canister call backend addLeaderboardEntry '("Alice", 100)') && \ + echo "$$result" && \ + echo "$$result" | grep -q '"Alice"' && \ + echo "$$result" | grep -q '100' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 3: getLeaderboard returns persisted entry ===" + @result=$$(icp canister call backend getLeaderboard '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q '"Alice"' && \ + echo "$$result" | grep -q '100' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 4: getRandomness returns a blob ===" + @result=$$(icp canister call backend getRandomness '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'blob' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/flying_ninja/README.md b/motoko/flying_ninja/README.md index 8fbc1223b..13bacdf18 100644 --- a/motoko/flying_ninja/README.md +++ b/motoko/flying_ninja/README.md @@ -1,53 +1,34 @@ # Flying Ninja -[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/flying_ninja) - -## Overview - -Flying Ninja is a 2D side-scroller game where players interact with the flying ninja character using their keyboard's space bar to move up and down. The goal is to avoid the obstacles and obtain points for each obstacle you dodge. When the game ends, the user can add their score to the leaderboard. - -## Deploying from ICP Ninja - -This example can be deployed directly from [ICP Ninja](https://icp.ninja), a browser-based IDE for ICP. To continue developing locally after deploying from ICP Ninja, see [BUILD.md](BUILD.md). - -[![Open in ICP Ninja](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/motoko/flying_ninja) - -> **Note:** ICP Ninja currently uses `dfx` under the hood, which is why this example includes a `dfx.json` configuration file. `dfx` is the legacy CLI, being superseded by [icp-cli](https://cli.internetcomputer.org), which is what developers should use for local development. +Flying Ninja is a 2D side-scroller game where players control a ninja character using the space bar to move up and down, dodging obstacles to earn points. When the game ends, players can submit their score to an on-chain leaderboard backed by a Motoko canister on ICP. ## Build and deploy from the command line ### Prerequisites -- [x] Install [Node.js](https://nodejs.org/en/download/) -- [x] Install [icp-cli](https://cli.internetcomputer.org): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- Node.js +- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` ### Install -Clone the example project: - ```bash git clone https://github.com/dfinity/examples cd examples/motoko/flying_ninja ``` -### Deployment - -Start the local network: +### Deploy and test ```bash icp network start -d -``` - -Deploy the canisters: - -```bash icp deploy +make test +icp network stop ``` -Stop the local network when done: +For frontend development with hot reload: ```bash -icp network stop +npm run dev ``` ## Updating the Candid interface @@ -57,9 +38,9 @@ The `backend/backend.did` file defines the backend canister's public interface. If you modify the backend's public API, regenerate the `.did` file: ```bash -$(mops toolchain bin moc) --idl $(mops sources) -o backend/backend.did backend/app.mo +$(mops toolchain bin moc) --idl -o backend/backend.did backend/app.mo ``` ## Security considerations and best practices -If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices. +Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app. diff --git a/motoko/flying_ninja/backend/app.mo b/motoko/flying_ninja/backend/app.mo index 6ac759446..ae68fad7d 100644 --- a/motoko/flying_ninja/backend/app.mo +++ b/motoko/flying_ninja/backend/app.mo @@ -2,7 +2,7 @@ import Array "mo:core/Array"; import Nat "mo:core/Nat"; import Random "mo:core/Random"; -persistent actor FlyingNinja { +actor FlyingNinja { type Order = { #less; #equal; #greater }; type LeaderboardEntry = { name : Text; diff --git a/motoko/flying_ninja/dfx.json b/motoko/flying_ninja/dfx.json deleted file mode 100644 index d073ee09f..000000000 --- a/motoko/flying_ninja/dfx.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "canisters": { - "backend": { - "main": "backend/app.mo", - "type": "motoko", - "args": "--enhanced-orthogonal-persistence" - }, - "frontend": { - "dependencies": ["backend"], - "frontend": { - "entrypoint": "frontend/index.html" - }, - "source": ["frontend/dist"], - "type": "assets" - } - }, - "output_env_file": ".env", - "defaults": { - "build": { - "packtool": "mops sources" - } - } -} diff --git a/motoko/flying_ninja/frontend/src/actor.js b/motoko/flying_ninja/frontend/src/actor.js index 2f487dbdb..963914fc8 100644 --- a/motoko/flying_ninja/frontend/src/actor.js +++ b/motoko/flying_ninja/frontend/src/actor.js @@ -7,14 +7,12 @@ import { createActor } from "./bindings/backend"; // via Set-Cookie header (see vite.config.js). const canisterEnv = safeGetCanisterEnv(); -// Resolve canister ID: cookie (icp-cli + dev server) → env var (dfx build-time) -const canisterId = - canisterEnv?.["PUBLIC_CANISTER_ID:backend"] ?? - process.env.CANISTER_ID_BACKEND; +// Resolve canister ID from cookie (icp-cli + dev server) +const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; if (!canisterId) { throw new Error( - "Canister ID for 'backend' not found. Run 'icp deploy' or 'dfx deploy' first." + "Canister ID for 'backend' not found. Run 'icp deploy' first." ); } diff --git a/motoko/flying_ninja/frontend/vite.config.js b/motoko/flying_ninja/frontend/vite.config.js index b5ac17930..d1495b37c 100644 --- a/motoko/flying_ninja/frontend/vite.config.js +++ b/motoko/flying_ninja/frontend/vite.config.js @@ -1,4 +1,4 @@ -import { defineConfig, loadEnv } from "vite"; +import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import { execSync } from "child_process"; import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; @@ -28,44 +28,12 @@ function getDevServerConfig() { }; } catch {} - // Try dfx - try { - const pingResult = JSON.parse( - execSync("dfx ping", { encoding: "utf-8", stdio: "pipe" }) - ); - const rootKeyHex = Buffer.from(pingResult.root_key).toString("hex"); - const canisterId = execSync("dfx canister id backend", { - encoding: "utf-8", - stdio: "pipe", - }).trim(); - return { - headers: { - "Set-Cookie": `ic_env=${encodeURIComponent( - `ic_root_key=${rootKeyHex}&PUBLIC_CANISTER_ID:backend=${canisterId}` - )}; SameSite=Lax;`, - }, - proxy: { - "/api": { - target: "http://127.0.0.1:4943", - changeOrigin: true, - }, - }, - host: "127.0.0.1", - }; - } catch {} - throw new Error( - "No local network running. Start with:\n icp network start -d && icp deploy\nor:\n dfx start --background && dfx deploy" + "No local network running. Start with:\n icp network start -d && icp deploy" ); } -export default defineConfig(({ command, mode }) => { - // dfx generates ../.env with CANISTER_ID_* vars on deploy. Bake them into the - // bundle so actor.js can fall back to them when the ic_env cookie does not - // contain canister IDs (dfx does not inject PUBLIC_CANISTER_ID:* env vars - // into the asset canister, unlike icp-cli). - const env = loadEnv(mode, "..", ["CANISTER_"]); - +export default defineConfig(({ command }) => { return { base: "./", plugins: [ @@ -75,11 +43,6 @@ export default defineConfig(({ command, mode }) => { outDir: "./src/bindings", }), ], - define: { - "process.env.CANISTER_ID_BACKEND": JSON.stringify( - env.CANISTER_ID_BACKEND - ), - }, optimizeDeps: { esbuildOptions: { define: { global: "globalThis" } }, }, diff --git a/motoko/flying_ninja/icp.yaml b/motoko/flying_ninja/icp.yaml index a4edd3a0e..bd053999d 100644 --- a/motoko/flying_ninja/icp.yaml +++ b/motoko/flying_ninja/icp.yaml @@ -1,14 +1,11 @@ canisters: - name: backend recipe: - type: "@dfinity/motoko@v4.1.0" - configuration: - main: backend/app.mo - candid: backend/backend.did + type: "@dfinity/motoko@v5.0.0" - name: frontend recipe: - type: "@dfinity/asset-canister@v2.1.0" + type: "@dfinity/asset-canister@v2.2.1" configuration: dir: frontend/dist build: diff --git a/motoko/flying_ninja/mops.toml b/motoko/flying_ninja/mops.toml index 2de72e881..ca7eed3e6 100644 --- a/motoko/flying_ninja/mops.toml +++ b/motoko/flying_ninja/mops.toml @@ -1,13 +1,17 @@ # Motoko dependencies (https://mops.one/) [toolchain] -moc = "1.5.1" +moc = "1.9.0" [dependencies] -core = "2.4.0" +core = "2.5.0" [moc] # M0236: use context dot notation (e.g. map.get(k) instead of Map.get(map, compare, k)) # M0237: redundant explicit implicit arguments (e.g. Nat.compare is inferred automatically) # M0223: redundant type instantiation (e.g. Array.tabulate instead of Array.tabulate) -args = ["-W", "M0236", "-W", "M0237", "-W", "M0223"] +args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"] + +[canisters.backend] +main = "backend/app.mo" +candid = "backend/backend.did" diff --git a/rust/flying_ninja/.devcontainer/devcontainer.json b/rust/flying_ninja/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/rust/flying_ninja/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/rust/flying_ninja/BUILD.md b/rust/flying_ninja/BUILD.md deleted file mode 100644 index ffb6557ec..000000000 --- a/rust/flying_ninja/BUILD.md +++ /dev/null @@ -1,26 +0,0 @@ -# Continue building locally - -Projects deployed through ICP Ninja are temporary; they will only be live for 30 minutes before they are removed. To continue building locally, follow these steps. - -### 1. Install developer tools - -Install [Node.js](https://nodejs.org/en/download/) and [icp-cli](https://cli.icp.build): - -```bash -npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm -``` - -Then navigate into your project's directory that you downloaded from ICP Ninja. - -### 2. Deploy locally - -Start the local network and deploy the project: - -```bash -icp network start -d -icp deploy -``` - -## Additional examples - -Additional code examples and sample applications can be found in the [DFINITY examples repo](https://github.com/dfinity/examples). diff --git a/rust/flying_ninja/Makefile b/rust/flying_ninja/Makefile new file mode 100644 index 000000000..c081f1ba3 --- /dev/null +++ b/rust/flying_ninja/Makefile @@ -0,0 +1,26 @@ +.PHONY: test + +test: + @echo "=== Test 1/4: is_high_score returns true when leaderboard is empty ===" + @result=$$(icp canister call backend is_high_score '(100 : nat)') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'true' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 2/4: add_leaderboard_entry returns leaderboard with new entry ===" + @result=$$(icp canister call backend add_leaderboard_entry '("Alice", 100 : nat)') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'Alice' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 3/4: get_leaderboard returns stored entries ===" + @result=$$(icp canister call backend get_leaderboard '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'Alice' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 4/4: get_randomness returns a blob ===" + @result=$$(icp canister call backend get_randomness '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'blob' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/flying_ninja/backend/Cargo.toml b/rust/flying_ninja/backend/Cargo.toml index 268b84c42..6b138ea8c 100644 --- a/rust/flying_ninja/backend/Cargo.toml +++ b/rust/flying_ninja/backend/Cargo.toml @@ -10,5 +10,6 @@ crate-type = ["cdylib"] path = "lib.rs" [dependencies] -candid = "0.10.10" -ic-cdk = "0.16.0" +candid = "0.10" +ic-cdk = "0.20" +ic-cdk-management-canister = "0.1.1" diff --git a/rust/flying_ninja/backend/lib.rs b/rust/flying_ninja/backend/lib.rs index 6906baaae..3693c65fa 100644 --- a/rust/flying_ninja/backend/lib.rs +++ b/rust/flying_ninja/backend/lib.rs @@ -1,5 +1,5 @@ use candid::{CandidType, Nat}; -use ic_cdk::api::management_canister; +use ic_cdk_management_canister::raw_rand; use std::cell::RefCell; @@ -65,7 +65,7 @@ fn add_leaderboard_entry(name: String, score: Nat) -> Leaderboard { // Update function to provide secure randomness as the game seed. #[ic_cdk::update] async fn get_randomness() -> Vec { - management_canister::main::raw_rand().await.unwrap().0 + raw_rand().await.expect("raw_rand failed") } // Export the interface for the smart contract. diff --git a/rust/flying_ninja/dfx.json b/rust/flying_ninja/dfx.json deleted file mode 100644 index 6ed5a89e5..000000000 --- a/rust/flying_ninja/dfx.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "canisters": { - "backend": { - "candid": "backend/backend.did", - "type": "custom", - "shrink": true, - "gzip": true, - "wasm": "target/wasm32-unknown-unknown/release/backend.wasm", - "build": [ - "cargo build --target wasm32-unknown-unknown --release -p backend", - "candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did" - ], - "metadata": [ - { - "name": "candid:service" - } - ] - }, - "frontend": { - "dependencies": ["backend"], - "frontend": { - "entrypoint": "frontend/index.html" - }, - "source": ["frontend/dist"], - "type": "assets" - } - }, - "output_env_file": ".env" -} diff --git a/rust/flying_ninja/icp.yaml b/rust/flying_ninja/icp.yaml index da0587413..4d63b8094 100644 --- a/rust/flying_ninja/icp.yaml +++ b/rust/flying_ninja/icp.yaml @@ -8,7 +8,7 @@ canisters: - name: frontend recipe: - type: "@dfinity/asset-canister@v2.1.0" + type: "@dfinity/asset-canister@v2.2.1" configuration: dir: frontend/dist build: