Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/Dockerfile.frontend
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM --platform=linux/amd64 ubuntu:20.04 AS build

ARG GODOT_VERSION="4.4"
ARG GODOT_VERSION="4.5.1"

USER root

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/how-to-play/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ export default function Menu({ className }: MenuProps) {
<ul className={clsx('fixed right-2 top-1/2 -translate-y-1/2 z-1 space-y-4 min-w-[168px] bg-[#0A0A0A]/80 p-2', className)}>
<MenuItem href="#create-ship" isActive={activeHash === '#create-ship'} label="CREATE SHIP" />
<MenuItem href="#move-ship" isActive={activeHash === '#move-ship'} label="MOVE SHIP" />
{/* <MenuItem href="#gather-fuel" isActive={activeHash === '#gather-fuel'} label="GATHER FUEL" />
<MenuItem href="#mine-tokens" isActive={activeHash === '#mine-tokens'} label="MINE TOKENS" />
<MenuItem href="#gather-fuel" isActive={activeHash === '#gather-fuel'} label="GATHER FUEL" />
<MenuItem href="#gather-token" isActive={activeHash === '#gather-token'} label="GATHER TOKEN" />
<MenuItem href="#mine-asteria" isActive={activeHash === '#mine-asteria'} label="MINE ASTERIA" />
<MenuItem href="#quit-game" isActive={activeHash === '#quit-game'} label="QUIT GAME" /> */}
<MenuItem href="#quit-game" isActive={activeHash === '#quit-game'} label="QUIT GAME" />
</ul>
);
}
28 changes: 27 additions & 1 deletion frontend/src/components/how-to-play/create-ship/Description.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
import Layout from "../mdx/Layout";
export default ({ children }) => <Layout>{children}</Layout>;

Creates a `ShipState` UTxO locking min ada and a `ShipToken` (minted in this tx), specifying in the datum the initial `pos_x` and `pos_y` coordinates of the ship, and setting `fuel` to an initial amount. Also adds to the `AsteriaUTxO` value the `SHIP_MINT_FEE` paid by the user.
#### This is the first step in order to play the game.

This transaction creates a `ShipState` UTxO locking min ada, a `ShipToken` (minted in this tx) and an `INITIAL_FUEL` amount of fuel tokens (minted in this tx), specifying in the datum the initial `pos_x` and `pos_y` coordinates of the ship, the ship and pilot token names and the `last_move_latest_time` as the upper bound of the transaction's validity range (posix time). Also adds to the `AsteriaUTxO` value the `SHIP_MINT_LOVELACE_FEE` paid by the user.

#### Rules to take into account:

