在 AO 上实现自治宠物游戏。
作者:李大狗
审阅:Lemon
来源:内容公会 - 新闻
从本篇开始,将以一个链上宠物游戏为例, Step by Step 讲述如何基于 AO 进行游戏开发。
仓库: https://github.com/rootMUD/ao-autonomous-pet/
Process Code Online:
https://www.ao.link/#/entity/oMIa9Ea8dWESVZJ3iDX62mbTNFdm9zwFtV-5th6f5zs
和其他区块链应用一致,AO-Autonomous-Pet
同样遵循「轻链上,重链下」、「抽象链上、具象链下」的原则。
在这种原则下,我们这样设计应用的架构:
+---------------+-------------------------------------------------+---------+
| Struct of Pet | Name | Description | Level | UpdatedTime | Type | Address |
+---------------+-------------------------------------------------+---------+
| Handlers
+------------- Create/Read/Write for Pet
------- ☝️ on-chain ☝️ ------- 👇 off-chain 👇 -------
+------------+ +------------+ +--------------+
| Pet Getter | | Pet Feeder | | Pet Renderer |
+------------+ +------------+ +--------------+
在链上,我们设计出抽象层面的 Pet 以及必要的 Create/Read/Write 等方法,在 AO Process 中实现。
在链下,我们把抽象层面的 Pet 通过渲染(Renderer)模块渲染为具体的 Pet;通过 Pet Getter 对 AO Process 中的数据进行读取。通过 Pet Feeder 对 Pet 的等级进行提升。
在 AO-Counter | AO 极速入门(一) 中,我们使用的是原生存储方式。在本次实践中,我们通过 AOS-SQLite
来实现我们的存储,SQLite
是非常流行的文件式关系型数据库。
AOS-SQLite Repo:
AOS CLi 启动命令:
$ aos ao-pets --module= {ao-sqlite module id}
在 AOS-SQLite Repo 中查看 ao-sqlite module id
的当前版本。
在 process 中加载 lua 脚本。
aos> .load ./lua/pet.lua
local json = require("json")
local sqlite3 = require("lsqlite3")
DB = DB or sqlite3.open_memory()
-- Create table for pets with unique constraint on address
DB:exec [[
CREATE TABLE IF NOT EXISTS pets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
description TEXT,
level INT,
type INT,
address TEXT UNIQUE,
lastUpdated INT
);
]]
加载 DB,然后执行 exec 脚本。
在这里,我们创建了一个表,里面包含如下字段:
CRW
即 Create-Read-Write
的缩写。通常,我们认为传统计算机中的基本操作是 CRUD
—— Create, Read, Update and Delete
。在区块链架构中,由于区块链不可篡改的基本特性,所以基本操作转变为 CRW
—— Create, Read and Write
。
initPet
-- Function to add a new pet
local function initPet(pet, timestamp)
local stmt = DB:prepare [[
INSERT INTO pets (name, description, level, type, address, lastUpdated)
VALUES (:name, :description, :level, :type, :address, :lastUpdated);
]]
if not stmt then
error("Failed to prepare SQL statement: " .. DB:errmsg())
end
local randomType = math.random(0, 1000)
stmt:bind_names({
name = pet.Name,
description = pet.Description,
level = 0,
type = randomType,
address = pet.Address,
lastUpdated = timestamp
})
local result = stmt:step()
if result ~= sqlite3.DONE then
print("Error: Address already exists")
Handlers.utils.reply("Error: Address already exists")(pet)
else
print('Pet Added!')
Handlers.utils.reply("Pet Added!")(pet)
end
stmt:reset()
end
...
-- Add initPet Handler
Handlers.add(
"initPet",
Handlers.utils.hasMatchingTag("Action", "initPet"),
function (msg)
initPet(msg, msg.Timestamp)
end
)
通过一个 Handler 和 一个 Function,我们实现了 initPet
。
stmt
stmt:step()
是 SQLite 中用于执行准备好的语句的函数。在 SQLite 的上下文中,准备好的语句是一个预编译的 SQL 语句,它可以用来提高效率和安全性。
当你调用 stmt:step()
时,它会执行准备好的 SQL 语句。如果 SQL 语句返回结果(例如 SELECT
查询),那么 stmt:step()
会将语句的执行状态移动到结果集中的下一行。对于其他类型的 SQL 语句(如 INSERT
、UPDATE
和 DELETE
),stmt:step()
会实际执行这些操作。
如果 stmt:step()
返回 sqlite3.ROW
,这意味着还有更多的行可以处理。如果返回 sqlite3.DONE
,这意味着语句已经完成执行,结果集已经完全遍历或者操作已经执行完毕。
在计算机编程中,随机数会具备两种属性:可预测性
与可验证性
。
+---------+---------------------+--------------+
| ฅ^•ﻌ•^ฅ | 可验证 | 不可验证 |
+---------+---------------------+--------------+
| 可预测 | 基于上一个区块哈希生成 | - |
+---------+---------------------+--------------+
| 不可预测 | VRF 可验证随机数 | math.random |
+---------+----------------------+--------------+
不同的使用场景对随机数有不同的需求。例如,在initPet
中,我们希望生成一个宠物类型,但这个随机数没有验证公平性的需求,所以我们可以直接使用 lua 自带的 math.random()
函数。
如果需要随机数可验证的话,会使用 AO 自带的 crypto.cipher.issac.getRandom()
,这个在本系列的后续中再予以阐述。
在 Handler 中,msg 默认携带时间戳,我们把 msg 中的时间戳作为变量传入函数即可。
getPet
& getPets
getPet
,通过 Address 作为 Key 来获取 Pet。getPets
,获取所有 Pets
-- Function to get a pet by address
local function getPet(address)
local stmt = DB:prepare [[
SELECT * FROM pets WHERE address = :address;
]]
if not stmt then
error("Failed to prepare SQL statement: " .. DB:errmsg())
end
stmt:bind_names({ address = address })
local rows = query(stmt)
return rows
end
-- Function to get all pets
local function getAllPets()
local stmt = DB:prepare [[
SELECT * FROM pets;
]]
if not stmt then
error("Failed to prepare SQL statement: " .. DB:errmsg())
end
local rows = query(stmt)
return rows
end
...
-- Add getPet Handler
Handlers.add(
"getPet",
Handlers.utils.hasMatchingTag("Action", "getPet"),
function (msg)
local pet = getPet(msg.Address)
local petsJson = json.encode(pet)
print(pet)
Handlers.utils.reply(petsJson)(msg)
end
)
-- Add getPets Handler to get all pets
Handlers.add(
"getPets",
Handlers.utils.hasMatchingTag("Action", "getPets"),
function (msg)
local pets = getAllPets()
print(pets)
local petsJson = json.encode(pets)
Handlers.utils.reply(petsJson)(msg)
end
)
我们通过json.encode
将 pets 转换为 string 进行回复,主要要在文件头引入 json package -- local json = require("json")
。
UpdateLevel
-- Function to update the level of a pet by petid
local function updatePetLevel(pet, timestampNow)
local currentPetStmt = DB:prepare [[
SELECT * FROM pets WHERE id = :id;
]]
if not currentPetStmt then
error("Failed to prepare SQL statement: " .. DB:errmsg())
end
currentPetStmt:bind_names({ id = pet.id })
local currentPet = query(currentPetStmt)[1]
if currentPet then
print(timestampNow)
print(currentPet.lastUpdated)
if timestampNow - currentPet.lastUpdated < 3600 then
-- 3600 seconds = 1 hour
print('Not now')
Handlers.utils.reply("Not now")(pet)
return
end
local newLevel = currentPet.level + 1
local stmt = DB:prepare [[
UPDATE pets SET level = :level, lastUpdated = :lastUpdated WHERE id = :id;
]]
if not stmt then
error("Failed to prepare SQL statement: " .. DB:errmsg())
end
stmt:bind_names({
id = pet.id,
level = newLevel,
lastUpdated = timestampNow
})
stmt:step()
stmt:reset()
print('Pet Level Updated!')
Handlers.utils.reply("Updated")(pet)
else
print('Pet not updated. New level must be higher than the current level.')
end
end
...
-- Add updateLevel Handler
Handlers.add(
"updateLevel",
Handlers.utils.hasMatchingTag("Action", "updateLevel"),
function (msg)
local pet = getPet(msg.Address)[1]
if pet then
updatePetLevel(pet, msg.Timestamp)
else
Handlers.utils.reply("Pet not found!")(msg)
end
end
)
在updatePetLevel
函数中,我们设置了基于时间的升级机制,升级后会需要一个小时的冷却时间。
initPet
aos> Send({Target = ao.id, Action = "initPet", Name = "apet", Description = "dog", Address = "0x1"})
getPet
aos> Send({Target = ao.id, Action = "getPet", Address = "0x1"})
getPets
aos> Send({Target = ao.id, Action = "getPets"})
updateLevel
aos> Send({Target = ao.id, Action = "updateLevel", Address = "0x1"})
本篇阐述一个额外的重要话题,如何利用 GPT 来辅助编写代码?
我们来一个一句话的极简指南:
💡把已有的代码或模板放在 Prompt 的最前面,然后再问问题 ——
抽象来看就是:
{{Code Existed}}
{{Question}}
或
{{Code Existed with Some TODO}}
以 .lua
的编写为例,我们将 https://github.com/iamgamelover/ao-twitter 作为起始模板用 AI 进行代码编写。
Prompt:
local json = require("json")local sqlite3 = require("lsqlite3")DB = DB or sqlite3.open_memory()DB:exec [[ CREATE TABLE IF NOT EXISTS stories ( id TEXT PRIMARY KEY, address TEXT, post TEXT, range TEXT, category TEXT, likes INT, replies INT, coins INT, time INT );]]DB:exec [[ CREATE TABLE IF NOT EXISTS replies ( id TEXT PRIMARY KEY, post_id TEXT, address TEXT, post TEXT, likes INT, replies INT, coins INT, time INT );]]DB:exec [[ CREATE TABLE IF NOT EXISTS likes ( id TEXT, address TEXT, time INT );]]DB:exec [[ CREATE TABLE IF NOT EXISTS txids ( id TEXT PRIMARY KEY, txid TEXT );]]Give me the code about ao-pets.lua, which includes follow fields: name, description, lastUpdated, level, type, address which is unique and id which is increased automatically.
Answer:
Here's an example of the ao-pets.lua script that includes the specified fields (name, description, lastUpdated, level, type, address, id which is unique and automatically incremented):
local sqlite3 = require("lsqlite3") local json = require("json") DB = DB or sqlite3.open_memory() -- Create the ao_pets table DB:exec [[ CREATE TABLE IF NOT EXISTS ao_pets ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, lastUpdated INT, level INT, type TEXT, address TEXT UNIQUE ); ]] -- Function to add a new pet local function add_pet(pet) local stmt = DB:prepare [[ INSERT INTO ao_pets (name, description, lastUpdated, level, type, address) VALUES (?, ?, ?, ?, ?, ?) ]] stmt:bind_values(pet.name, pet.description, pet.lastUpdated, pet.level, pet.type, pet.address) stmt:step() stmt:finalize() end -- Function to get all pets local function get_all_pets() for row in DB:nrows("SELECT * FROM ao_pets") do print(json.encode(row)) end end -- Example usage local new_pet = { name = "Fluffy", description = "A very fluffy cat", lastUpdated = os.time(), level = 1, type = "Cat", address = "unique_address_123" } add_pet(new_pet) get_all_pets()
In this script:
- The
ao_pets
table is created with fieldsid
,name
,description
,lastUpdated
,level
,type
, andaddress
.- The
id
field is set as the primary key and will automatically increment.- The
address
field is unique.- The
add_pet
function inserts a new pet into theao_pets
table.- The
get_all_pets
function retrieves and prints all pets in the table.You can modify the script further based on your specific needs.
Prompt:
Handlers.add( "AddNew", Handlers.utils.hasMatchingTag("Action", "AddNew"), function (msg) table.insert(Players, msg.Data) count = #Players Handlers.utils.reply("bizz buzz")(msg) end)
Here is a handler example, impl Action "GetAllPets".
Answer:
Here's an example of a handler for the Action "GetAllPets" that retrieves all pets from the ao_pets table and sends them back as a response:
Handlers.add( "GetAllPets", Handlers.utils.hasMatchingTag("Action", "GetAllPets"), function (msg) local pets = {} for row in DB:nrows("SELECT * FROM ao_pets") do table.insert(pets, { id = row.id, name = row.name, description = row.description, lastUpdated = row.lastUpdated, level = row.level, type = row.type, address = row.address }) end local response = { Action = "GetAllPetsResponse", Data = pets } Handlers.utils.reply(response)(msg) end )
In this handler:
- The
GetAllPets
action is defined and will be triggered when a message with theAction
"GetAllPets" is received.- The handler function retrieves all rows from the
ao_pets
table and stores them in apets
table.- A response table is created with the
Action
"GetAllPetsResponse" and the retrieved pets data.- The response is sent back using
Handlers.utils.reply
.This will ensure that when the "GetAllPets" action is received, the handler will retrieve all pets from the database and send them back in the response.
关于 PermaDAO:Website | Twitter | Telegram | Discord| Medium | Youtube
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!