What about :movement_disallow() method?Molo_Parko wrote: ↑Sat Nov 25, 2023 9:14 pm AiTribesCityDefender Forces computer-controlled tribes to keep a unit in every city. If the only unit in a city moves out of the city, it is moved back into the city.
New features via Lua script
Re: New features via Lua script
-
- Hardened
- Posts: 177
- Joined: Fri Jul 02, 2021 4:00 pm
Re: New features via Lua script
^ I use Freeciv 2.6.4 which does not have that feature. It's only available from Freeciv 3.0 and later. I could add it as alternate method when Freeciv version is 3 or more though.
EDIT: Now that I think about it, that might hinder the computer-controlled tribe from choosing a better defender -- since the Warriors can't leave, they might not also build a Phalanx, Pikemen, Musketeer, or Rifleman unit to defend the city when those options are available. The way the current script handles it is to only prevent the -last- unit in a city from leaving, so if a Warriors is in the city, and a Musketeers is built, then the Warriors can leave or be disbanded without triggering the script since the city isn't empty because a better unit is in it.
EDIT: Now that I think about it, that might hinder the computer-controlled tribe from choosing a better defender -- since the Warriors can't leave, they might not also build a Phalanx, Pikemen, Musketeer, or Rifleman unit to defend the city when those options are available. The way the current script handles it is to only prevent the -last- unit in a city from leaving, so if a Warriors is in the city, and a Musketeers is built, then the Warriors can leave or be disbanded without triggering the script since the city isn't empty because a better unit is in it.
Last edited by Molo_Parko on Thu Nov 30, 2023 8:21 pm, edited 1 time in total.
-
- Hardened
- Posts: 177
- Joined: Fri Jul 02, 2021 4:00 pm
Re: New features via Lua script
RailMoveLimit limits land units to maximum 9 tile moves per turn -- even on Railroad.
Attached Scenario file "RandoMap" has only the RailMoveLimit Lua scripts for testing.
Code: Select all
--##############################################################################
function Lua_RailMoveLimitRefresh(turn, year)
-- Clear and re-create the global table of unit RailMoves
RailMoves=nil RailMoves={}
end
signal.connect("turn_started", "Lua_RailMoveLimitRefresh")
--##############################################################################
function Lua_RailMoveLimit(unit, src_tile, dst_tile)
-- All land units limited to maximum 9 tiles of movement on Railroad
if ( RailMoveLimit == nil ) then RailMoveLimit=true end
if not ( RailMoveLimit ) then return end
-- Prevents looping forever
if ( RailMoveLimitInProgress == nil ) then RailMoveLimitInProgress=false end
if ( RailMoveLimitInProgress ) then return end
RailMoveLimitInProgress=true
if ( debugLuaScripts == nil ) then debugLuaScripts=false end
if ( debugLuaScripts ) then log.error("%s","RailMoveLimit BEGIN") end
-- Freeciv 2.6.4 Lua can't determine whether a unit's class is land, sea, or air
if ( unit.utype.id == find.unit_type("Fighter").id )
or ( unit.utype.id == find.unit_type("Bomber").id )
or ( unit.utype.id == find.unit_type("Helicopter").id )
or ( unit.utype.id == find.unit_type("Stealth Fighter").id )
or ( unit.utype.id == find.unit_type("Stealth Bomber").id )
or ( unit.utype.id == find.unit_type("Cruise Missile").id )
or ( unit.utype.id == find.unit_type("Nuclear").id )
or ( unit.utype.id == find.unit_type("AWACS").id ) then
return
end
if ( RailMoves[(unit).id] == nil ) then RailMoves[(unit).id]=0 end
if ( (src_tile):has_extra("Railroad") )
and ( (dst_tile):has_extra("Railroad") ) then
-- Each rail-to-rail tile movement counts toward 9 total
RailMoves[(unit).id]=( RailMoves[(unit).id] + 1 )
-- Every 3rd rail-to-rail move should use 1 fragment of unit movement
if ( RailMoves[(unit).id] % 3 == 0 ) then
edit.unit_move(unit,dst_tile,1)
end
-- Upper limit
if ( RailMoves[(unit).id] >= 9 ) then
-- Remove all unit movement points
edit.unit_move(unit,dst_tile,99)
end
else
-- Every move counts toward 9 maximum moves on Railroad
RailMoves[(unit).id]=( RailMoves[(unit).id] + 1 )
end
if ( debugLuaScripts ) then log.error("%s","RailMoveLimit END") end
RailMoveLimitInProgress=false
end
signal.connect("unit_moved", "Lua_RailMoveLimit")
- Attachments
-
- RandoMap with RailMoveLimit ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
- (8.89 KiB) Downloaded 416 times
-
- Hardened
- Posts: 177
- Joined: Fri Jul 02, 2021 4:00 pm
Re: New features via Lua script
ExtrasUpkeep charges each player for tile extras within the radius of the player's cities. I'm still experimenting with the costs (in gold) per tile-extra-type. Here's the current list:
^ Updated 2023-12-04 -- Set math.floor to ensure that payments extracted are in whole units of gold, since decimals don't actually work!
One issue is that tiles that exist in more than one city's radius are charged against the player once per each city. Instead of specifying cost in gold, it might be interesting to specify it in terms of labor-hours, and then calculate gold per labor hour, while considering government type. So in Despotism, a job that could be done in 1 turn might take 3 turns in Democracy (due to 8 hour workdays) and also might cost 100x as much per labor hour...
Attached is a playable scenario file "RandoMap with ExtrasUpkeep only" for Freeciv 2.6.4 and later for testing the routine, and which contains only this script and no others.
-- Upkeep cost per tile extra
cityExtraCosts={
["Airbase"]=1,
["Buoy"]=.1,
["Fallout"]=0,
["Farmland"]=.3,
["Fortress"]=.6,
["Hut"]=0,
["Irrigation"]=.07,
["Mine"]=.09,
["Oil Well"]=.3,
["Pollution"]=0,
["Railroad"]=.5,
["River"]=0,
["Road"]=.05,
["Ruins"]=-.1
} -- Set fee rate per extra, per turn in list above, example Road=.1 (gold)
Code: Select all
--##############################################################################
function Lua_ExtrasUpkeep(turn, year)
if ( ExtrasUpkeep == nil ) then ExtrasUpkeep=true end
if not ( ExtrasUpkeep ) then return end
if ( debugLuaScripts == nil ) then debugLuaScripts=false end
if ( debugLuaScripts ) then log.error("%s","ExtrasUpkeep BEGIN") end
if ( _G.xSize == nil ) then
-- declare local variables
local i, topology, count, field, topoFields
topology=(server.setting.get("topology"))
_G.xSize=tonumber(server.setting.get("xsize"))
_G.ySize=tonumber(server.setting.get("ysize"))
_G.dispersion=(server.setting.get("dispersion"))
_G.citymindist=(server.setting.get("citymindist"))
_G.topo="classic"
_G.wrapX=false
_G.wrapY=false
topoFields={}
-- Split topology field (from scenario settings) to a table
local count=1
for field in string.gmatch(topology, "[^|]+") do
topoFields[count]=field
count=(count+1)
end
-- Evaluate topology settings
if ( #topoFields > 0 ) then
for i=1,#topoFields,1 do
if ( topoFields[i] == "WRAPX" ) then _G.wrapX=true
elseif ( topoFields[i] == "WRAPY" ) then _G.wrapY=true
elseif ( topoFields[i] == "ISO" ) then _G.topo="ISO"
elseif ( topoFields[i] == "HEX" ) then _G.topo="HEX" end
end
end
end
-- Declare global function
function _G.is_XY_map_location_valid(x,y)
if ( _G.wrapX and _G.wrapY )
or ( _G.wrapX and ( y >= 0 ) and ( y < _G.ySize ) )
or ( _G.wrapY and ( x >= 0 ) and ( x < _G.xSize ) )
or ( y >= 0 and y < _G.ySize and x >= 0 and x < _G.xSize ) then
return true
else
return false
end
end
local i, h, v, x, y, player, cityTile, city, tile, label, value, upkeepCostTotal, upkeepCost, payment, previousDebtBalance
local cityExtras={}
if ( _G.Debt == nil ) then _G.Debt={} end
-- Skip turn 0
if ( turn < 1 ) then
if ( debugLuaScripts ) then log.error("%s","\tExtrasUpkeep Not active yet END") end
return
end
-- Upkeep cost per tile extra
cityExtraCosts={
["Airbase"]=1,
["Buoy"]=.1,
["Fallout"]=0,
["Farmland"]=.3,
["Fortress"]=.6,
["Hut"]=0,
["Irrigation"]=.07,
["Mine"]=.05,
["Oil Well"]=.3,
["Pollution"]=0,
["Railroad"]=.03,
["River"]=0,
["Road"]=.01,
["Ruins"]=-.1
} -- Set fee rate per extra, per turn in list above, example Road=.1 (gold)
for player in players_iterate() do
-- Reset some variables for each player
previousDebtBalance=0
payment=0
upkeepCost=0
upkeepCostTotal=0
if ( _G.Debt[player] == nil ) then _G.Debt[player]=0 end
cityExtras={ ["Irrigation"]=0,["Mine"]=0,["Oil Well"]=0,["Pollution"]=0,["Hut"]=0,
["Farmland"]=0,["Fallout"]=0,["Fortress"]=0,["Airbase"]=0,["Buoy"]=0,["Ruins"]=0,
["Road"]=0,["Railroad"]=0,["River"]=0 }
for city in (player):cities_iterate() do
cityTile=(city).tile
tile=cityTile
x=(tile).x
y=(tile).y
-- Check 21 tiles in city radius for tile extras
h={-1,-0, 1,-2,-1, 1, 2, 0,-2, 2,-1, 1,-2,-1, 1, 2, 0,-1,-0, 1, 0}
v={-2,-2,-2,-1,-1,-1,-1,-1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 0}
for i=1,#h,1 do
if ( _G.is_XY_map_location_valid( ( h[i] + x ),( v[i] + y ) ) ) then
tile=find.tile( ( h[i] + x ),( v[i] + y ) )
for label, value in pairs(cityExtras) do
if ( (tile):has_extra(label) ) then
cityExtras[label]=( cityExtras[label] + 1 )
end
end
end
end
end
if ( (player):is_human() ) and ( _G.Debt[player] > 0 ) then
log.error("%s","\tCity tile extras upkeep expense for:" .. tostring(player) .. " gold=" .. (player):gold() .. " debt=" .. _G.Debt[player] )
end
for label, value in pairs(cityExtras) do
if ( cityExtras[label] > 0 ) then
upkeepCost=( (cityExtras[label]) * cityExtraCosts[label] )
if ( (player):is_human() ) then
log.error("%s","\t" .. label .. "\tqty:" .. cityExtras[label] .. " @ " .. cityExtraCosts[label] .. "=" .. upkeepCost )
end
upkeepCostTotal=( upkeepCostTotal + upkeepCost )
end
end
-- Add the cost to the player's account
previousDebtBalance=_G.Debt[player]
_G.Debt[player]=( upkeepCostTotal + _G.Debt[player] )
-- If player has gold and debt, deduct gold for debt payment
if ( (player):gold() > 1 ) and ( _G.Debt[player] > 1 ) then
if ( (player):gold() > _G.Debt[player] ) then
payment=math.floor( _G.Debt[player] )
else
payment=math.floor( (player):gold() )
end
_G.Debt[player]=( _G.Debt[player] - payment )
-- the next command uses "change by" amount rather than "change to" amount
edit.change_gold( player, -payment )
end
if ( (player):is_human() ) and ( _G.Debt[player] > 0 ) then
message="\n\tINVOICE - Turn " .. turn .. "- Tile extra upkeep costs for cities owned by: " .. tostring(player) .. " gold=" .. (player):gold() .. " debt=" .. _G.Debt[player] .. "\n\t\tPrior balance:\t\t\t" .. previousDebtBalance .. "\n\t\tNew charges:\t\t\t+" .. upkeepCostTotal .. "\n\t\tPayment extracted:\t-" .. payment .. "\n\t\tNew balance:\t\t\t" .. _G.Debt[player] .. " gold owed.\n"
log.error("%s",message)
--notify.event(player, nil, E.SCRIPT,message.."\n\tItemization available in Chat pane.")
end
end
if ( debugLuaScripts ) then log.error("%s","\tExtrasUpkeep END") end
end
signal.connect("turn_started", "Lua_ExtrasUpkeep")
One issue is that tiles that exist in more than one city's radius are charged against the player once per each city. Instead of specifying cost in gold, it might be interesting to specify it in terms of labor-hours, and then calculate gold per labor hour, while considering government type. So in Despotism, a job that could be done in 1 turn might take 3 turns in Democracy (due to 8 hour workdays) and also might cost 100x as much per labor hour...
Attached is a playable scenario file "RandoMap with ExtrasUpkeep only" for Freeciv 2.6.4 and later for testing the routine, and which contains only this script and no others.
- Attachments
-
- RandoMap with ExtrasUpkeep ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
- (11.24 KiB) Downloaded 436 times
Last edited by Molo_Parko on Mon Dec 04, 2023 3:48 pm, edited 5 times in total.
-
- Hardened
- Posts: 177
- Joined: Fri Jul 02, 2021 4:00 pm
Re: New features via Lua script
DecayingExtras -- extras on tiles which are not currently within any city radius decay over time. This is a companion script to "ExtrasUpkeep" which charges for upkeep of extras within any city radius.
^ Updated 2023-12-04
The attached playable scenario for Freeciv 2.6.4 or later "RandoMap with DecayingExtras only" has only this script for testing. To test, start the scenario game and click "Turn done" repeatedly while watching areas of the map outside of any city radius.
Code: Select all
--##############################################################################
function Lua_DecayingExtras(turn, year)
if ( DecayingExtras == nil ) then DecayingExtras=true end
if not ( DecayingExtras ) then return end
if ( debugLuaScripts == nil ) then debugLuaScripts=false end
if ( debugLuaScripts ) then log.error("%s","DecayingExtras BEGIN") end
if ( turn == 0 ) and ( _G.xSize == nil ) then
-- declare local variables
local i, topology, count, field, topoFields
topology=(server.setting.get("topology"))
_G.xSize=tonumber(server.setting.get("xsize"))
_G.ySize=tonumber(server.setting.get("ysize"))
_G.dispersion=(server.setting.get("dispersion"))
_G.citymindist=(server.setting.get("citymindist"))
_G.topo="classic"
_G.wrapX=false
_G.wrapY=false
topoFields={}
-- Split topology field (from scenario settings) to a table
local count=1
for field in string.gmatch(topology, "[^|]+") do
topoFields[count]=field
count=(count+1)
end
-- Evaluate topology settings
if ( #topoFields > 0 ) then
for i=1,#topoFields,1 do
if ( topoFields[i] == "WRAPX" ) then _G.wrapX=true
elseif ( topoFields[i] == "WRAPY" ) then _G.wrapY=true
elseif ( topoFields[i] == "ISO" ) then _G.topo="ISO"
elseif ( topoFields[i] == "HEX" ) then _G.topo="HEX" end
end
end
end
-- Declare global function
function _G.is_XY_map_location_valid(x,y)
if ( _G.wrapX and _G.wrapY )
or ( _G.wrapX and ( y >= 0 ) and ( y < _G.ySize ) )
or ( _G.wrapY and ( x >= 0 ) and ( x < _G.xSize ) )
or ( y >= 0 and y < _G.ySize and x >= 0 and x < _G.xSize ) then
return true
else
return false
end
end
-- declare local variables
local i, j, h, v, x, y, ib, jb, tile
-- The order matters. Railroad before road prevents leaving a tile with an Extra missing a prerequisite
local decayingExtras={"Railroad", "Road", "Farmland", "Irrigation", "Mine", "Oil Well", "Airbase", "Buoy", "Fortress"}
-- Scenario setting "borders" defaults to Enabled (in some Fc version?)
-- Possible values are nil, SEE_INSIDE, EXPAND, ENABLED, DISABLED
_G.Borders=server.setting.get("borders")
if ( _G.Borders == nil ) then
_G.Borders="ENABLED"
end
-- Select a random tile within each 5x5 area of the map (non-overlapping)
for ib=0,(_G.ySize-1),5 do
for jb=0,(_G.xSize-1),5 do
x=random(jb,(jb+5))
y=random(ib,(ib+5))
if ( x > _G.xSize-1 ) or ( y > _G.ySize-1 ) then
goto continue_1
end
-- Check 21 tiles radius for any city
h={-1,-0, 1,-2,-1, 1, 2, 0,-2, 2,-1, 1,-2,-1, 1, 2, 0,-1,-0, 1, 0}
v={-2,-2,-2,-1,-1,-1,-1,-1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 0}
for i=1,#h,1 do
if ( _G.is_XY_map_location_valid( ( h[i] + x ),( v[i] + y ) ) ) then
tile=find.tile( ( h[i] + x ),( v[i] + y ) )
if ( (tile):city() ) then goto continue_1 end
end
end
-- If not within any city radius
tile=find.tile(x,y)
for i=1,#decayingExtras,1 do
if ( (tile):has_extra(decayingExtras[i]) ) then
-- It should be VERY likely that tile extras outside city radius will fall apart
if ( random(1,10) > 1 ) then
-- ^ 90% chance
-- Remove the first matching extra
edit.remove_extra(tile,decayingExtras[i])
goto continue_1
end
end
end
::continue_1::
end
end
if ( debugLuaScripts ) then log.error("%s","\tDecayingExtras End") end
end
signal.connect("turn_started", "Lua_DecayingExtras")
The attached playable scenario for Freeciv 2.6.4 or later "RandoMap with DecayingExtras only" has only this script for testing. To test, start the scenario game and click "Turn done" repeatedly while watching areas of the map outside of any city radius.
- Attachments
-
- RandoMap with DecayingExtras ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
- (11.48 KiB) Downloaded 447 times
Last edited by Molo_Parko on Mon Dec 04, 2023 3:44 pm, edited 2 times in total.
-
- Hardened
- Posts: 177
- Joined: Fri Jul 02, 2021 4:00 pm
Re: New features via Lua script
CamelSprint allows a caravan's camel to rest at a distance, and then to sprint forward as many tiles as turns rested, to a target tile. This allows a caravan to reach a foreign city to create a trade-route without being in the tribe's territory at turn end. After the camel sprints forward to the target tile, select the caravan and establish the trade route.
^ Updated 2023-12-05
^ Caravan is 6 tiles from Dire Dawa and wishes to avoid being in the other tribe's territory during any "turn end" to preserve peaceful relations.
^ CamelSprint Lua script called with the x,y values of the Caravan's current tile and the target tile.
^ Caravan rests for 6 turns (Sentry the unit if you like) and then sprints forward to Dire Dawa in a single turn!
^ Select the Caravan within Dire Dawa and establish a trade route.
^ Trade route established without the Caravan being in any foreign tribe's territory at turn end.
Code: Select all
--##############################################################################
function Lua_CamelSprint(x, y, x2, y2)
-- This routine is activated manually by user calling it
CamelSprint=true
if ( debugLuaScripts == nil ) then debugLuaScripts=false end
if ( debugLuaScripts ) then log.error("%s","CamelSprint BEGIN") end
local i, unit, tile1, tile2, distance
-- Prepare table for up to 100 camels max
if ( _G.camels == nil ) then
_G.camels={}
end
tile1=find.tile(x,y)
tile2=find.tile(x2,y2)
distance=math.sqrt((tile1):sq_distance(tile2))
if ( (tile2).terrain:class_name() ~= "Land" ) then
log.error("%s","\tCamelSprint: Destination tile must be of class Land")
return
end
log.error("%s","\tCamelSprint: "..tostring(tile1).." "..tostring(tile2).." "..distance)
if ( (tile1):num_units() > 0 ) then
for unit in (tile1):units_iterate() do
if ( (unit).utype == find.unit_type("Caravan") ) then
i=( #_G.camels + 1 )
_G.camels[i]={["unit"]=unit, ["tile1"]=tile1, ["tile2"]=tile2, ["turns"]=(distance-1)}
end
end
end
if ( debugLuaScripts ) then log.error("%s","CamelSprint END") end
end
--##############################################################################
function Lua_CamelSprintGo(turn, year)
-- If this routine is disabled then just return
if ( CamelSprint == nil ) then return end
if not ( CamelSprint ) then return end
if ( debugLuaScripts == nil ) then debugLuaScripts=false end
if ( debugLuaScripts ) then log.error("%s","CamelSprintGo BEGIN") end
local i, unit, tile1, tile2, turns, x, y, x2, y2, removeFromList
for i=1,#_G.camels,1 do
if ( _G.camels[i].unit == nil ) then break end
removeFromList=false
unit=_G.camels[i].unit
tile1=_G.camels[i].tile1
tile2=_G.camels[i].tile2
turns=_G.camels[i].turns
log.error("%s","\tCamelSprintGo: unit=" .. tostring(unit) .. " tile1=" .. tostring(tile1) .. " tile2=" .. tostring(tile2) .. " turns=" .. tostring(turns))
if ( (unit).tile ~= tile1 ) then
log.error("%s","\tCamelSprintGo CANCELLED: Camel " .. tostring(unit) .. " no longer resting at tile: " .. tostring(tile1) )
removeFromList=true
end
if ( ( turns > 0 ) and ( removeFromList == false ) ) then
_G.camels[i]={["unit"]=unit, ["tile1"]=tile1, ["tile2"]=tile2, ["turns"]=(turns-1)}
else
edit.unit_move(unit,tile2,0)
removeFromList=true
end
if ( removeFromList ) then
_G.camels[i]=nil
_G.camels=(table.pack(table.unpack(_G.camels)))
end
end
if ( debugLuaScripts ) then log.error("%s","CamelSprintGo END") end
end
signal.connect("turn_started", "Lua_CamelSprintGo")
^ Caravan is 6 tiles from Dire Dawa and wishes to avoid being in the other tribe's territory during any "turn end" to preserve peaceful relations.
^ CamelSprint Lua script called with the x,y values of the Caravan's current tile and the target tile.
^ Caravan rests for 6 turns (Sentry the unit if you like) and then sprints forward to Dire Dawa in a single turn!
^ Select the Caravan within Dire Dawa and establish a trade route.
^ Trade route established without the Caravan being in any foreign tribe's territory at turn end.
- Attachments
-
- RandoMap with CamelSprint ONLY 48x48=2k, 80% land, Classic, Flat, v1.sav.zip
- (10.3 KiB) Downloaded 440 times
Last edited by Molo_Parko on Tue Dec 05, 2023 4:51 pm, edited 2 times in total.
-
- Hardened
- Posts: 177
- Joined: Fri Jul 02, 2021 4:00 pm
Re: New features via Lua script
Emulate some commands from newer versions of Freeciv implements some commands from Freeciv 3.2 in Freeciv version 2.4 or later so that those commands work in either version. Currently limited to Freeciv version related stuff.
Code: Select all
--##############################################################################
-- Emulate some commands from newer versions of Freeciv
if ( tonumber(string.sub(fc_version(),17,19)) >= 2.4 ) and ( tonumber(string.sub(fc_version(),17,19)) < 3.2 ) then
-- Emulate Fc 3.2 "version_string()"
if ( version_string == nil ) then
function version_string()
return string.sub(fc_version(),17)
end
end
-- Emulate Fc 3.2 "name_version()"
if ( name_version == nil ) then
function _G.name_version()
return fc_version()
end
end
-- Emulate Fc 3.2 "comparable_version()"
if ( comparable_version == nil ) then
function _G.comparable_version()
local text, count, field, fields
fields={}
text=string.sub(fc_version(),17)
count=1
for field in ( string.gmatch(text, "[^.]+") ) do
fields[count]=field
count=(count+1)
end
if ( #fields > 0 ) then
return (fields[1].."."..fields[2])
else
return nil
end
end
end
-- Emulate Fc 3.2 "versions_compare()"
if ( versions_compare == nil ) then
function _G.versions_compare(v1,v2)
if ( v1 == v2 ) then return 0
elseif ( v1 > v2 ) then return -1 -- Totally guessing at what "returns a negative" meant
else return 1
end
end
end
end
-
- Hardened
- Posts: 177
- Joined: Fri Jul 02, 2021 4:00 pm
Re: New features via Lua script
I have some catching-up to do in this topic!
This is the Lua script included in the scenario MiniMap Islands, shared at this link: https://forum.freeciv.org/f/viewtopic.p ... 04#p110004
Creating this Lua script in Freeciv 2.6.4 included a few frustrations...
Lua has no way to determine the startpos values included in the scenario file's [map] section. If the normal process of allowing aifill to create all players initially is used then it is fairly simple to get a tile on which a player's unit is standing at the beginning of turn 0 -- a prior script LateArrivalStartunits does just that. Also if startcity is enabled then it is very easy to get the tile where each tribe's startcity is built. But in this scenario I wanted to limit the computer-controlled tribes to using only island nations -- so aifill had to be disabled since it can't be restricted to solely island nations, and therefore there are no "AI" tribe startunits at the startpos tiles listed in the scenario. And there are no startcities for the computer-controlled tribes since the players don't yet exist when the game begins. So I had to duplicate the startpos values included in the scenario, by adding them manually in the script.
Since aifill is disabled, each of computer-controlled tribes had to be created manually, and the first bit of data needed is "leader name" -- which Lua can't get despite that the names are in the nation's ruleset files. So I used as leader names, Curly, Larry, and Moe instead.
Lua also can't determine which nations are or aren't island nations, so I had to do that manually by grep'ing for "island" in the nations files, sorting out those that aren't actually island nations, correcting the names to exactly match the nation ruleset names, and including the final list in the script itself. A flag for nation geographical type (island, coastal, inland or land-locked) in the ruleset files might be helpful.
After creating each player, the player's startcity must be manually created, and the startunits must be manually created.
All that just to limit nations to island nations. But it works and if it helps someone trying to make a more serious island based scenario, then I'm happy.
This is the Lua script included in the scenario MiniMap Islands, shared at this link: https://forum.freeciv.org/f/viewtopic.p ... 04#p110004
Code: Select all
o = "\n\n[c fg=\"#0000ff\"][ b ]Welcome to MiniMap Islands[ /b ][ /c ]" ..
"[c fg=\"#ff0000\"][ b ]\n\n\n" ..
"Select an island nation for your tribe via right-click\n" ..
"on the player shown near the top of this window. Then\n" ..
"select 'Pick Nation' in the pop-up list. Click the 'All'\n" ..
"button for all nations and select an island nation\n" ..
"such as Hawaiian.\n\n[ /b ][ /c ]"
notify.event( nil, nil, E.CHAT_MSG, o )
function s_setup()
if not _G.setupDone then
local tileIDs = { 34, 45, 210, 221 }
local island_nations = { "Acehnese", "Ainu", "Alander", "Aleut", "Antiguan and Barbudan", "Antillean", "Bahamian", "Bahraini", "Barbadian", "Cape Verdean", "Chinese", "Chumash", "Comorian", "Cretan", "Cuban", "Cypriot", "Dominican", "Dominicano", "Ecuadorian", "Equatoguinean", "Faroese", "Fijian", "Formosan", "Greenlander", "Grenadian", "Guanche", "Icelandic", "Illyrian", "Indonesian", "Jamaican", "Kiribati", "Malagasy", "Maldivian", "Maltese", "Manx", "Maori", "Marshallese", "Mauritian", "Moluccan", "Nauruan", "Nestorian", "Newfoundland", "New Zealand", "Nuu-chah-nulth", "Papuan", "Papua New Guinean", "Polynesian", "Puerto Rican", "Rapa Nui", "Saint Lucian", "Samoan", "Santomean", "Sardinian", "Seychellois", "Sicilian", "Sinhalese", "Solomon Islander", "Tahitian", "Taino", "Taiwanese", "Tongan", "Trinidadian and Tobagonian", "Turkish Cypriot", "Vanuatuan", "Venetian", "Vincentian", "West Indian" }
local rulerNames = { "Curly", "Larry", "Moe" }
for i = 1, 3, 1 do
tribe = find.nation_type( island_nations[ random( 1, #island_nations ) ] )
if tribe then
player = edit.create_player( rulerNames[ i ], tribe, nil )
for j = 1, 4, 1 do
tile = find.tile( tileIDs[ j ] )
if not tile:city() then
city = edit.create_city( player, tile, nil )
local startunits = server.setting.get( "startunits" )
for i = 1, #startunits do
local c = startunits:sub(i,i)
if c == "c" then unit = find.unit_type( "Settlers" )
elseif c == "f" then unit = find.unit_type( "Trireme" )
elseif c == "d" then unit = find.unit_type( "Warriors" )
end
edit.create_unit_full( player, tile, unit, 0, nil, 0, -1, nil )
end
break
end
end
end
end
for player in players_iterate() do
edit.give_tech( player, find.tech_type( "Map Making" ), 0, nil, nil )
end
_G.setupDone = true
end
end
signal.connect( "turn_started", "s_setup" )
Lua has no way to determine the startpos values included in the scenario file's [map] section. If the normal process of allowing aifill to create all players initially is used then it is fairly simple to get a tile on which a player's unit is standing at the beginning of turn 0 -- a prior script LateArrivalStartunits does just that. Also if startcity is enabled then it is very easy to get the tile where each tribe's startcity is built. But in this scenario I wanted to limit the computer-controlled tribes to using only island nations -- so aifill had to be disabled since it can't be restricted to solely island nations, and therefore there are no "AI" tribe startunits at the startpos tiles listed in the scenario. And there are no startcities for the computer-controlled tribes since the players don't yet exist when the game begins. So I had to duplicate the startpos values included in the scenario, by adding them manually in the script.
Since aifill is disabled, each of computer-controlled tribes had to be created manually, and the first bit of data needed is "leader name" -- which Lua can't get despite that the names are in the nation's ruleset files. So I used as leader names, Curly, Larry, and Moe instead.
Lua also can't determine which nations are or aren't island nations, so I had to do that manually by grep'ing for "island" in the nations files, sorting out those that aren't actually island nations, correcting the names to exactly match the nation ruleset names, and including the final list in the script itself. A flag for nation geographical type (island, coastal, inland or land-locked) in the ruleset files might be helpful.
After creating each player, the player's startcity must be manually created, and the startunits must be manually created.
All that just to limit nations to island nations. But it works and if it helps someone trying to make a more serious island based scenario, then I'm happy.
Last edited by Molo_Parko on Sat Nov 09, 2024 5:44 am, edited 2 times in total.
-
- Hardened
- Posts: 177
- Joined: Fri Jul 02, 2021 4:00 pm
Re: New features via Lua script
This is the "fast_start" Lua script included in the scenario Pieland, shared at this link: https://forum.freeciv.org/f/viewtopic.p ... 96#p109996
The script runs at the beginning of the game and creates 11 cities per tribe to "skip" past some of the earliest activities in the game which includes scouting the local area, and building cities. In this particular case, since each tribe's "homeland" area is shaped like a piece of pie or pizza, I just manually specified in the script which tiles to build the cities on -- and I alternated selecting tiles of one tribe with their neighbor across the water so that no tribe would have all the best spots and leave their neighbor all the worst spots for building cities.
It is possible to instead create a routine to deduce which tiles would be the best (by some criteria) city build sites, but the scenario is not large and it was easier to gather the tile IDs.
In this script too, the inability of Lua in Freeciv 2.6.4 to read the startpos that are included in the scenario file's [map] section, created a bit of extra work, but since startunits are provided it was easy to find them, get the tileID of their location, and use that tileID to determine in which pie-slice the tribe is located.
I used a single tile of river in the scenario's map as an indicator of whether the script had already run (the river is removed by the script) -- same as in another scenario "The Shadow Vales", so that during a reload of a saved game, the script won't try to run again and re-create all the cities, any of which may have already been destroyed.
Code: Select all
log.error( "%s", "fast_start can be disabled via the chat pane by entering the next line (followed by return or enter.)\n\n/lua fast_start = false\n" )
if _G.fast_start == nil then _G.fast_start = true end
-- Emulate Fc 3.0 game.current_turn()
if game.current_turn == nil then
function _G.game.current_turn()
return math.floor( game.turn() )
end
end
function s_fast_start()
if game.current_turn() == 0 then
if find.tile( 4, 43 ):has_extra( "River" ) then
if _G.fast_start == true then
log.error("%s", "fast_start: creating 11 cities for each tribe")
local city_tiles = {}
for player in players_iterate() do
local x, y, tile
for unit in player:units_iterate() do
tile = unit.tile
break
end
if tile.id == 985 then
city_tiles = { 797, 792, 655, 602, 515, 462, 409, 371, 271, 267, 121 }
elseif tile.id == 987 then
city_tiles = { 987, 1039, 897, 1044, 707, 854, 1049, 566, 711, 859, 1004 }
elseif tile.id == 1227 then
city_tiles = { 1227, 1184, 1189, 1194, 1374, 1379, 1384, 1520, 1573, 1578, 1716 }
elseif tile.id == 1321 then
city_tiles = { 1370, 1517, 1562, 1802, 1757, 1664, 2041, 1949, 1856, 2000, 1909 }
elseif tile.id == 1317 then
city_tiles = { 1317, 1557, 1458, 1598, 1649, 1797, 2037, 1889, 1838, 1883, 2031 }
elseif tile.id == 1219 then
city_tiles = { 1266, 1406, 1166, 1206, 1258, 1498, 1250, 1398, 1642, 1541, 1735 }
elseif tile.id == 979 then
city_tiles = { 979, 1023, 880, 971, 780, 968, 775, 586, 1060, 915, 583 }
elseif tile.id == 981 then
city_tiles = { 835, 644, 687, 496, 453, 493, 394, 301, 306, 309, 117 }
end
for i = 1, #city_tiles, 1 do
tile = find.tile( city_tiles[ i ] )
edit.create_city( player, tile, nil )
end
end
end
edit.remove_extra( find.tile( 4, 43 ), "River" )
end
end
end
signal.connect( "turn_started", "s_fast_start" )
It is possible to instead create a routine to deduce which tiles would be the best (by some criteria) city build sites, but the scenario is not large and it was easier to gather the tile IDs.
In this script too, the inability of Lua in Freeciv 2.6.4 to read the startpos that are included in the scenario file's [map] section, created a bit of extra work, but since startunits are provided it was easy to find them, get the tileID of their location, and use that tileID to determine in which pie-slice the tribe is located.
I used a single tile of river in the scenario's map as an indicator of whether the script had already run (the river is removed by the script) -- same as in another scenario "The Shadow Vales", so that during a reload of a saved game, the script won't try to run again and re-create all the cities, any of which may have already been destroyed.
Last edited by Molo_Parko on Sat Nov 09, 2024 5:47 am, edited 2 times in total.
-
- Hardened
- Posts: 177
- Joined: Fri Jul 02, 2021 4:00 pm
Re: New features via Lua script
Scripts from the scenario The Shadow Vales, which is shared at this link: https://forum.freeciv.org/f/viewtopic.php?p=109990
The one thing I am most happy about having changed in scripting within this scenario is that I stopped using tables and started using "field strings". What a pain in the butt it is to use tables, backup the tables to strings since tables are lost on reloads, then restore from the string back to the tables. Why not just use a string the whole time? So I did, and it works well! Here's what a "field string" looks like and a simple Lua routine to extract a value.
Below is the maxCitiesFields string. Field #'s are from 0 because player 0 has the first field. Player zero has held a maximum of 16 cities at the start of any turn, so the first field is "16". Player's 3 and 4 have each had a maximum of 18 cities. This method is simple to use and since the field is a string rather than a table, no backing-up and restoring the values after reloads is required at all.
maxCitiesFields="16|16|17|18|18|16|16|15|50|5"
My next favorite change is a consistent construct used to identify a player (to the human, in text output.) It includes the player number, nation name, and the ruler name so that those who can't recall which player is which ( namely, ME! ) see the #, tribe, and ruler together which greatly increases the liklihood of recognizing the player/tribe/ruler.
^ Ouput is consistently like this "#1 Mongol (Genghis)" instead of sometimes just a player number, sometimes just a tribe name, or sometimes just a ruler name.
Another one which was particularly helpful is below, it is used for Suburban Sprawl when creating roads outward from cities, and for determining to whom the bounty for a Barbarian Leader will be paid (in the scenario), and for Weatherizer_Winds when units are blown away to a distance corresponding to wind strength. It is also used in a routine to check whether a given tile is next to a specified type of terrain (as in a Mountains tile next to any Grassland tile.)
Next is one to check whether a city exists within citymindist of a given tile. I tried using the Freeciv Lua function "(unit):is_on_possible_city_tile" but it does not appear to take into account cities within citymindist, but rather only whether the tile terrain would accommodate a city. Next was "(tile):city_exists_within_city_radius and (tile):city_exists_within_max_city_map -- neither of which actually indicated whether or not a city was within citymindist of a given tile. I also tried various uses of (city):map_sq_radius, but became annoyed about having to manually screen-out some tiles (corners just outside city radius and the city tile itself) every time.
Suburban Sprawl gradually produces roads emanating outward from cities. The computer-controlled tribes won't produce workers for quite some time in The Shadow Vales, so I wrote this routine to create roads to make unit travel faster. This one is quite portable and can use signal.connect with turn_started ( Freeciv 2 ) or turn_begin (Freeciv 3), or just call it from another per turn routine. This does also need the "s_list_ring_of_tileIDs_around" routine which is posted above. Also the settings are flexible as to how often it will create a road segment per city, and also how far out from the city the roads will go (it works out to at least 10 rings which covers the entire map with roads by about turn 100, provided the frequnecy of creating a road segment is 1 per turn.)
Roving Rivers Creates random rivers from a fixed start point ( the isolated lake tile in each vale ) to a fixed shoreline ( the outer edges of the map ). In "The Shadow Vales", the routine is called once every 17 turns. When called it removes old rivers that it had created, and randomly plots new rivers -- while also adding tile irrigation and destroying roads on flat land tiles. This routine is a little less portable, but provided the scenario has fixed start tiles for the source of the river, and fixed shore lines, it wouldn't be too hard to port it into another scenario.
The one thing I am most happy about having changed in scripting within this scenario is that I stopped using tables and started using "field strings". What a pain in the butt it is to use tables, backup the tables to strings since tables are lost on reloads, then restore from the string back to the tables. Why not just use a string the whole time? So I did, and it works well! Here's what a "field string" looks like and a simple Lua routine to extract a value.
Below is the maxCitiesFields string. Field #'s are from 0 because player 0 has the first field. Player zero has held a maximum of 16 cities at the start of any turn, so the first field is "16". Player's 3 and 4 have each had a maximum of 18 cities. This method is simple to use and since the field is a string rather than a table, no backing-up and restoring the values after reloads is required at all.
maxCitiesFields="16|16|17|18|18|16|16|15|50|5"
Code: Select all
function s_extract_field( combinedFieldsString, fieldNumber )
local count = 0
for field in string.gmatch( combinedFieldsString, "[^|]+" ) do
if count == fieldNumber then return field end
count = count + 1
end
end
Code: Select all
function s_number_nation_ruler_name( player )
local playerName
if player and player ~= "" then
playerName = string.sub( tostring( player.nation ), 14, -2 )
playerName = playerName:gsub( "^.-%s", "", 1 )
playerName = ( "#" .. math.floor( player.id ) .. " " ..
playerName .. " ( " .. player.name .. " )"
)
return playerName
end
return false
end
Another one which was particularly helpful is below, it is used for Suburban Sprawl when creating roads outward from cities, and for determining to whom the bounty for a Barbarian Leader will be paid (in the scenario), and for Weatherizer_Winds when units are blown away to a distance corresponding to wind strength. It is also used in a routine to check whether a given tile is next to a specified type of terrain (as in a Mountains tile next to any Grassland tile.)
Code: Select all
function s_list_ring_of_tileIDs_around( centerTile, ringNumber )
local tileIDs = {}
local x = centerTile.x
local y = centerTile.y
for tile in ( centerTile ):square_iterate( ringNumber ) do
if math.abs( tile.x - x ) == ringNumber or math.abs( tile.y - y ) == ringNumber then
tileIDs[ #tileIDs + 1 ] = tile.id
end
end
return tileIDs
end
Code: Select all
function s_is_a_city_within_citymindist( tile )
local citymindist = tonumber( server.setting.get( "citymindist" ) )
for h = ( tile.x - ( citymindist - 1 ) ), ( tile.x + ( citymindist - 1 ) ), 1 do
for v = ( tile.y - ( citymindist - 1 ) ), ( tile.y + ( citymindist - 1 ) ), 1 do
if find.tile( h, v ):city() then return true end
end
end
return false
end
Code: Select all
function s_suburban_sprawl( turn, year )
local output="[ b ]Suburban sprawl creates roads emanating from cities[ /b ]"
for player in players_iterate() do
local roads = ""
local playerName = s_number_nation_ruler_name( player )
for city in player:cities_iterate() do
if random( 1, 100 ) > ( city.size + 1 ) * 10 then
goto loop_exit
end
local tile
local tileIDs = {}
for ring = 1, 4, 1 do
tileIDs = s_list_ring_of_tileIDs_around( city.tile, ring )
for i = 1, #tileIDs, 1 do
local t = tileIDs[ i ]
local tile = find.tile( t )
if tile.terrain:class_name() == "Land"
and tile:has_extra( "Road" ) == false
and tile:has_extra( "River" ) == false
and tile.terrain.id ~= find.terrain( "Inaccessible" ).id
then
edit.create_extra( tile, "Road" )
roads = ( roads .. math.floor( tile.x ) .. "," .. math.floor( tile.y ) .. " " )
goto loop_exit
end
::offmap::
end
end
::loop_exit::
end
if roads ~= "" then
if output ~= "" then
chat( "\t" .. output )
output=""
end
chat( "\t\t" .. playerName .. " at " .. roads )
end
end
chat()
end
Code: Select all
function s_roving_rivers( turn, year )
local valeBoundsStartX = { 0, 22, 44, 0, 0, 48, 0, 22, 44 }
local valeBoundsEndX = { 16, 38, 60, 12, 0, 60, 16, 38, 60 }
local valeBoundsStartY = { 0, 0, 0, 22, 0, 22, 44, 48, 44 }
local valeBoundsEndY = { 16, 12, 16, 38, 0, 38, 60, 60, 60 }
local valeLakesX = { 12, 30, 49, 12, 0, 48, 12, 30, 50 }
local valeLakesY = { 10, 12, 10, 30, 0, 30, 50, 48, 49 }
if _G.riverPathsFieldString == nil then
_G.riverPathsFieldString = ""
else
for field in string.gmatch( _G.riverPathsFieldString, "[^|]+" ) do
local tile = find.tile( field )
edit.remove_extra( tile, "River" )
end
_G.riverPathsFieldString = ""
end
local vale, coast, atShoreline, x, y, tile
for vale = 1, 9, 1 do
if vale == 1 then
coast = "x-"
if random( 1, 2 ) == 1 then
coast = "y-"
end
elseif vale == 2 then
coast = "y-"
elseif vale == 3 then
coast = "x+"
if random( 1, 2 ) == 1 then
coast = "y-"
end
elseif vale == 4 then
coast = "x-"
elseif vale == 5 then
goto vale_done
elseif vale == 6 then
coast = "x+"
elseif vale == 7 then
coast = "x-"
if random( 1, 2 ) == 1 then
coast = "y+"
end
elseif vale == 8 then
coast = "y+"
else
coast = "x+"
if random( 1, 2 ) == 1 then
coast = "y+"
end
end
x, y = valeLakesX[ vale ], valeLakesY[ vale ]
atShoreline = 0
repeat
local ok = 0
repeat
local h, v = 0, 0
if coast == "x-" then
if random( 1, 2 ) == 1 then
h = h - 1
else
if random( 1, 2 ) == 1 then
v = v + 1
else
v = v - 1
end
end
elseif coast == "x+" then
if random( 1, 2 ) == 1 then
h = h + 1
else
if random( 1, 2 ) == 1 then
v = v + 1
else
v = v - 1
end
end
elseif coast == "y-" then
if random( 1, 2 ) == 1 then
v = v - 1
else
if random( 1, 2 ) == 1 then
h = h + 1
else
h = h - 1
end
end
else
if random( 1, 2 ) == 1 then
v = v + 1
else
if random( 1, 2 ) == 1 then
h = h + 1
else
h = h - 1
end
end
end
tile = find.tile( x + h, y + v )
if tile.terrain:rule_name() ~= "Mountains"
and tile.terrain:rule_name() ~= "Inaccessible" then
x, y = x + h, y + v
ok = 1
end
until ok > 0
if x < 1 or x > 59 or y < 1 or y > 59 then goto vale_done end
if tile.terrain:rule_name() ~= ( "Lake" ) then
edit.create_extra( tile, "River" )
end
if tile.terrain:rule_name() ~= "Lake"
and tile.terrain:rule_name() ~= "Jungle"
and tile.terrain:rule_name() ~= "Swamp"
and tile.terrain:rule_name() ~= "Forest"
and not tile:city() then
edit.create_extra( tile, "Irrigation" )
end
if not tile:city()
and tile.terrain:rule_name() ~= "Forest" then
edit.remove_extra( tile, "Road" )
end
_G.riverPathsFieldString = ( _G.riverPathsFieldString .. math.floor( tile.id ) .. "|" )
if x < 2 or x > 58 or y < 2 or y > 58 then
atShoreline = 1
end
until atShoreline > 0
::vale_done::
end
_G.riverPathsFieldString = string.sub( _G.riverPathsFieldString, 1, -2 )
if game.current_turn() > 1 then
local output = "[ b ]Vale Echo News:[ /b ]\t\tMeltwater and heavy rains caused rivers to overrun their banks, washing-out roads."
if not _G.ReloadDetected then notify.event( nil, nil, E.REPORT, "__" .. output ) end
notify.event( nil, nil, E.CHAT_MSG, "\t" .. output )
end
end
Last edited by Molo_Parko on Sat Nov 09, 2024 5:25 am, edited 4 times in total.