- the ship number has to be exactly +1 of the last ship minted
- the tip slot must be recent because it's used during validation of the tx
- the start position has to be at a certain distance ([manhattan distance](https://en.wikipedia.org/wiki/Taxicab_geometry)) of coordinates (0,0). For the mainnet challenge the minimum distance is `50`.
Comment on lines +6 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix outdated datum field name.

The Create Ship flow now sets the datum field to last_move_timestamp, but the doc still refers to last_move_latest_time. This mismatch will trip anyone copying the guidance into the updated tx template—please update the wording to match the current field name.

🧰 Tools
🪛 LanguageTool

[style] ~8-~8: ‘take into account’ might be wordy. Consider a shorter alternative.
Context: ...E_FEE` paid by the user. #### Rules to take into account: - the ship number has to be exactly +...

(EN_WORDINESS_PREMIUM_TAKE_INTO_ACCOUNT)

🤖 Prompt for AI Agents
In frontend/src/components/how-to-play/create-ship/Description.mdx around lines
6 to 12, the documentation references the outdated datum field name
`last_move_latest_time`; update all occurrences in this block to the current
field name `last_move_timestamp` so the text matches the updated Create Ship
flow and the transaction template (i.e., replace the phrase "the
`last_move_latest_time` as the upper bound" with "the `last_move_timestamp` as
the upper bound" and ensure any other mentions in these lines use
`last_move_timestamp`).


#### You can query the next available ship and pilot token names using the following curl:

```
curl --location 'https://8000-ethereal-audience-bb83g6.us1.demeter.run/graphql' \
--header 'Content-Type: application/json' \
--data '{"query":"query { nextShipTokenName(spacetimePolicyId: \"0291ae7aebaf064b785542093c2b13169effb34462301e68d4b44f43\", spacetimeAddress: \"addr1wypfrtn6awhsvjmc24pqj0ptzvtfalang33rq8ng6j6y7scnlkytx\") { shipName pilotName } }","variables":{}}'
```

#### You can query the tip of the chain using the following curl:

```
curl --location 'https://8000-ethereal-audience-bb83g6.us1.demeter.run/graphql' \
--header 'Content-Type: application/json' \
--data '{"query":"query { lastSlot { slot } }","variables":{}}'
```

#### Diagram

![createShip diagram](/txs/build-ship.png)
214 changes: 139 additions & 75 deletions frontend/src/components/how-to-play/create-ship/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,93 +15,116 @@ import type { ConnectedWallet } from '@/hooks/useCardano';
import { useChallengeStore } from '@/stores/challenge';

const tx3File = `tx create_ship(
p_pos_x: Int, // Ship Position X
p_pos_y: Int, // Ship Position Y
ship_name: Bytes, // Name of the ship
pilot_name: Bytes, // Name of the pilot
tip_slot: Int, // TODO: remove when tip_slot() implemented
p_pos_x: Int, // Ship Position X
p_pos_y: Int, // Ship Position Y
ship_name: Bytes, // Name of the ship
pilot_name: Bytes, // Name of the pilot
tip_slot: Int, // TODO: remove when tip_slot() implemented
last_move_timestamp: Int,
) {
locals {
initial_fuel: 480, // Should be taken from spaceTime datum
ship_mint_lovelace_fee: 1000000, // Should be taken from asteria script datum
spacetime_policy_hash: 0x0291ae7aebaf064b785542093c2b13169effb34462301e68d4b44f43,
spacetime_policy_ref: 0x3d308c0f3deb1eff764cbb765452c53d30704748681d7acd61c7775aeb8a8e46#1,
asteria_policy_ref: 0x3d308c0f3deb1eff764cbb765452c53d30704748681d7acd61c7775aeb8a8e46#0,
pellet_policy_ref: 0x3d308c0f3deb1eff764cbb765452c53d30704748681d7acd61c7775aeb8a8e46#2,
}
locals {
initial_fuel: 5, // Should be taken from spaceTime datum
ship_mint_lovelace_fee: 1000000, // Should be taken from asteria script datum
spacetime_policy_hash: 0x0291ae7aebaf064b785542093c2b13169effb34462301e68d4b44f43,
spacetime_policy_ref: 0x3d308c0f3deb1eff764cbb765452c53d30704748681d7acd61c7775aeb8a8e46#1,
asteria_policy_ref: 0x3d308c0f3deb1eff764cbb765452c53d30704748681d7acd61c7775aeb8a8e46#0,
pellet_policy_ref: 0x3d308c0f3deb1eff764cbb765452c53d30704748681d7acd61c7775aeb8a8e46#2,
}

validity {
until_slot: tip_slot, // tip_slot() + 300
}
validity {
until_slot: tip_slot, // tip_slot() + 300
}

reference SpacetimeRef {
ref: spacetime_policy_ref,
}
reference SpacetimeRef {
ref: spacetime_policy_ref,
}

reference AsteriaRef {
ref: asteria_policy_ref,
}
reference AsteriaRef {
ref: asteria_policy_ref,
}

reference PelletRef {
ref: pellet_policy_ref,
}
reference PelletRef {
ref: pellet_policy_ref,
}

input source {
from: Player,
min_amount: fees + Ada(ship_mint_lovelace_fee),
}
input* gas {
from: Player,
min_amount: fees + Ada(ship_mint_lovelace_fee) + min_utxo(pilot_token) + min_utxo(new_ship) + min_utxo(gas_change),
}

input asteria {
from: AsteriaPolicy,
min_amount: AdminToken(1),
datum_is: AsteriaDatum,
}

mint {
amount: AnyAsset(spacetime_policy_hash, pilot_name, 1) + AnyAsset(spacetime_policy_hash, ship_name, 1),
redeemer: (),
}
input asteria {
from: AsteriaPolicy,
min_amount: AdminToken(1),
datum_is: AsteriaDatum,
redeemer: AsteriaRedeemer::AddNewShip {},
}

mint {
amount: AnyAsset(spacetime_policy_hash, pilot_name, 1) + AnyAsset(spacetime_policy_hash, ship_name, 1),
redeemer: ShipyardRedeemer::MintShip {},
}

mint {
amount: Fuel(initial_fuel),
redeemer: (),
}
mint {
amount: Fuel(initial_fuel),
redeemer: FuelRedeemer::MintFuel {},
}

output {
to: Player,
amount: source - fees - Ada(ship_mint_lovelace_fee) + AnyAsset(spacetime_policy_hash, pilot_name, 1),
}
output pilot_token {
to: Player,
amount: AnyAsset(spacetime_policy_hash, pilot_name, 1) + min_utxo(pilot_token),
}

output {
to: AsteriaPolicy,
amount: asteria + Ada(ship_mint_lovelace_fee),
datum: AsteriaDatum {
ship_counter: asteria.ship_counter + 1,
shipyard_policy: asteria.shipyard_policy,
},
}
output updated_asteria {
to: AsteriaPolicy,
amount: asteria + Ada(ship_mint_lovelace_fee),
datum: AsteriaDatum {
ship_counter: asteria.ship_counter + 1,
shipyard_policy: asteria.shipyard_policy,
},
}

output {
to: SpacetimePolicy,
amount: AnyAsset(spacetime_policy_hash, ship_name, 1) + Fuel(initial_fuel),
datum: ShipDatum {
pos_x: p_pos_x,
pos_y: p_pos_y,
ship_token_name: ship_name,
pilot_token_name: pilot_name,
last_move_latest_time: tip_slot,
},
}
output new_ship {
to: SpacetimePolicy,
amount: AnyAsset(spacetime_policy_hash, ship_name, 1) + Fuel(initial_fuel) + min_utxo(new_ship),
datum: ShipDatum {
pos_x: p_pos_x,
pos_y: p_pos_y,
ship_token_name: ship_name,
pilot_token_name: pilot_name,
last_move_latest_time: last_move_timestamp,
},
}

output gas_change {
to: Player,
amount: gas - fees - Ada(ship_mint_lovelace_fee) - min_utxo(pilot_token) - min_utxo(new_ship),
}

collateral {
from: Player,
min_amount: fees,
}
}`;

const jsFile = `const result = await protocol.createShipTx({
const jsFile = `const playerAddress = process.env.PLAYER_ADDRESS;
const positionX = 4; // Replace with your desired start X position
const positionY = -50; // Replace with your desired start Y position
const shipName = "SHIP0"; // Replace 0 with the next ship number
const pilotName = "PILOT0"; // Replace 0 with the next ship number
const tipSlot = 165246231; // Replace with the latest block slot
const lastMoveTimestamp = Date.now() + 300_000;

const args: CreateShipParams = {
player: playerAddress,
pPosX: positionX,
pPosY: positionY,
pilotName: new TextEncoder().encode(\`PILOT\${shipNumber}\`),
shipName: new TextEncoder().encode(\`SHIP\${shipNumber}\`),
tipSlot: blockSlotValue + 300, // 5 minutes from last block
});`;
pilotName: new TextEncoder().encode(pilotName),
shipName: new TextEncoder().encode(shipName),
tipSlot: tipSlot + 300, // 5 minutes from last block
lastMoveTimestamp,
};

const response = await client.createShipTx(args);`;

interface CreateShipProps {
isActive: boolean;
Expand All @@ -116,6 +139,8 @@ export default function CreateShip(props: CreateShipProps) {
const [position, setPosition] = useState<{ x: number; y: number }|null>(null);
const [wallet, setWallet] = useState<ConnectedWallet|null>(null);
const [address, setAddress] = useState<string>('');
const [shipNumber, setShipNumber] = useState<number|undefined>(undefined);
const [tipSlot, setTipSlot] = useState<number|undefined>(undefined);

const errors = formState.errors || {};
const dataTx = formState.data?.tx;
Expand Down Expand Up @@ -155,6 +180,36 @@ export default function CreateShip(props: CreateShipProps) {
setAddress(connectedWallet?.changeAddress || '');
}

const handleFetchShipNumber = async () => {
const shipRes = await (await fetch("https://8000-ethereal-audience-bb83g6.us1.demeter.run/graphql", {
"headers": { "content-type": "application/json" },
"body": "{\"query\":\"query { nextShipTokenName(spacetimePolicyId: \\\"0291ae7aebaf064b785542093c2b13169effb34462301e68d4b44f43\\\", spacetimeAddress: \\\"addr1wypfrtn6awhsvjmc24pqj0ptzvtfalang33rq8ng6j6y7scnlkytx\\\") { shipName pilotName } }\",\"variables\":{}}",
"method": "POST"
})).json();

console.log(shipRes);

setShipNumber(parseInt(shipRes.data.nextShipTokenName.shipName.replace('SHIP', '')));
}

const updateShipNumber = (event: ChangeEvent<HTMLInputElement>) => {
setShipNumber(parseInt(event.target.value));
}

const handleFetchLastSlot = async () => {
const slotRes = await (await fetch("https://8000-ethereal-audience-bb83g6.us1.demeter.run/graphql", {
"headers": { "content-type": "application/json" },
"body": "{\"query\":\"query { lastSlot { slot } }\"}",
"method": "POST"
})).json();

setTipSlot(slotRes.data.lastSlot.slot);
}

const updateTipSlot = (event: ChangeEvent<HTMLInputElement>) => {
setTipSlot(parseInt(event.target.value));
}

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSubmitting(true);
Expand All @@ -179,9 +234,9 @@ export default function CreateShip(props: CreateShipProps) {

return (
<Tabs className="w-full h-full overflow-hidden" contentClassName="overflow-auto">
{/* <Tab label="Description">
<Tab label="Description">
<CreateShipDescription />
</Tab> */}
</Tab>

<Tab label="Tx Form">
<form className="flex flex-col gap-8 justify-between h-full" onSubmit={handleSubmit}>
Expand All @@ -199,7 +254,7 @@ export default function CreateShip(props: CreateShipProps) {

<ConnectWallet onWalletConnected={handleWallet} />

<div className="w-full flex flex-row gap-x-4">
<div className="w-full flex flex-row gap-x-4 mt-[-16px]">
<Input
name="shipNumber"
type="number"
Expand All @@ -208,16 +263,25 @@ export default function CreateShip(props: CreateShipProps) {
label="Ship Number"
error={errors.shipNumber}
disabled={submitting}
button="fetch last"
onClickButton={handleFetchShipNumber}
value={shipNumber}
onChange={updateShipNumber}
required
/>

<Input
name="blockSlot"
type="number"
placeholder="Latest block slot"
containerClassName="flex-1"
label="Latest block slot"
error={errors.shipNumber}
error={errors.blockSlot}
disabled={submitting}
button="fetch last"
onClickButton={handleFetchLastSlot}
value={tipSlot}
onChange={updateTipSlot}
required
/>
</div>
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/how-to-play/gather-fuel/Description.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Layout from "../mdx/Layout";
export default ({ children }) => <Layout>{children}</Layout>;

#### With this transaction you can gather fuel from the fuel pellets for your ship.

Updates the amount of fuel tokens in both `ShipState` and `PelletState` UTxOs, adding the `amount` (specified in the redeemer) from the first and subtracting it from the latter. Also allows the ship owner to get any amount of the prize tokens held in the pellet.

#### Rules to take into account:

- the ship number has to be the one controlled by the pilot token in your wallet.
- the ship has to be in the same position than the pellet
- the ship can't have more than 5 fuel units

#### Diagram

![gatherFuel diagram](/txs/gather-fuel.png)
Loading
Loading