A fully on-chain NFT game
MoreMissilesPlz.sol
has been removed, we now primarily deal with the UfoInvasion.sol
contract, and
MissileMaker.sol
contract, but also the WorldLeader.sol
contract when it comes to the Biden / Putin NFTs.
WorldLeader:
0xAdC166631145abCe289C5ba9B5AD4D260CB9b9CA
MissileMaker
0x803e6F74c8Ed2f0444Ad3104B359802a1450c500
UfoInvasion:
0x1e0a0b2f493a25e728CDCb93E561c9667B9c56B2
initialize the contract like
// typescript
const worldLeaderContract = new web3.eth.Contract(
worldLeaderAbi.abi as any,
worldLeaderAddress
);
const missileMakerContract = new web3.eth.Contract(
missileMakerAbi.abi as any,
missileMakerAddress
);
const ufoInvasionContract = new web3.eth.Contract(
ufoInvasionContract.abi as any,
ufoInvsaionAddress
);
here are some helpers for removing all the garbage from solidity data queries and event responses:
// typescript
const getNamedValsFromObj = <T>(obj: T): T =>
Object.assign({}, ...Object.entries(obj)
.filter(([key,]) => key.length > 0 && Number.isNaN(Number(key[0])))
.map(([key, val]) => ({[key]: (typeof val === "string" && !val.startsWith("0x") && !Number.isNaN(Number(val)) ? Number(val) : val)}))
);
type HasReturnValsProp<T> = { returnValues: T };
const hasReturnVals = <T>(obj: T | HasReturnValsProp<T>): obj is HasReturnValsProp<T> =>
(obj as any).hasOwnProperty("returnValues");
const getNamedProps = <T>(events: T[] | HasReturnValsProp<T>[]) =>
events.map(x => hasReturnVals(x) ? x.returnValues : x).map(getNamedValsFromObj);
mint new NFTs like this:
const mintNftMethod = worldLeaderMintContract.methods.mintNFTs(amountToMint);
const receipt = await mintNftMethod
.send({
from: walletAddress,
to: worldLeaderMintAddress,
gas: await mintNftMethod.estimateGas(),
nonce: await web3.eth.getTransactionCount(walletAddress, "pending"),
chainId: 1088
});
you can check the status of the release like this:
enum ReleaseStatus {
Unreleased,
Whitelisted,
Released
}
const curReleaseStatus = await worldLeaderMintContract.methods.getReleaseStatus()
.call({ from: walletAddress, value: "0x0" });
if (curReleaseStatus === ReleaseStatus.Whitelisted) {
// ..
}
// solidity
function maybeGetMissiles(uint randVal) external;
// typescript
const numMissilesReadyToRoll = await missileMakerContract.methods.numMissilesReadyToRoll()
.call({ from: walletAddress, value: "0x00" });
if (numMissilesReadyToRoll > 0) {
const maybeGetMissileMethod = missileMakerContract.methods.maybeGetMissiles(getRandomInt(1, 100000));
const receipt = await maybeGetMissileMethod
.send({
from: walletAddress,
to: missileMakerAddress,
gas: await maybeGetNukeMethod.estimateGas(),
nonce: await web3.eth.getTransactionCount(walletAddress, "pending"),
chainId: 1088
});
}
// solidity
event MissilesCreated(uint missileCreatedEventId, address createdForAddress);
The MissilesCreated
event on MissileMaker.sol
contains a missileCreatedEventId
prop which can be used to query more complex state.
// solidity
struct MissileCreatedState {
uint16 dmg;
uint32 missileCreatedEventId;
address owner;
uint missileNftId;
}
function getMissileCreatedInfo(uint missileCreatedEventId) public view returns (MissileCreatedState memory);
// typescript
type MissileCreated = {
dmg: number,
missileCreatedEventId: number,
address: string,
missileNftId: number
};
const getMissileCreatedInfoFromEvent = async (walletAddress: string) => {
const missilesCreatedEvents = getNamedProps(await missileMakerContract.getPastEvents("MissilesCreated", { fromBlock: 0 }))
.filter(x => x.createdForAddress === walletAddress);
const data: MissileCreated[] = [];
for (const { missileCreatedEventId, createdForAddress } of missilesCreatedEvents) {
data.push(
await missileMakerContract.methods.getMissileCreatedInfo(missileCreatedEventId)
.call({ from: walletAddress, value: "0x0" })
);
}
return getNamedProps(data);
};
trying to attack a UFO before there are > 100 WorldLeader NFTs minted will fail
//solidity
function attackRandomUFOs(uint randVal, uint[] memory missileIds, uint amountUFOs) external;
attack amountUFOs
random UFOs using missileIds
missiles (max is 5 missiles)
// get all the missiles the user owns (this might be better as the user selecting which ones to use themselves)
const userMissiles = (await missileMakerContract.methods.getUserMissiles(walletAddress)
.call({ from: walletAddress, value: "0x00" }))
.map((x: string) => Number(x));
const attackRandomUfoMethod = missileMakerContract.methods.attackRandomUFOs(
getRandomInt(1, 1000),
userMissiles.slice(0, 4), // first 5 missiles
3 // amountOfUFOs
);
const receipt = await attackRandomUfoMethod
.send({
from: walletAddress,
to: missileMakerAddress,
gas: await attackRandomUfoMethod.estimateGas(),
nonce: await web3.eth.getTransactionCount(walletAddress, "pending"),
chainId: 1088
});
Start a new UFO invasion game (this should be called by a client when they receive a GameOver) event
don't worry about multiple clients calling it at the same time, _gameActive
acts as a mutex for ensuring
it's only called once at a time. The player who starts the game will be given 5% of the total UFO HP of the game
they created as score as a reward for paying to start the game.
// solidity
function startNewUfoInvasionGame(uint randVal) public;
// typescript
const isGameActive = await ufoInvasionContract.methods.isGameActive()
.call({ from: walletAddress, value: "0x0" });
if (!isGameActive) {
const startNewUfoInvasionGameMethod = ufoInvasionContract.methods.startNewUfoInvasionGame(getRandomInt(1, 10000));
const receipt = await startNewUfoInvasionGameMethod
.send({
from: walletAddress,
to: ufoInvasionContract,
gas: await startNewUfoInvasionGameMethod.estimateGas(),
nonce: await web3.eth.getTransactionCount(walletAddress, "pending"),
chainId: 1088
});
}
The MissileAttackedUFO
event contains a missileTxnId
prop which will be the same for missiles used within
the same txn, and the other two are self-explanatory.
// solidity
event MissileAttackedUFO(uint32 gameNum, address attacker, uint missileTxnId, uint missileId);
You can use the missileId
given by this event with the getMissileAttackInfo
method on UfoInvasion.sol
to
query more complex state about the missile attack event.
// solidity
struct MissileAttack {
uint16 dmg;
uint16 hpBefore;
uint16 hpAfter;
uint32 missileTxnId;
uint32 gameNum;
address attacker;
address locationAddress;
uint missileId;
uint ufoId;
}
function getMissileAttackInfo(uint missileId) public view returns (MissileAttack memory);
type MissileAttack = {
gameNum: number,
missileTxnId: number,
missileId: number,
ufoId: number,
attacker: string,
dmg: number,
hpBefore: number,
hpAfter: number
}
export const getMissileAttackInfoFromEvent = async (walletAddress: string) => {
// in case we only want missile attacks for a specific user, but u shud prob be subscribing to events for that anyway
const missileAttacks = getNamedProps(await ufoInvasionContract.getPastEvents("MissileAttackedUFO", { fromBlock: 0 }))
.filter(x => x.attacker === walletAddress);
const data: MissileAttack[] = [];
for (const { missileId, missileTxnId } of missileAttacks) {
data.push(
await ufoInvasionContract.methods.getMissileAttackInfo(missileId)
.call({ from: walletAddress, value: "0x0" })
);
}
return getNamedProps(data);
};
The GameOver
event fires when every UFO in the game reaches 0 hp and includes just the gameNumber of the match.
// solidity
event GameOver(uint gameNum);
You can use the gameNumber
given by this event with the getGameStatsByGameNum
method on UfoInvasion.sol
to
query more complex state about the game which has just finished.
// solidity
struct GameStats {
bool isOver;
uint16 totalUfoHp;
uint32 gameNum;
address winner;
uint gameStartTimeInSeconds;
uint elapsedSecs;
uint[] ufoIds;
}
function getGameStatsByGameNum(uint gameNum) public view returns (GameStats memory);
type GameStats = {
gameNum: number,
isOver: boolean,
winner: string
gameStartTimeInSeconds: number,
ufoIds: number[]
};
export const getGameStatsFromGameOverEvent = async (walletAddress: string): Promise<GameStats | void> => {
const gameOverEvents = getNamedProps(await ufoInvasionContract.getPastEvents("GameOver", { fromBlock: 0 }));
if (gameOverEvents.length === 0 || !gameOverEvents[0].hasOwnProperty("gameNum")) {
return console.log("got no game over events!");
}
return getNamedProps(
await ufoInvasionContract.methods.getGameStatsByGameNum(gameOverEvents[0].gameNum)
.call({ from: walletAddress, value: "0x0" })
);
};
within each GameStats
object, there is an array of ufoIds
for the ids of UFOs from that game.
the following function returns information about the current game's UFOs in the case its second argument "gameNum" is undefined, otherwise it uses the "gameNum" value to determine which game's UFOs it should get information about
// solidity
struct UfoState {
uint16 curHp;
uint16 startingHp;
uint32 gameNum;
address locationAddress;
uint ufoId;
}
function getUfoAtIdxByGameNum(uint ufoIdx, uint gameNum) external view returns (UfoState memory);
function getUfoAtIdxInCurrentGame(uint ufoIdx) external view returns (UfoState memory);
// typescript
type GameUfo = {
locationAddress: string,
ufoId: number,
curHp: number,
gameNum: number.
startingHp: number,
gameNumber: number
};
const getGameUFOs = async (
walletAddress: string,
gameNum?: number
): Promise<GameUfo[]> => {
const gameNumUfos =
Number(
!gameNum
? await ufoInvasionContract.methods.getCurGameNumUFOs()
.call({ from: walletAddress, value: "0x00" })
: await ufoInvasionContract.methods.getNumUFOsInGameByGameNum(gameNum)
.call({ from: walletAddress, value: "0x00" })
);
const data: GameUfo[] = [];
for (let i = 0; i < gameNumUfos; i++) {
data.push(
!gameNum
? await ufoInvasionContract.methods.getUfoAtIdxInCurrentGame(i)
.call({ from: walletAddress, value: "0x00" })
: await ufoInvasionContract.methods.getUfoAtIdxByGameNum(i, gameNum)
.call({ from: walletAddress, value: "0x00" })
)
}
return getNamedProps(data);
};
you could use thie function to retrieve the information of every game's UFO like this
const getAllGamesUFOs = async (
walletAddress: string
) => {
const totalNumberOfGames = Number(
await ufoInvasionContract.methods.getTotalNumberOfGames()
.call({ from: walletAddress, value: "0x00" })
);
const data: {[gameNum: number]: GameUfo[]} = {};
for (let i = 0; i < totalNumberOfGames; i++) {
data[i] = await getGameUFOs(walletAddress, i);
}
return data;
};
here are the UfoInvasion.sol
data querying methods:
// solidity
// returns whether the UFO with the id `ufoId` is alive or not
function ufoIsAlive(uint ufoId) public view returns (bool);
// returns whether there is currently a game active or not
function isGameActive() external view returns (bool);
// returns the number of UFOs in the game index by `gameNum`
function getNumUFOsInGameByGameNum(uint gameNum) external view returns;
// returns information about the UFO at the index `ufoIdx` for the game number `gameNum`
function getUfoAtIdxByGameNum(uint ufoIdx, uint gameNum) external view returns (UfoState memory);
// calls getNumUFOsInGameByGameNum(_totalNumGamesPlayed)
function getCurGameNumUFOs() external view returns (uint);
// calls getUfoAtIdxByGameNum(ufoIdx, _totalNumGamesPlayed);
function getUfoAtIdxInCurrentGame(uint ufoIdx) external view returns (UfoState memory);
// returns the number of players in the current game's stats
function getCurGameNumPlayers() external view returns (uint);
// returns the stats for the player at the index `idx` for the current game
function getCurGamePlayerAtIdx(uint idx) external view returns (CurGameScore memory);
// returns the total number of players who have stats on the leaderboard
function getNumLeaderboardPlayers() external view returns (uint);
// returns the leaderboard information for the player at the index `idx`
function getLeaderboardPlayerAtIdx(uint idx) external view returns (AllTimeLeaderboard memory);
// returns the total number of games played so far
function getTotalNumberOfGames() public view returns (uint);
// returns the game stats for the game with the idx `gameNum`
function getGameStatsByGameNum(uint gameNum) public view returns (GameStats memory);
// returns the total hp for all UFOs in the current or last game
function getTotalHpForUFOs() public view returns (uint);
here are the MissileMaker.sol
data querying methods:
// solidity
// returns the number of missiles rolling attempts the message sender has available currently
function numMissilesReadyToRoll() public view returns (uint);
// returns the amount of damage the missile with the id `missileId` does
function getMissileDmg(uint256 missileId) public view returns (uint64);
// returns the missile ids of the missiles owned by the user
function getUserMissiles(address userAddr) external view returns (uint[] memory);
// returns the percentage chance each World Leader NFT has at rolling a missile
function getMissilePercChance() public view returns (uint);