-- Freeciv - Copyright (C) 2007 - The Freeciv Project -- This program is free software; you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation; either version 2, or (at your option) -- any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- When creating new ruleset, you should copy this file only if you -- need to override default one. Usually you should implement your -- own scripts in ruleset specific script.lua. This way maintaining -- ruleset is easier as you do not need to keep your own copy of -- default.lua updated when ever it changes in Freeciv distribution. -- nef 2019/01/20 -- enable new version of civ1 script.lua 20190912.0 --[[ changes from previous version are to: - add parameters and 'unit' return value for mass action functions, - enable direct coding of gold - mass action functions can now 'hand off' to a default, recursively subject to a nominal limit (currently 10) - ruleset scripts can selectively use these functions when defining (and processing) their own profiles allowing maximal reuse the functions have been written to accommodate ANY unit based stochastic process - not just hut enter the assert statements are there for those wanting to write their own profiles and want (quick) diagnostics At the time of writing the only global variables used (in default.lua} are: _deflua_nearest_city -- general use _deflua_hut_callback _deflua_unit_get_home -- for civ1 legacy scripts _deflua_hut_ruleset_context -- defined by rulesets to set their own profile _deflua_hut -- everything else ]] --[[ hut enter support functions =========================== ]] --[[ _deflua_nearest_city -------------------- This could be used for any Civ I/ Civ II purposes requiring identification of new unit owner. Here we do hut enter but other uses could be - settlers from disbanded cities - partisans ??? - bribed units Some consider these to be exploits - perhaps a server option would be in order (it could be tested in Lua - making it completely soft code). Because of its general use it has been left 'outside' the package table (_deflua_hut). The distance metric used is pythagorean squared as defined by tile:sq_distance.]] function _deflua_nearest_city (tile) local old_city, old_distance for new_player in players_iterate () do for new_city in new_player:cities_iterate () do local new_distance = tile:sq_distance (new_city.tile) if not old_distance or new_distance < old_distance then old_city, old_distance = new_city, new_distance end end end return old_city, old_distance end -- The following are inside the table _deflua_hut _deflua_hut = { -- the end of this table is a long way down near_city = function (context) local tile = context.unit.tile -- alternative is to use circle_iterate (context.city_radius_sq) -- and test each tile for a city but we have _deflua_nearest_city -- so why not use it if type (context.city_radius_sq) == "number" then local city, distance = _deflua_nearest_city (tile) return distance and distance <= context.city_radius_sq else return tile:city_exists_within_max_city_map(true) end end; unit_home_city = function (context) return context.unit:get_homecity() end; city_home_city = function (context) local unit = context.unit local player, tile = unit.owner, unit.tile local city = _deflua_nearest_city (tile) if city and city.owner == player then return city -- else it's a freebie end end; --[[ end support functions =====================]] tail = { --[[ mass point action TAIL functions ================================ Do not use as mass points - the parameter lists differ. Take care with changes since these can also be used by ruleset scripts. They should return the same value set as for mass point functions so that they can be used as tail calls. They are all fields in _deflua_hut.tail --------------- -------------------------------------------------------]] new_unit = function (context, utype) local unit = context.unit local player, tile = unit.owner, unit.tile player:create_unit(tile, utype, 0, context:home_city(), -1) end; mercenaries = function (context, utype) local unit = context.unit local player, tile = unit.owner, unit.tile local MPF, tail = context._MPF, context._tail notify.event(player, tile, E.HUT_MERC, _("A band of friendly mercenaries joins your cause.") ) return tail.new_unit (context, utype) -- take care not to get into indefinite tail calls -- keep to the convention of only using previously -- defined functions end; -- assumes the tech has already been gotten - so just reports technology = function (unit, tech) local player, tile = unit.owner, unit.tile local tech_name = tech:name_translation() notify.event(player, tile, E.HUT_TECH, _("You found %s in ancient scrolls of wisdom."), tech_name) notify.research(player, false, E.TECH_GAIN, -- /* TRANS: One player got tech for the whole team. */ _("The %s found %s in ancient scrolls of wisdom for you."), player.nation:plural_translation(), tech_name) notify.research_embassies(player, E.TECH_EMBASSY, -- /* TRANS: first %s is leader or team name */ _("%s has acquired %s from ancient scrolls of wisdom."), player:research_name_translation(), tech_name) end; barbarians = function (context, unit) local player, tile = unit.owner, unit.tile local utype = unit.utype -- get this before the unit disappears if tile:unleash_barbarians() then notify.event(player, tile, E.HUT_BARB, _("You have unleashed a horde of barbarians!")) else notify.event(player, tile, E.HUT_BARB_KILLED, _("Your %s has been killed by barbarians!"), utype:name_translation()) context.unit = nil -- unit has actually been detroyed end end; gold = function (context, gold) local unit = context.unit local player, tile = unit.owner, unit.tile player:change_gold(gold) notify.event(player, tile, E.HUT_GOLD, PL_("You found %d gold.", "You found %d gold.", gold), gold) end; }; -- end of _deflua_hut.tail --[[ MASS POINT FUNCTIONS ==================== Primary purpose of Mass Point Functions is to be used as the key values in the profile, with the corresponding probability mass as the value. It is possible to make (some) of these functions anonymous but the code looks UGLY. MPFs can be used by ruleset scripts. They all have the same parameter list, and the same set of return values. Typically, support functions will also return the same set so a 'closed' call can be used directly in return statements MPFs can be also be used as "handoffs" - that is unlike tail calls these can be returned as the return value. In these cases it is the function name (i.e. reference) that is returned. MPFs are fields in _deflua_hut.MPF --------------- ----------------------------------------------------]] MPF = { weeds = function (context) local unit = context.unit local player, tile = unit.owner, unit.tile notify.event(player, tile, E.HUT_BARB_CITY_NEAR, _("An abandoned village is here.") ) end; consolation = function (context) return context.consolation or context._MPF.weeds end; --[[ find.role_unit_type () doesn't work so well for role "Hut" since it only ever selects the first in units.ruleset the change here allows a search for user defined FLAGS that have the format hut starting with "hut0". If "hut0" is not found then default is old behaviour Note: numbers must be contiguous (from 0) as used in 'flags = "hut0" '; order is statistically irrelevant; duplicate use of same flag treated like role "hut" i.e. ignored; units may have multiple (different) flags - increasing their chance of being selected ]] old_hut = function (context) local unit = context.unit local player, tile = unit.owner, unit.tile local MPF, tail = context._MPF, context._tail local utype = find.role_unit_type('Hut', nil) if utype and utype:can_exist_at_tile(tile) then return tail.mercenaries (context, utype) else return context.consolation or MPF.weeds end end; hutx = function (context) local unit = context.unit local player, tile = unit.owner, unit.tile local MPF, tail = context._MPF, context._tail local hutx, hut_units = nil, {} for huty, new_unit in function (tile, newy) local utype = find.role_unit_type("hut" .. newy, nil) if utype and utype:can_exist_at_tile(tile) then return newy + 1, utype end end, tile, 0 do hutx, hut_units [huty] = huty, new_unit end if hutx then utype = hut_units [math.floor (random(1,hutx) )] return tail.mercenaries (context, utype) else return MPF.old_hut end end; mercenary = function (context) local unit = context.unit local player, tile = unit.owner, unit.tile local MPF, tail = context._MPF, context._tail local utype = find.role_unit_type('HutTech', player) if utype and utype:can_exist_at_tile(tile) then return tail.mercenaries (context, utype) else return MPF.hutx end end; technology = function (context) local unit = context.unit local player, tile = unit.owner, unit.tile local MPF, tail = context._MPF, context._tail local tech = edit.give_tech (player, nil, -1, false, "hut") if tech then return tail.technology (unit, tech) else return context.consolation or MPF.weeds end end; barbarians = function (context) local unit = context.unit local player, tile = unit.owner, unit.tile local MPF, tail = context._MPF, context._tail if server.setting.get("barbarians") == "DISABLED" or context:_near_city () or unit.utype:has_flag('Gameloss') then return context.weeds or MPF.weeds else return tail.barbarians (context, unit) end end; settlers = function (context) local unit = context.unit local player, tile = unit.owner, unit.tile local MPF, tail = context._MPF, context._tail local utype = find.role_unit_type ('Cities', player) if utype and utype:can_exist_at_tile (tile) then notify.event(player, tile, E.HUT_SETTLER, _("Friendly nomads are impressed by you, and join you.") ) return tail.new_unit (context, utype) else return context.consolation or MPF.weeds end end; city = function (context) local unit = context.unit local player, tile = unit.owner, unit.tile local MPF, tail = context._MPF, context._tail if unit:is_on_possible_city_tile() then player:create_city(tile, "") notify.event(player, tile, E.HUT_CITY, _("You found a friendly city.") ) else return context.settlers or MPF.settlers end end; }; -- end of _deflua_hut.MPF --[[ PROFILE PROCESSING FUNCTIONS ============================ These functions are quite general and can be used for a variety of unit stochastic processes that use a single uniform random variate. So, for example, battle bouts in "maths of Freeciv", Civ II, and Civ III but not Civ I where two are required(?). One can emulate other distributions by manipulating the masses but this could get a little arcane. For Civ I battles one could write another version using two calls to 'random', one each for attacker and defender. Many of these other applications are usually too trivial but this function may be useful where there are a modest number of mutually exclusive outcomes. ----------------------------------------------------------]] get_point = function (context) local profile = context._profile context.loop_count = 0 --[[ If an index table is needed for repeatability when using an RNG seed the two for loops will need to be changed to use ipairs on the index. Indications are that Lua is deterministic if one sticks with the Freeciv supplied RNG so this should not be neccessary. (BUT possibly not invariant across platforms - even one machine to the next.) Mass entries of zero are valid (point will be ignored) but the total must be at least one. ]] local total = 0 for MPA, mass in pairs (profile) do assert (type (mass) == "number" and mass >= 0, "profile item mass error") total = total + mass end assert (total >= 1, "profile total mass error") total = math.floor (random(1, total)) -- Freeciv RNG for MPA, mass in pairs (profile) do total = total - mass if total <= 0 then return MPA end end assert (nil, "program error") -- previous assert should pick this up end; --[[ An example of creeping elegance ? This was rather trivial allowing one to 'duplicate' mass points, but the main purpose is to facilitate revisions to the way the profile is handled, that is, changes to _deflua_get_point are more easily coordinated by keeping all the internal details here. A reference to this function is created in the context table with key "_set_point" by the function _deflua_hut_reset MPA - Mass Point Action ]] set_point = function (context, MPA, mass) assert ( type (mass) == "number" and mass >= 0, "Invalid probability mass") mass = math.floor (mass) local profile = context._profile if type (profile [MPA]) == "number" then profile [MPA] = profile [MPA] + mass else profile [MPA] = mass end end; --[[ Use _deflua_hut.reset to create the profile table before using set_point. Trivial now but open to revision in the future. Lua scripts in rulesets should avoid interfering with the key fields: _set_point used to define mass points _MPF table of mass point action functions _tail table of tail functions _near_city function allowing use of city_radius_sq In particular, don't use in any way _profile internal description of points At the time of writing, user defined fields are unit - more manageable than using parameter/return value consolation - typically a gold value but could be any mass point action ;default 25 use_number - the "tail" function for providing gold; that is, when an MPA is numeric; default is to provide gold equal to the MPA number The arguments provided are (context, MPA) home_city - support function defining new unit home city; default is to use that of unit entering the hut (unit_home_city). The supplied alternative is city_home_city which uses nearest city. Single argument - context. Return value fc city or nil. city_radius_sq - Radius used by near_city to replace tile:city_exists_within_max_city_map(true) when a different radius is wanted note the actual test is <= not < to be compatible with circle_iterate settlers alternate MPA to use for city substitute when tile unsuitable for a city weeds alternate MPA to use when barbs should not be created loop_count - for debugging ]] reset = function (self, unit) return { _profile = {}; _set_point = self.set_point; _MPF = self.MPF; _tail = self.tail; _near_city = self.near_city; unit = unit; consolation = 25; use_number = self.tail.gold; home_city = self.unit_home_city; } end; --[[ key processing function - use a different version for different applications (this one is for huts but could also be used more generally) Has been set up to use as an iterator in a 'for' statement ======================================================]] process = function(context, MPA) if type (MPA) == "function" then return MPA (context), context.loop_count + 1 elseif type (MPA) == "number" and type (context.use_number) == "function" then return context:use_number (MPA), context.loop_count + 1 else assert (nil, "profile MPA invalid") end end; --[[ callback stuff ============== ]] enter = function(self, context) local unit = context.unit for MPA, loop_count in self.process, context, self.get_point (context) do assert (loop_count < 10, "profile function excessive recursion") context.loop_count = loop_count end return context.unit -- see "barbarians" tail function end; ruleset_context = _deflua_hut_ruleset_context; } -- end of _deflua_hut function _deflua_hut_callback(unit) local DH, context = _deflua_hut -- allow individual rulesets to set their own if type (DH.ruleset_context) == "function" then context = DH:ruleset_context (unit) else context = DH.context; context.unit = unit end return not DH:enter (context, unit) -- see "barbarians" tail function end signal.connect("hut_enter", "_deflua_hut_callback") --[[ and now for the actual default Lua profile ========================================== Turn this into a function if you want it to be dynamic (i.e. setup each callback) and call it from the 'else' branch above as for the 'then' branch. The return value is the context table. ]] do local context = _deflua_hut:reset (nil) -- set unit during callback local MPF, tail = context._MPF, context._tail --[[ The function _deflua_unit_get_home is not used anymore except as an indicator so that this default script will work with the previous version of civ1 script.lua 'as advertised'. This conditional is redundant if the current civ1 script is used, and is optional, at best. ]] if type (_deflua_unit_get_home) == "function" then context.use_nearest_city = true end -- traditional freeciv profile context:_set_point ( 25, 1) context:_set_point ( 50, 3) context:_set_point (100, 1) context:_set_point (MPF.technology, 3) context:_set_point (MPF.mercenary, 2) context:_set_point (MPF.barbarians, 1) context:_set_point (MPF.city, 1) _deflua_hut.context = context end --[[ end hut enter functions =======================]] --[[ Make partisans around conquered city if requirements to make partisans when a city is conquered is fulfilled this routine makes a lot of partisans based on the city`s size. To be candidate for partisans the following things must be satisfied: 1) The loser of the city is the original owner. 2) The Inspire_Partisans effect must be larger than zero. If these conditions are ever satisfied, the ruleset must have a unit with the Partisan role. In the default ruleset, the requirements for inspiring partisans are: a) Guerilla warfare must be known by atleast 1 player b) The player must know about Communism and Gunpowder c) The player must run either a democracy or a communist society. ]]-- function _deflua_make_partisans_callback(city, loser, winner, reason) if reason ~= 'conquest' or city:inspire_partisans(loser) <= 0 then return end local partisans = random(0, 1 + (city.size + 1) / 2) + 1 if partisans > 8 then partisans = 8 end city.tile:place_partisans(loser, partisans, city:map_sq_radius()) notify.event(loser, city.tile, E.CITY_LOST, _("The loss of %s has inspired partisans!"), city.name) notify.event(winner, city.tile, E.UNIT_WIN_ATT, _("The loss of %s has inspired partisans!"), city.name) end signal.connect("city_transferred", "_deflua_make_partisans_callback") -- Notify player about the fact that disaster had no effect if that is -- the case function _deflua_harmless_disaster_message(disaster, city, had_internal_effect) if not had_internal_effect then notify.event(city.owner, city.tile, E.DISASTER, _("We survived the disaster without serious damage.")) end end signal.connect("disaster_occurred", "_deflua_harmless_disaster_message")