Bots And Games
竞技场机制
本指南全面概述了在 `aos` 中设计和管理竞技场风格游戏所必需的基本机制
在竞技场游戏中,参与者进行回合比赛,有策略地相互竞争以消灭对方,直到出现唯一的胜利者。
这里介绍的框架为制作各种游戏奠定了基础,所有游戏都共享相同的核心功能。 探索游戏开发的复杂性,并在这个多功能的舞台上释放您的创造力。
核心功能
现在,让我们深入了解竞技场风格游戏的核心功能:
- 游戏进展模式:
竞技场游戏被打造为循环运行的回合,具有以下进展模式:"Not-Started"
→"Waiting"
→"Playing"
→[Someone wins or timeout]
→"Waiting"
...注意:如果等待状态后没有足够的玩家开始游戏,则循环超时。
回合为玩家提供了明确的参与时间范围,从而增强了游戏的刺激性。 - 代币质押:
玩家必须存入指定数量的代币(由PaymentQty
定义)才能参与游戏。 这些代币为游戏添加了有形的赌注元素。 - 奖金奖励:
除了胜利的兴奋之外,玩家还被额外奖励的前景所吸引。 构建者可以灵活地提供由BonusQty
定义的奖励代币,每轮分配。 玩家所下的任何赌注也会添加到这些奖金中。 这些奖金作为额外的激励,增强了游戏的竞争精神。 - 玩家管理:
- 等待加入下一场比赛的玩家会在
Waiting
表中进行跟踪。 - 正在比赛的玩家及其游戏状态存储在
Players
表中。 - 被淘汰的玩家会立即从
Players
表中删除,并放入Waiting
表中进行下一场比赛。
- 等待加入下一场比赛的玩家会在
- 每轮获胜者奖励:
当一个玩家淘汰另一个玩家时,他们不仅可以获得吹牛的权利,还可以获得被淘汰玩家的质押代币作为奖励。 此外,每轮的获胜者都会分享一部分奖金代币以及他们的原始质押的代币,进一步激励玩家争取胜利。 - 监听器模式:
对于那些喜欢观看行动展开的人来说,Listen
模式提供了一个无需实际参与即可了解情况的机会。 进程可以注册为侦听器,授予它们访问游戏中所有公告的权限。 虽然他们不作为玩家参与,但听众可以继续观察游戏的进度,除非他们明确要求删除。 - 游戏状态管理:
为了维持竞技场游戏的流畅性和公平性,自动化系统会监督游戏状态的转换。 这些转换包括等待、游戏中和结束阶段。 每个状态的持续时间(例如WaitTime
和GameTime
)可确保回合遵守定义的时间范围,从而防止游戏无限期地持续。
您可以在下面参考竞技场的代码:
-- ARENA GAME BLUEPRINT.
-- This blueprint provides the framework to operate an 'arena' style game
-- inside an ao process. Games are played in rounds, where players aim to
-- eliminate one another until only one remains, or until the game time
-- has elapsed. The game process will play rounds indefinitely as players join
-- and leave.
-- When a player eliminates another, they receive the eliminated player's deposit token
-- as a reward. Additionally, the builder can provide a bonus of these tokens
-- to be distributed per round as an additional incentive. If the intended
-- player type in the game is a bot, providing an additional 'bonus'
-- creates an opportunity for coders to 'mine' the process's
-- tokens by competing to produce the best agent.
-- The builder can also provide other handlers that allow players to perform
-- actions in the game, calling 'eliminatePlayer()' at the appropriate moment
-- in their game logic to control the framework.
-- Processes can also register in a 'Listen' mode, where they will receive
-- all announcements from the game, but are not considered for entry into the
-- rounds themselves. They are also not unregistered unless they explicitly ask
-- to be.
-- GLOBAL VARIABLES.
-- Game progression modes in a loop:
-- [Not-Started] -> Waiting -> Playing -> [Someone wins or timeout] -> Waiting...
-- The loop is broken if there are not enough players to start a game after the waiting state.
GameMode = GameMode or "Not-Started"
StateChangeTime = StateChangeTime or undefined
-- State durations (in milliseconds)
WaitTime = WaitTime or 2 * 60 * 1000 -- 2 minutes
GameTime = GameTime or 20 * 60 * 1000 -- 20 minutes
Now = Now or undefined -- Current time, updated on every message.
-- Token information for player stakes.
UNIT = 1000
PaymentToken = PaymentToken or "ADDR" -- Token address
PaymentQty = PaymentQty or tostring(math.floor(UNIT)) -- Quantity of tokens for registration
BonusQty = BonusQty or tostring(math.floor(UNIT)) -- Bonus token quantity for winners
-- Players waiting to join the next game and their payment status.
Waiting = Waiting or {}
-- Active players and their game states.
Players = Players or {}
-- Number of winners in the current game.
Winners = 0
-- Processes subscribed to game announcements.
Listeners = Listeners or {}
-- Minimum number of players required to start a game.
MinimumPlayers = MinimumPlayers or 2
-- Default player state initialization.
PlayerInitState = PlayerInitState or {}
-- Sends a state change announcement to all registered listeners.
-- @param event: The event type or name.
-- @param description: Description of the event.
function announce(event, description)
for ix, address in pairs(Listeners) do
ao.send({
Target = address,
Action = "Announcement",
Event = event,
Data = description
})
end
return print(Colors.gray .. "Announcement: " .. Colors.red .. event .. " " .. Colors.blue .. description .. Colors.reset)
end
-- Sends a reward to a player.
-- @param recipient: The player receiving the reward.
-- @param qty: The quantity of the reward.
-- @param reason: The reason for the reward.
function sendReward(recipient, qty, reason)
if type(qty) ~= number then
qty = tonumber(qty)
end
ao.send({
Target = PaymentToken,
Action = "Transfer",
Quantity = tostring(qty),
Recipient = recipient,
Reason = reason
})
return print(Colors.gray .. "Sent Reward: " ..
Colors.blue .. tostring(qty) ..
Colors.gray .. ' tokens to ' ..
Colors.green .. recipient .. " " ..
Colors.blue .. reason .. Colors.reset
)
end
-- Starts the waiting period for players to become ready to play.
function startWaitingPeriod()
GameMode = "Waiting"
StateChangeTime = Now + WaitTime
announce("Started-Waiting-Period", "The game is about to begin! Send your token to take part.")
print('Starting Waiting Period')
end
-- Starts the game if there are enough players.
function startGamePeriod()
local paidPlayers = 0
for player, hasPaid in pairs(Waiting) do
if hasPaid then
paidPlayers = paidPlayers + 1
end
end
if paidPlayers < MinimumPlayers then
announce("Not-Enough-Players", "Not enough players registered! Restarting...")
for player, hasPaid in pairs(Waiting) do
if hasPaid then
Waiting[player] = false
sendReward(player, PaymentQty, "Refund")
end
end
startWaitingPeriod()
return
end
LastTick = undefined
GameMode = "Playing"
StateChangeTime = Now + GameTime
for player, hasPaid in pairs(Waiting) do
if hasPaid then
Players[player] = playerInitState()
else
ao.send({
Target = player,
Action = "Ejected",
Reason = "Did-Not-Pay"
})
removeListener(player) -- Removing player from listener if they didn't pay
end
end
announce("Started-Game", "The game has started. Good luck!")
print("Game Started....")
end
-- Handles the elimination of a player from the game.
-- @param eliminated: The player to be eliminated.
-- @param eliminator: The player causing the elimination.
function eliminatePlayer(eliminated, eliminator)
sendReward(eliminator, PaymentQty, "Eliminated-Player")
Waiting[eliminated] = false
Players[eliminated] = nil
ao.send({
Target = eliminated,
Action = "Eliminated",
Eliminator = eliminator
})
announce("Player-Eliminated", eliminated .. " was eliminated by " .. eliminator .. "!")
local playerCount = 0
for player, _ in pairs(Players) do
playerCount = playerCount + 1
end
print("Eliminating player: " .. eliminated .. " by: " .. eliminator) -- Useful for tracking eliminations
if playerCount < MinimumPlayers then
endGame()
end
end
-- Ends the current game and starts a new one.
function endGame()
print("Game Over")
Winners = 0
Winnings = tonumber(BonusQty) / Winners -- Calculating winnings per player
for player, _ in pairs(Players) do
Winners = Winners + 1
end
Winnings = tonumber(BonusQty) / Winners
for player, _ in pairs(Players) do
-- addLog("EndGame", "Sending reward of:".. Winnings + PaymentQty .. "to player: " .. player) -- Useful for tracking rewards
sendReward(player, Winnings + tonumber(PaymentQty), "Win")
Waiting[player] = false
end
Players = {}
announce("Game-Ended", "Congratulations! The game has ended. Remaining players at conclusion: " .. Winners .. ".")
startWaitingPeriod()
end
-- Removes a listener from the listeners' list.
-- @param listener: The listener to be removed.
function removeListener(listener)
local idx = 0
for i, v in ipairs(Listeners) do
if v == listener then
idx = i
break
end
end
if idx > 0 then
table.remove(Listeners, idx)
end
end
-- HANDLERS: Game state management
-- Handler for cron messages, manages game state transitions.
Handlers.add(
"Game-State-Timers",
function(Msg)
return "continue"
end,
function(Msg)
Now = Msg.Timestamp
if GameMode == "Not-Started" then
startWaitingPeriod()
elseif GameMode == "Waiting" then
if Now > StateChangeTime then
startGamePeriod()
end
elseif GameMode == "Playing" then
if onTick and type(onTick) == "function" then
onTick()
end
if Now > StateChangeTime then
endGame()
end
end
end
)
-- Handler for player deposits to participate in the next game.
Handlers.add(
"Transfer",
function(Msg)
return
Msg.Action == "Credit-Notice" and
Msg.From == PaymentToken and
tonumber(Msg.Quantity) >= tonumber(PaymentQty) and "continue"
end,
function(Msg)
Waiting[Msg.Sender] = true
ao.send({
Target = Msg.Sender,
Action = "Payment-Received"
})
announce("Player-Ready", Msg.Sender .. " is ready to play!")
end
)
-- Registers new players for the next game and subscribes them for event info.
Handlers.add(
"Register",
Handlers.utils.hasMatchingTag("Action", "Register"),
function(Msg)
if Msg.Mode ~= "Listen" and Waiting[Msg.From] == undefined then
Waiting[Msg.From] = false
end
removeListener(Msg.From)
table.insert(Listeners, Msg.From)
ao.send({
Target = Msg.From,
Action = "Registered"
})
announce("New Player Registered", Msg.From .. " has joined in waiting.")
end
)
-- Unregisters players and stops sending them event info.
Handlers.add(
"Unregister",
Handlers.utils.hasMatchingTag("Action", "Unregister"),
function(Msg)
removeListener(Msg.From)
ao.send({
Target = Msg.From,
Action = "Unregistered"
})
end
)
-- Adds bet amount to BonusQty
Handlers.add(
"AddBet",
Handlers.utils.hasMatchingTag("Reason", "AddBet"),
function(Msg)
BonusQty = tonumber(BonusQty) + tonumber(Msg.Tags.Quantity)
announce("Bet-Added", Msg.From .. "has placed a bet. " .. "BonusQty amount increased by " .. Msg.Tags.Quantity .. "!")
end
)
-- Retrieves the current game state.
Handlers.add(
"GetGameState",
Handlers.utils.hasMatchingTag("Action", "GetGameState"),
function (Msg)
local json = require("json")
local TimeRemaining = StateChangeTime - Now
local GameState = json.encode({
GameMode = GameMode,
TimeRemaining = TimeRemaining,
Players = Players,
})
ao.send({
Target = Msg.From,
Action = "GameState",
Data = GameState})
end
)
-- Alerts users regarding the time remaining in each game state.
Handlers.add(
"AnnounceTick",
Handlers.utils.hasMatchingTag("Action", "Tick"),
function (Msg)
local TimeRemaining = StateChangeTime - Now
if GameMode == "Waiting" then
announce("Tick", "The game will start in " .. (TimeRemaining/1000) .. " seconds.")
elseif GameMode == "Playing" then
announce("Tick", "The game will end in " .. (TimeRemaining/1000) .. " seconds.")
end
end
)
-- Sends tokens to players with no balance upon request
Handlers.add(
"RequestTokens",
Handlers.utils.hasMatchingTag("Action", "RequestTokens"),
function (Msg)
print("Transfering Tokens: " .. tostring(math.floor(10000 * UNIT)))
ao.send({
Target = ao.id,
Action = "Transfer",
Quantity = tostring(math.floor(10000 * UNIT)),
Recipient = Msg.From,
})
end
)
竞技场游戏蓝图
对于那些有兴趣使用此 arena 框架的人,我们已通过蓝图轻松访问此代码。 只需在终端中运行以下代码:
.load-blueprint arena
总结
了解竞技场的机制不仅可以帮助您改进上一节中创建的自主代理,还可以让您利用核心功能来打造独特的游戏。
在接下来的 Building a Game
部分中,我们将深入探讨利用这些机制在此框架内构建迷人且独一无二的游戏的艺术。 准备好踏上游戏开发动态领域的旅程吧! 🎮