widelands-dev team mailing list archive
-
widelands-dev team
-
Mailing list archive
-
Message #01505
[Merge] lp:~stdh/widelands/rewrite_wincondition into lp:widelands
Steven De Herdt has proposed merging lp:~stdh/widelands/rewrite_wincondition into lp:widelands.
Requested reviews:
Widelands Developers (widelands-dev)
For more details, see:
https://code.launchpad.net/~stdh/widelands/rewrite_wincondition/+merge/178461
A rewrite of the Territorial Lord win condition, imho clearer with better abstractions. It should also be a bit more straightforward to adapt to a land-ownership-change hook, should that materialize.
I hope the structure of the code is clear as is, otherwise I'd like to add some comments.
Remaining questions, on top of those posed in code comments:
*What does wc_version mean & should I provide compatibility code for old savegames?
*The code to check for defeated players was broken in that it only checks once. I didn't touch it, some other win conditions use the same faulty code and it should be adressed more globally, I feel. Bonus question: should other players be noticed when one of their friends/enemies is defeated?
*I noticed that "not rawequal(wl.Game().players[2],wl.Game().players[2])", while the two are considered equal by the "==" operator. This means a Player cannot be used as a key in a table, which is slightly annoying and should be mentioned in the docs if not outright fixed.
*After I found this out, I used wl.game.Player.name as a key. I hope that's a unique property...
*Code style and formatting OK? A quick search didn't turn up information about any preference in the project.
*I must have forgotten what should come here, maybe later...
And a known bug: it doesn't survive save-load. More specifically, the main loop seems to be restarted so that it loses connection to the last thread it started. In specific conditions it is possible to win twice! I have a script more or less ready to demonstrate that, but it seemed a bit silly to put that in this branch...
--
https://code.launchpad.net/~stdh/widelands/rewrite_wincondition/+merge/178461
Your team Widelands Developers is requested to review the proposed merge of lp:~stdh/widelands/rewrite_wincondition into lp:widelands.
=== modified file 'scripting/infrastructure.lua'
--- scripting/infrastructure.lua 2010-09-22 17:50:53 +0000
+++ scripting/infrastructure.lua 2013-08-04 14:52:28 +0000
@@ -64,7 +64,7 @@
--
-- prefilled_buildings(wl.Game().players[1],
-- {"sentry", 57, 9}, -- Sentry completely full with soldiers
--- {"sentry", 57, 9, soldier={[{0,0,0,0}]=1}}, -- Sentry with one soldier
+-- {"sentry", 57, 9, soldiers={[{0,0,0,0}]=1}}, -- Sentry with one soldier
-- {"bakery", 55, 20, wares = {wheat=6, water=6}}, -- bakery with wares and workers
-- {"well", 52, 30}, -- a well with workers
-- )
=== modified file 'scripting/table.lua'
--- scripting/table.lua 2010-09-25 19:34:01 +0000
+++ scripting/table.lua 2013-08-04 14:52:28 +0000
@@ -41,3 +41,26 @@
return rv
end
+-- RST
+-- .. function:: array_ind_max(a)
+--
+-- Finds the index of the greatest value in the given array.
+--
+-- :arg a: An array of sortables (numbers, strings, ...) which are
+-- comparable between themselves.
+-- :type a: :class:`array`
+--
+-- :returns: The index of the greatest value. If this value occurs multiple
+-- times: the index of the last occurrence.
+function array_ind_max(a)
+ max = -math.huge
+ maxi = 0
+ for k,v in ipairs(a) do
+ if v >= max then
+ max = v
+ maxi = k
+ end
+ end
+ return maxi
+end
+
=== modified file 'scripting/win_condition_functions.lua'
--- scripting/win_condition_functions.lua 2012-04-25 07:29:45 +0000
+++ scripting/win_condition_functions.lua 2013-08-04 14:52:28 +0000
@@ -96,6 +96,37 @@
end
-- RST
+-- .. function:: get_factions(plrs)
+--
+-- Calculates factions so that each given player is in exactly one faction,
+-- and each faction contains one team, or teamless player.
+--
+-- :arg plrs: only these Players (array) are considered
+-- :returns: an array with all factions in the game, a faction being an
+-- array of the Players in it
+function get_factions(plrs)
+ local factions = {}
+ for k,v in pairs(plrs) do
+ if v.team == 0 then
+ factions[#factions+1] = {v}
+ else
+ local gotteam = false
+ for fk, fv in pairs(factions) do
+ if fv[1].team == v.team then
+ fv[#fv+1] = v
+ gotteam = true
+ break
+ end
+ end
+ if not gotteam then
+ factions[#factions+1] = {v}
+ end
+ end
+ end
+ return factions
+end
+
+-- RST
-- .. function:: broadcast(plrs, header, msg[, options])
--
-- broadcast a message to all players using
=== modified file 'scripting/win_conditions/03_territorial_lord.lua'
--- scripting/win_conditions/03_territorial_lord.lua 2013-06-08 10:48:35 +0000
+++ scripting/win_conditions/03_territorial_lord.lua 2013-08-04 14:52:28 +0000
@@ -1,208 +1,220 @@
+-- Copyright (C) ??-??, 2013 by the Widelands Development Team
+--
+-- 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
+-- of the License, 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.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program; if not, write to the Free Software
+-- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+-- 02110-1301, USA.
+
-- =======================================================================
-- Territorial Lord Win condition
-- =======================================================================
-use("aux", "coroutine") -- for sleep
+set_textdomain("win_conditions")
+
+use("aux", "coroutine")
use("aux", "table")
use("aux", "win_condition_functions")
-
-set_textdomain("win_conditions")
-
use("aux", "win_condition_texts")
-local wc_name = _ "Territorial Lord"
-local wc_version = 2
-local wc_desc = _ (
- "Each player or team tries to obtain more than half of the maps' " ..
- "area. The winner will be the player or the team that is able to keep " ..
- "that area for at least 20 minutes."
-)
+local wc_name = _("Territorial Lord")
+local wc_version = 3 --stdh: increased by one, right?
+local wc_desc = _(
+[[Each player or team tries to obtain more than half of the maps' area. The
+winner will be the player or the team that is able to keep that area for at
+least 20 minutes.]] )
+
+-- Get an array of all valueable fields of the map. A field is valuable if it
+-- is not swimmable, and it's either walkable or has an immovable. The editor
+-- disallows placing immovables on dead and acid fields.
+-- stdh: can walkable fields be swimmable?
+-- can swimmable fields have immovables?
+local get_valfields = function()
+ local valfields = {}
+ local map = wl.Game().map
+ for x=0,map.width-1 do
+ for y=0,map.height-1 do
+ local f = map:get_field(x,y)
+ --stdh: is this f sometimes nil?
+ if (f and not f:has_caps("swimmable") and
+ (f:has_caps("walkable") or f.immovable)) then
+ valfields[#valfields+1] = f
+ end
+ end
+ end
+ return valfields
+end
+
+-- Returns array with the number of fields each player owns, using
+-- corresponding keys of 'players'. Only given fields and players
+-- (both arrays) are considered.
+local land_per_player = function(fields, players)
+ local lpp = {}
+ local plind = {} --player.name:index map
+ for k, pl in ipairs(players) do
+ lpp[k] = 0
+ plind[pl.name] = k
+ end
+ for k, field in ipairs(fields) do
+ local owner = field.owner
+ if owner then
+ local oname = plind[owner.name]
+ if not oname then break end
+ lpp[oname] = lpp[oname] + 1
+ end
+ end
+ return lpp
+end
+
+-- Returns array with the number of fields each faction owns, using the same
+-- keys as the input 'factions'. Only given fields and factions (such as
+-- returned by get_factions) are considered.
+local land_per_faction = function(fields, factions)
+ local lpf = {}
+ local plfind = {} --player.name:faction_index map
+ local players = {}
+ for fack, facv in ipairs(factions) do
+ lpf[fack] = 0
+ for plk, plv in ipairs(facv) do
+ plfind[plv.name] = fack
+ players[#players+1] = plv
+ end
+ end
+ for plind, pllandsize in ipairs(land_per_player(fields, players)) do
+ local facind = plfind[players[plind].name]
+ lpf[facind] = lpf[facind] + pllandsize
+ end
+ return lpf
+end
+
+-- Function which performs the countdown from when one faction reaches 50% land
+-- 'till win. Gives messages at 5 min. interval and upon win. When 20 minutes
+-- have elapsed, finalizes the game: sets map visible for everyone, sends
+-- appropriate won/lost messages and report_results to the metaserver.
+-- Set proceed[1] (call by ref) to false to abort this procedure.
+local win_countdown = function(factions, candfaci, proceed)
+ local candstr
+ if #factions[candfaci] > 1 then
+ local teamnr = factions[candfaci][2].team
+ candstr = game_status_territoral_lord.team:format(teamnr)
+ else
+ candstr = factions[candfaci][1].name
+ end
+ local msg1 = game_status_territoral_lord.other1:format(candstr) .. "\n" ..
+ game_status_territoral_lord.other2
+ local msg2 = game_status_territoral_lord.player1 .. "\n" ..
+ game_status_territoral_lord.player2
+ for timetogo = 20,5,-5 do --send status messages at 20, 15, 10 and 5 min
+ if not proceed[1] then return end
+ for faci, facv in ipairs(factions) do
+ if faci == candfaci then
+ for wi, wv in ipairs(facv) do
+ wv:send_message(game_status.title, msg2:format(timetogo),
+ {popup = true})
+ end
+ else
+ for li, lv in ipairs(facv) do
+ lv:send_message(game_status.title, msg1:format(timetogo),
+ {popup = true})
+ end
+ end
+ end
+ sleep(5*60*1000) --5 minutes
+ end
+ if not proceed[1] then return end
+ --We have a winner:
+ local plrs = wl.Game().players
+ local lpp = land_per_player(get_valfields(), plrs)
+ local lppn = {} --player.name:land map
+ for pli, plv in ipairs(plrs) do
+ lppn[plv.name] = lpp[pli]
+ end
+ for faci, facv in ipairs(factions) do
+ if faci == candfaci then
+ for wi, wv in ipairs(facv) do
+ wv.see_all = 1
+ wv:send_message(won_game_over.title, won_game_over.body,
+ {popup = true})
+ --stdh: points are awarded for player land, disregarding teams.
+ -- Right?
+ wl.game.report_result(wv, true, lppn[wv.name],
+ make_extra_data(wv, wc_name, wc_version))
+ end
+ else
+ for li, lv in ipairs(facv) do
+ lv.see_all = 1
+ lv:send_message(lost_game_over.title, lost_game_over.body,
+ {popup = true})
+ wl.game.report_result(lv, false, lppn[lv.name],
+ make_extra_data(lv, wc_name, wc_version))
+ end
+ end
+ end
+ proceed[1] = "won"
+end
+
+--Initiates win_countdown and returns a 'proceed' handle.
+local start_win_countdown = function(factions, candfaci)
+ local proceed = {true}
+ run(win_countdown, factions, candfaci, proceed)
+ return proceed
+end
+
+
+local wc_func = function()
+ local plrs = wl.Game().players
+ local factions = get_factions(plrs)
+ local fields = get_valfields()
+ local reqnumfields = #fields/2
+
+ broadcast(plrs, wc_name, wc_desc)
+
+ -- Start a new coroutine that checks for defeated players
+ --stdh: TODO: fix 'cause only runs once
+ run(function()
+ sleep(5000)
+ check_player_defeated(plrs, lost_game.title, lost_game.body,
+ wc_name, wc_version)
+ end)
+
+ local candfac = nil --candidate faction
+ local proceed = {false} --whether to proceed the win_countdown
+
+ --Main loop:
+ while true do
+ --Sleep 30 seconds == STATISTICS_SAMPLE_TIME
+ --stdh: why is that important/pertinent?
+ sleep(30000)
+ lpf = land_per_faction(fields, factions)
+ local maxf = array_ind_max(lpf) --faction owning the most land
+ if lpf[maxf] >= reqnumfields then
+ if not candfac or maxf ~= candfac then --new candidate
+ proceed[1] = false
+ candfac = maxf
+ proceed = start_win_countdown(factions, maxf)
+ else --same candidate
+ if proceed[1] == "won" then --game over
+ break
+ end
+ end
+ else --no candidate
+ candfac = nil
+ proceed[1] = false
+ end
+ end
+end
+
return {
name = wc_name,
description = wc_desc,
- func = function()
-
- -- Get all valueable fields of the map
- local fields = {}
- local map = wl.Game().map
- for x=0,map.width-1 do
- for y=0,map.height-1 do
- local f = map:get_field(x,y)
- if f then
- -- add this field to the list as long as it has not movecaps swim
- if not f:has_caps("swimmable") then
- if f:has_caps("walkable") then
- fields[#fields+1] = f
- else
- -- editor disallows placement of immovables on dead and acid fields
- if f.immovable then
- fields[#fields+1] = f
- end
- end
- end
- end
- end
- end
-
- -- these variables will be used once a player or team owns more than half
- -- of the map's area
- local currentcandidate = "" -- Name of Team or Player
- local candidateisteam = false
- local remaining_time = 10 -- (dummy) -- time in secs, if == 0 -> victory
-
- -- Find all valid players
- local plrs = wl.Game().players
-
- -- send a message with the game type to all players
- broadcast(plrs, wc_name, wc_desc)
-
- -- Find all valid teams
- local teamnumbers = {} -- array with team numbers
- for idx,p in ipairs(plrs) do
- local team = p.team
- if team > 0 then
- local found = false
- for idy,t in ipairs(teamnumbers) do
- if t == team then
- found = true
- break
- end
- end
- if not found then
- teamnumbers[#teamnumbers+1] = team
- end
- end
- end
-
- local _landsizes = {}
- local function _calc_current_landsizes()
- -- init the landsizes for each player
- for idx,plr in ipairs(plrs) do
- _landsizes[plr.number] = 0
- end
-
- for idx,f in ipairs(fields) do
- -- check if field is owned by a player
- local o = f.owner
- if o then
- local n = o.number
- _landsizes[n] = _landsizes[n] + 1
- end
- end
- end
-
- function _calc_points()
- local teampoints = {} -- points of teams
- local maxplayerpoints = 0 -- the highest points of a player without team
- local maxpointsplayer = 0 -- the player
- local foundcandidate = false
-
- _calc_current_landsizes()
-
- for idx, p in ipairs(plrs) do
- local team = p.team
- if team == 0 then
- if maxplayerpoints < _landsizes[p.number] then
- maxplayerpoints = _landsizes[p.number]
- maxpointsplayer = p
- end
- else
- if not teampoints[team] then -- init the value
- teampoints[team] = 0
- end
- teampoints[team] = teampoints[team] + _landsizes[p.number]
- end
- end
-
- if maxplayerpoints > ( #fields / 2 ) then
- -- player owns more than half of the map's area
- foundcandidate = true
- if candidateisteam == false and currentcandidate == maxpointsplayer.name then
- remaining_time = remaining_time - 30
- else
- currentcandidate = maxpointsplayer.name
- candidateisteam = false
- remaining_time = 20 * 60 -- 20 minutes
- end
- else
- for idx, t in ipairs(teamnumbers) do
- if teampoints[t] > ( #fields / 2 ) then
- -- this team owns more than half of the map's area
- foundcandidate = true
- if candidateisteam == true and currentcandidate == t then
- remaining_time = remaining_time - 30
- else
- currentcandidate = t
- candidateisteam = true
- remaining_time = 20 * 60 -- 20 minutes
- end
- end
- end
- end
- if not foundcandidate then
- currentcandidate = ""
- candidateisteam = false
- remaining_time = 10
- end
- end
-
- function _send_state()
- local msg1 = game_status_territoral_lord.other1:format(currentcandidate)
- if candidateisteam then
- local teamstr = game_status_territoral_lord.team:format(currentcandidate)
- msg1 = game_status_territoral_lord.other1:format(teamstr)
- end
- msg1 = msg1 .. "\n"
- msg1 = msg1 .. game_status_territoral_lord.other2:format(remaining_time / 60)
-
- local msg2 = game_status_territoral_lord.player1
- msg2 = msg2 .. "\n"
- msg2 = msg2 .. game_status_territoral_lord.player2:format(remaining_time / 60)
-
- for idx, p in ipairs(plrs) do
- if candidateisteam and currentcandidate == p.team
- or not candidateisteam and currentcandidate == p.name then
- p:send_message(game_status.title, msg2, {popup = true})
- else
- p:send_message(game_status.title, msg1, {popup = true})
- end
- end
- end
-
- -- Start a new coroutine that checks for defeated players
- run(function()
- sleep(5000)
- check_player_defeated(plrs, lost_game.title, lost_game.body, wc_name, wc_version)
- end)
-
- -- here is the main loop!!!
- while true do
- -- Sleep 30 seconds == STATISTICS_SAMPLE_TIME
- sleep(30000)
-
- -- Check if a player or team is a candidate and update variables
- _calc_points()
-
- -- Do this stuff, if the game is over
- if remaining_time == 0 then
- for idx, p in ipairs(plrs) do
- p.see_all = 1
- if candidateisteam and currentcandidate == p.team
- or not candidateisteam and currentcandidate == p.name then
- p:send_message(won_game_over.title, won_game_over.body, {popup = true})
- wl.game.report_result(p, true, _landsizes[p.number], make_extra_data(p, wc_name, wc_version))
- else
- p:send_message(lost_game_over.title, lost_game_over.body, {popup = true})
- wl.game.report_result(p, false, _landsizes[p.number], make_extra_data(p, wc_name, wc_version))
- end
- end
- break
- end
-
- -- If there is a candidate, check whether we have to send an update
- if remaining_time % 300 == 0 then -- every 5 minutes (5 * 60 )
- _send_state()
- end
- end
- end
+ func = wc_func
}
=== modified file 'src/scripting/lua_bases.cc'
--- src/scripting/lua_bases.cc 2013-08-01 03:20:46 +0000
+++ src/scripting/lua_bases.cc 2013-08-04 14:52:28 +0000
@@ -185,7 +185,7 @@
/* RST
.. attribute:: number
- (RO) The number of this Player.
+ (RO) The number of the slot this Player is assigned to.
*/
int L_PlayerBase::get_number(lua_State * L) {
lua_pushuint32(L, m_pl);
Follow ups