Created
February 9, 2024 16:58
-
-
Save ropats16/bcbdc0e3c1d3617de823e0a02b227211 to your computer and use it in GitHub Desktop.
Arena Debugging
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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