Skip to content

Instantly share code, notes, and snippets.

@ropats16
Created February 9, 2024 16:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ropats16/bcbdc0e3c1d3617de823e0a02b227211 to your computer and use it in GitHub Desktop.
Save ropats16/bcbdc0e3c1d3617de823e0a02b227211 to your computer and use it in GitHub Desktop.
Arena Debugging
-- Initializing global variables to store the latest game state and game host process.
LatestGameState = LatestGameState or nil
Game = Game or nil
InAction = InAction or false
Logs = Logs or {}
colors = {
red = "\27[31m",
green = "\27[32m",
blue = "\27[34m",
reset = "\27[0m",
gray = "\27[90m"
}
function addLog(msg, text) -- Function definition commented for performance, can be used for debugging
Logs[msg] = Logs[msg] or {}
table.insert(Logs[msg], text)
end
-- Checks if two points are within a given range.
-- @param x1, y1: Coordinates of the first point.
-- @param x2, y2: Coordinates of the second point.
-- @param range: The maximum allowed distance between the points.
-- @return: Boolean indicating if the points are within the specified range.
function inRange(x1, y1, x2, y2, range)
return math.abs(x1 - x2) <= range and math.abs(y1 - y2) <= range
end
-- Decides the next action based on player proximity and energy.
-- If any player is within range, it initiates an attack; otherwise, moves randomly.
function decideNextAction()
local player = LatestGameState.Players[ao.id]
local targetInRange = false
for target, state in pairs(LatestGameState.Players) do
if target ~= ao.id and inRange(player.x, player.y, state.x, state.y, 1) then
targetInRange = true
break
end
end
if player.energy > 5 and targetInRange then
print(colors.red .. "Player in range. Attacking." .. colors.reset)
ao.send({Target = Game, Action = "PlayerAttack", AttackEnergy = tostring(player.energy)})
else
print(colors.red .. "No player in range or insufficient energy. Moving randomly." .. colors.reset)
local directionMap = {"Up", "Down", "Left", "Right", "UpRight", "UpLeft", "DownRight", "DownLeft"}
local randomIndex = math.random(#directionMap)
ao.send({Target = Game, Action = "PlayerMove", Direction = directionMap[randomIndex]})
end
InAction = false
end
-- Handler to print game announcements and trigger game state updates.
Handlers.add(
"PrintAnnouncements",
Handlers.utils.hasMatchingTag("Action", "Announcement"),
function (msg)
if msg.Event == "Started-Waiting-Period" then
ao.send({Target = ao.id, Action = "AutoPay"})
elseif (msg.Event == "Tick" or msg.Event == "Started-Game") and not InAction then
InAction = true
-- print("Getting game state...")
ao.send({Target = Game, Action = "GetGameState"})
elseif InAction then
print("Previous action still in progress. Skipping.")
end
print(colors.green .. msg.Event .. ": " .. msg.Data .. colors.reset)
end
)
-- Handler to trigger game state updates.
Handlers.add(
"GetGameStateOnTick",
Handlers.utils.hasMatchingTag("Action", "Tick"),
function ()
if not InAction then
InAction = true
print(colors.gray .. "Getting game state..." .. colors.reset)
ao.send({Target = Game, Action = "GetGameState"})
else
print("Previous action still in progress. Skipping.")
end
end
)
-- Handler to automate payment confirmation when waiting period starts.
Handlers.add(
"AutoPay",
Handlers.utils.hasMatchingTag("Action", "AutoPay"),
function (msg)
print("Auto-paying confirmation fees.")
ao.send({ Target = Game, Action = "Transfer", Recipient = Game, Quantity = "1"})
end
)
-- Handler to update the game state upon receiving game state information.
Handlers.add(
"UpdateGameState",
Handlers.utils.hasMatchingTag("Action", "GameState"),
function (msg)
local json = require("json")
LatestGameState = json.decode(msg.Data)
ao.send({Target = ao.id, Action = "UpdatedGameState"})
print("Game state updated. Print \'LatestGameState\' for detailed view.")
end
)
-- Handler to decide the next best action.
Handlers.add(
"decideNextAction",
Handlers.utils.hasMatchingTag("Action", "UpdatedGameState"),
function ()
if LatestGameState.GameMode ~= "Playing" then
InAction = false
return
end
print("Deciding next action.")
decideNextAction()
ao.send({Target = ao.id, Action = "Tick"})
end
)
-- Handler to automatically attack when hit by another player.
Handlers.add(
"ReturnAttack",
Handlers.utils.hasMatchingTag("Action", "Hit"),
function (msg)
if not InAction then
InAction = true
local playerEnergy = LatestGameState.Players[ao.id].energy
if playerEnergy == undefined then
print(colors.red .. "Unable to read energy." .. colors.reset)
ao.send({Target = Game, Action = "Attack-Failed", Reason = "Unable to read energy."})
elseif playerEnergy == 0 then
print(colors.red .. "Player has insufficient energy." .. colors.reset)
ao.send({Target = Game, Action = "Attack-Failed", Reason = "Player has no energy."})
else
print(colors.red .. "Returning attack." .. colors.reset)
ao.send({Target = Game, Action = "PlayerAttack", AttackEnergy = tostring(playerEnergy)})
end
InAction = false
ao.send({Target = ao.id, Action = "Tick"})
else
print("Previous action still in progress. Skipping.")
end
end
)
-- AO EFFECT: Game Mechanics for AO Arena Game
-- Game grid dimensions
Width = 40 -- Width of the grid
Height = 40 -- Height of the grid
Range = 1 -- The distance for blast effect
-- Player energy settings
MaxEnergy = 100 -- Maximum energy a player can have
EnergyPerSec = 1 -- Energy gained per second
-- Attack settings
AverageMaxStrengthHitsToKill = 3 -- Average number of hits to eliminate a player
-- Initializes default player state
-- @return Table representing player's initial state
function playerInitState()
return {
x = math.random(0, Width/8),
y = math.random(0, Height/8),
health = 100,
energy = 0
}
end
-- Function to incrementally increase player's energy
-- Called periodically to update player energy
function onTick()
if GameMode ~= "Playing" then return end -- Only active during "Playing" state
if LastTick == undefined then LastTick = Now end
local Elapsed = Now - LastTick
if Elapsed >= 1000 then -- Actions performed every second
for player, state in pairs(Players) do
local newEnergy = math.floor(math.min(MaxEnergy, state.energy + (Elapsed * EnergyPerSec // 2000)))
state.energy = newEnergy
end
LastTick = Now
end
end
-- Handles player movement
-- @param msg: Message request sent by player with movement direction and player info
function move(msg)
local playerToMove = msg.From
local direction = msg.Tags.Direction
local directionMap = {
Up = {x = 0, y = -1}, Down = {x = 0, y = 1},
Left = {x = -1, y = 0}, Right = {x = 1, y = 0},
UpRight = {x = 1, y = -1}, UpLeft = {x = -1, y = -1},
DownRight = {x = 1, y = 1}, DownLeft = {x = -1, y = 1}
}
-- calculate and update new coordinates
if directionMap[direction] then
local newX = Players[playerToMove].x + directionMap[direction].x
local newY = Players[playerToMove].y + directionMap[direction].y
-- updates player coordinates while checking for grid boundaries
Players[playerToMove].x = (newX - 1) % Width + 1
Players[playerToMove].y = (newY - 1) % Height + 1
announce("Player-Moved", playerToMove .. " moved to " .. Players[playerToMove].x .. "," .. Players[playerToMove].y .. ".")
else
ao.send({Target = playerToMove, Action = "Move-Failed", Reason = "Invalid direction."})
end
onTick() -- Optional: Update energy each move
end
-- Handles player attacks
-- @param msg: Message request sent by player with attack info and player state
function attack(msg)
local player = msg.From
local attackEnergy = tonumber(msg.Tags.AttackEnergy)
-- get player coordinates
local x = Players[player].x
local y = Players[player].y
-- check if player has enough energy to attack
if Players[player].energy < attackEnergy then
ao.send({Target = player, Action = "Attack-Failed", Reason = "Not enough energy."})
return
end
-- update player energy and calculate damage
Players[player].energy = Players[player].energy - attackEnergy
local damage = math.floor((math.random() * 2 * attackEnergy) * (1/AverageMaxStrengthHitsToKill))
announce("Attack", player .. " has launched a " .. damage .. " damage attack from " .. x .. "," .. y .. "!")
-- check if any player is within range and update their status
for target, state in pairs(Players) do
if target ~= player and inRange(x, y, state.x, state.y, Range) then
local newHealth = state.health - damage
if newHealth <= 0 then
eliminatePlayer(target, player)
else
Players[target].health = newHealth
ao.send({Target = target, Action = "Hit", Damage = tostring(damage), Health = tostring(newHealth)})
ao.send({Target = player, Action = "Successful-Hit", Recipient = target, Damage = tostring(damage), Health = tostring(newHealth)})
end
end
end
end
-- Helper function to check if a target is within range
-- @param x1, y1: Coordinates of the attacker
-- @param x2, y2: Coordinates of the potential target
-- @param range: Attack range
-- @return Boolean indicating if the target is within range
function inRange(x1, y1, x2, y2, range)
return x2 >= (x1 - range) and x2 <= (x1 + range) and y2 >= (y1 - range) and y2 <= (y1 + range)
end
-- HANDLERS: Game state management for AO-Effect
-- Handler for player movement
Handlers.add("PlayerMove", Handlers.utils.hasMatchingTag("Action", "PlayerMove"), move)
-- Handler for player attacks
Handlers.add("PlayerAttack", Handlers.utils.hasMatchingTag("Action", "PlayerAttack"), attack)
-- ARENA GAME BLUEPRINT.
-- REQUIREMENTS: cron must be added and activated for game operation.
-- 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.
PaymentToken = PaymentToken or "ADDR" -- Token address
PaymentQty = PaymentQty or 1 -- Quantity of tokens for registration
BonusQty = BonusQty or 1 -- 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 {}
-- Log storage for debugging.
Logs = Logs or {}
-- Functions for game and player management.
-- Adds logs for debugging purposes. Calls are currently commented out but can be activated for detailed debugging.
-- @param msg: The log category or identifier.
-- @param text: The log message.
function addLog(msg, text) -- Function definition commented for performance, can be used for debugging
Logs[msg] = Logs[msg] or {}
table.insert(Logs[msg], text)
end
-- 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
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)
ao.send({
Target = PaymentToken,
Action = "Transfer",
Quantity = tostring(qty),
Recipient = recipient,
Reason = reason
})
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.")
-- Logs cleared at the start of the waiting period.
-- Logs = {}
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
-- addLog("StartGamePeriod", "Paid players: " .. paidPlayers) -- Useful for debugging player count
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
announce("Started-Game", "The game has started. Good luck!")
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
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)
-- addLog("EliminatePlayer", "Eliminating player: " .. eliminated .. " by: " .. eliminator) -- Useful for tracking eliminations
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
if playerCount < MinimumPlayers then
endGame()
end
end
-- Ends the current game and starts a new one.
function endGame()
Winners = 0
Winnings = BonusQty / Winners -- Calculating winnings per player
for player, _ in pairs(Players) do
Winners = Winners + 1
end
Winnings = 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 + 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
-- addLog("removeListener", "Found listener: " .. listener .. " at index: " .. idx) -- Useful for tracking listener removal
break
end
end
if idx > 0 then
-- addLog("removeListener", "Removing listener: " .. listener .. " at index: " .. idx) -- Useful for tracking listener removal
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 true
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
onTick()
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) >= PaymentQty
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.Sender .. " 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 = 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)
ao.send({
Target = ao.id,
Action = "Transfer",
Quantity = "10000",
Recipient = Msg.From,
})
end
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment