Created
April 13, 2024 09:12
-
-
Save ropats16/14bcc161587e5d452dbb8948a69d6b55 to your computer and use it in GitHub Desktop.
Gist for GRID Game setup
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
-- CRED = "Sa0iBLPNyJQrwpTTG-tWLQU-1QeUAJA73DdxGGiKoJc" | |
Version = "0.3" | |
-- Attack info | |
LastPlayerAttacks = LastPlayerAttacks or {} | |
CurrentAttacks = CurrentAttacks or 0 | |
TenSecondCheck = TenSecondCheck or 0 | |
-- grid dimensions | |
Width = 16 | |
Height = 16 | |
Range = 3 | |
-- Player energy | |
MaxEnergy = 100 | |
EnergyPerSec = 1 | |
-- Attack settings | |
AverageMaxStrengthHitsToKill = 3 -- Average number of hits to eliminate a player | |
Requested = {} | |
GameBalances = {} | |
-- Initializes default player state | |
-- @return Table representing player's initial state | |
function playerInitState() | |
return { | |
x = math.random(0, Width), | |
y = math.random(0, Height), | |
health = 100, | |
energy = 0, | |
lastTurn = 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 | |
if TenSecondCheck == 0 then | |
TenSecondCheck = Now | |
end | |
local TenSecondElaspedCheck = Now - TenSecondCheck | |
if TenSecondElaspedCheck >= 10000 then | |
-- only keep the last 20 | |
while #LastPlayerAttacks > 20 do | |
table.remove(LastPlayerAttacks, 1) | |
end | |
TenSecondCheck = Now | |
end | |
end | |
local function isOccupied(x,y) | |
local result = false | |
for k,v in pairs(Players) do | |
if v.x == x and v.y == y then | |
result = true | |
return | |
end | |
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} | |
} | |
-- only 1 turn per second | |
if msg.Timestamp < Players[playerToMove].lastTurn + 1000 then | |
return | |
end | |
-- 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 | |
-- Player cant move to cell already occupied. | |
if isOccupied(newX, newY) then | |
Send({Target = playerToMove, Action = "Move-Failed", Reason = "Cell Occupied."}) | |
return | |
end | |
-- updates player coordinates while checking for grid boundaries | |
Players[playerToMove].x = (newX - 1) % Width + 1 | |
Players[playerToMove].y = (newY - 1) % Height + 1 | |
Send({Target = playerToMove, Action="Player-Moved", Data = playerToMove .. " moved to " .. Players[playerToMove].x .. "," .. Players[playerToMove].y .. "."}) | |
--announce("Player-Moved", playerToMove .. " moved to " .. Players[playerToMove].x .. "," .. Players[playerToMove].y .. ".") | |
else | |
Send({Target = playerToMove, Action = "Move-Failed", Reason = "Invalid direction."}) | |
end | |
Players[playerToMove].lastTurn = msg.Timestamp | |
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) < 0 and 0 or tonumber(msg.Tags.AttackEnergy) | |
-- get player coordinates | |
local x = Players[player].x | |
local y = Players[player].y | |
-- only 1 turn per second | |
if msg.Timestamp < Players[player].lastTurn + 1000 then | |
return | |
end | |
-- 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 | |
-- Document Current Attacks | |
CurrentAttacks = CurrentAttacks + 1 | |
LastPlayerAttacks[CurrentAttacks] = { | |
Player = player, | |
Target = target, | |
id = CurrentAttacks | |
} | |
if newHealth <= 0 then | |
eliminatePlayer(target, player) | |
else | |
Players[target].health = newHealth | |
Send({Target = target, Action = "Hit", Damage = tostring(damage), Health = tostring(newHealth)}) | |
Send({Target = player, Action = "Successful-Hit", Recipient = target, Damage = tostring(damage), Health = tostring(newHealth)}) | |
end | |
end | |
end | |
Players[player].lastTurn = msg.Timestamp | |
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 "Playing" | |
StateChangeTime = StateChangeTime or 0 | |
-- State durations (in milliseconds) | |
WaitTime = WaitTime or 2 * 60 * 1000 -- 2 minutes | |
GameTime = GameTime or 20 * 60 * 1000 -- 20 minutes | |
Now = Now or 0 -- Current time, updated on every message. | |
-- Token information for player stakes. | |
PaymentToken = PaymentToken or ao.id -- 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 1 | |
-- 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 | |
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 | |
-- 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 | |
-- 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, tonumber(GameBalances[eliminated]), "Eliminated-Player") | |
GameBalances[eliminated] = "0" | |
Players[eliminated] = nil | |
Send({ | |
Target = eliminated, | |
Action = "Eliminated", | |
Eliminator = eliminator | |
}) | |
removeListener(eliminated) | |
-- announce("Player-Eliminated", eliminated .. " was eliminated by " .. eliminator .. "!") | |
local playerCount = 0 | |
for player, _ in pairs(Players) do | |
playerCount = playerCount + 1 | |
end | |
end | |
function scaleNumber(oldValue) | |
local oldMin = 10 | |
local oldMax = 1000 | |
local newMin = 1 | |
local newMax = 100 | |
local newValue = (((oldValue - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin | |
return newValue | |
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 = tonumber(Msg.Timestamp) | |
onTick() | |
end | |
) | |
-- Handler for player deposits to participate in the next game. | |
Handlers.add( | |
"Transfer", | |
function(Msg) | |
return | |
Msg.Action == "Credit-Notice" and | |
-- Msg.From == CRED and | |
tonumber(Msg.Quantity) >= PaymentQty and "continue" | |
end, | |
function(Msg) | |
print("Player " .. Msg.Sender .. " has deposited " .. Msg.Quantity .. " " .. PaymentToken .. " to join the game.") | |
local q = tonumber(Msg.Quantity) | |
if not GameBalances[Msg.Sender] then | |
GameBalances[Msg.Sender] = "0" | |
end | |
local balance = tonumber(GameBalances[Msg.Sender]) | |
Players[Msg.Sender] = playerInitState() | |
balance = math.floor(balance + q) | |
GameBalances[Msg.Sender] = tostring(balance) | |
if balance <= 10 then | |
Players[Msg.Sender].health = 1 | |
elseif balance >= 1000 then | |
Players[Msg.Sender].health = 100 | |
else | |
Players[Msg.Sender].health = math.floor(scaleNumber(balance)) | |
end | |
Send({ | |
Target = Msg.Sender, | |
Action = "Payment-Received", | |
Data = "You are in the game." | |
}) | |
end | |
) | |
-- Exits the game receives CRED | |
Handlers.add( | |
"Withdraw", | |
Handlers.utils.hasMatchingTag("Action", "Withdraw"), | |
function(Msg) | |
Players[Msg.From] = nil | |
Send({Target = Game, Action = "Transfer", Quantity = GameBalances[Msg.From], Recipient = Msg.From }) | |
GameBalances[Msg.From] = "0" | |
removeListener(Msg.From) | |
Send({ | |
Target = Msg.From, | |
Action = "Removed from the Game" | |
}) | |
end | |
) | |
-- Retrieves the current game state. | |
Handlers.add( | |
"GetGameState", | |
Handlers.utils.hasMatchingTag("Action", "GetGameState"), | |
function (Msg) | |
if Players[Msg.From] and Msg.Name then | |
Players[Msg.From].name = Msg.Name | |
end | |
local json = require("json") | |
local GameState = json.encode({ | |
GameMode = GameMode, | |
Players = Players, | |
}) | |
Send({ | |
Target = Msg.From, | |
Action = "GameState", | |
Data = GameState | |
}) | |
end | |
) | |
-- Retrieves the current attacks that has been made in the game. | |
Handlers.add( | |
"GetGameAttacksInfo", | |
Handlers.utils.hasMatchingTag("Action", "GetGameAttacksInfo"), | |
function (Msg) | |
local GameAttacksInfo = require("json").encode({ | |
LastPlayerAttacks = Utils.values(LastPlayerAttacks) | |
}) | |
Send({ | |
Target = Msg.From, | |
Action = "GameAttacksInfo", | |
Data = GameAttacksInfo | |
}) | |
end | |
) | |
Handlers.add( | |
"RequestTokens", | |
Handlers.utils.hasMatchingTag("Action", "RequestTokens"), | |
function(Msg) | |
-- Check if Msg.From is already in the Requested array | |
if not Requested[Msg.From] then | |
-- Send tokens and add Msg.From to the Requested array | |
ao.send({Target = ao.id, Action = "Transfer", Quantity = "1000", Recipient = Msg.From }) | |
Requested[Msg.From] = true | |
else | |
-- Send action indicating that tokens are not claimable | |
ao.send({Target = Msg.From, Action = "TokensAlreadyClaimed"}) | |
end | |
end | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment