← Back to team overview

widelands-dev team mailing list archive

[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