← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/bug-1759857-territorial-lord into lp:widelands

 

GunChleoc has proposed merging lp:~widelands-dev/widelands/bug-1759857-territorial-lord into lp:widelands.

Commit message:
Pulled out common code for Territorial Lord and Territorial Time. Ensure that check_player_defeated is called before points are calculated.

Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #1759857 in widelands: ""Lua Coroutine Failed" as Inbox Message"
  https://bugs.launchpad.net/widelands/+bug/1759857

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/bug-1759857-territorial-lord/+merge/355860

Since Territorial Lord and Territorial Time are almost the same win condition, I pulled out common code while I was touching them anyway.

Needs thorough testing to make sure that the crash is indeed gone. I left some NOCOM logging in there to help with debugging, and that needs to be removed before this branch gets merged.
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/bug-1759857-territorial-lord into lp:widelands.
=== added file 'data/scripting/win_conditions/territorial_functions.lua'
--- data/scripting/win_conditions/territorial_functions.lua	1970-01-01 00:00:00 +0000
+++ data/scripting/win_conditions/territorial_functions.lua	2018-09-28 18:09:51 +0000
@@ -0,0 +1,298 @@
+-- RST
+-- territorial_functions.lua
+-- ---------------------------
+--
+-- This file contains common code for the "Territorial Lord" and "Territorial Time" win conditions.
+
+set_textdomain("win_conditions")
+
+include "scripting/richtext.lua"
+include "scripting/win_conditions/win_condition_texts.lua"
+
+local team_str = _"Team %i"
+local wc_has_territory = _"%1$s has %2$3.0f%% of the land (%3$i of %4$i)."
+local wc_had_territory = _"%1$s had %2$3.0f%% of the land (%3$i of %4$i)."
+
+-- RST
+-- .. function:: get_buildable_fields()
+--
+--    Collects all fields that are buildable
+--
+--    :returns: a table with the map's buildable fields
+--
+function get_buildable_fields()
+   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.is_buildable then
+            table.insert(fields, f)
+         end
+      end
+   end
+   print("NOCOM Found " .. #fields .. " buildable fields")
+   return fields
+end
+
+-- RST
+-- .. function:: count_owned_fields_for_all_players(fields, players)
+--
+--    Counts all owned fields for each player.
+--
+--    :arg fields: Table of all buildable fields
+--    :arg players: Table of all players
+--
+--    :returns: a table with ``playernumber = count_of_owned_fields``  entries
+--
+local function count_owned_fields_for_all_players(fields, players)
+   local owned_fields = {}
+   -- init the landsizes for each player
+   for idx,plr in ipairs(players) do
+      owned_fields[plr.number] = 0
+   end
+
+   for idx,f in ipairs(fields) do
+      -- check if field is owned by a player
+      local owner = f.owner
+      if owner then
+         local owner_number = owner.number
+         if owned_fields[owner_number] == nil then
+            -- In case player was defeated and lost all their warehouses, make sure they don't count
+            owned_fields[owner_number] = -1
+         elseif owned_fields[owner_number] >= 0 then
+            owned_fields[owner_number] = owned_fields[owner_number] + 1
+         end
+      end
+   end
+   return owned_fields
+end
+
+
+-- Used by calculate_territory_points keep track of when the winner changes
+local winning_players = {}
+local winning_teams = {}
+
+
+-- RST
+-- .. data:: territory_points
+--
+--    This table contains information about the current points and winning status for all
+--    players and teams:
+--
+--    .. code-block:: lua
+--
+--       territory_points = {
+--          -- The currently winning team, if any. -1 means that no team is currently winning.
+--          last_winning_team = -1,
+--          -- The currently winning player, if any. -1 means that no player is currently winning.
+--          last_winning_player = -1,
+--          -- Remaining time in secs for victory by > 50% territory. Default value is also used to calculate whether to send a report to players.
+--          remaining_time = 10,
+--          -- Points by player
+--          all_player_points = {},
+--          -- Points by rank, used to generate messages to the players
+--          points = {}
+--       }
+--
+territory_points = {
+   -- TODO(GunChleoc): We want to be able to list multiple winners in case of a draw.
+   last_winning_team = -1,
+   last_winning_player = -1,
+   remaining_time = 10,
+   all_player_points = {},
+   points = {}
+}
+
+-- RST
+-- .. function:: calculate_territory_points(fields, players, wc_descname, wc_version)
+--
+--    First checks if a player was defeated, then fills the ``territory_points`` table
+--    with current data.
+--
+--    :arg fields: Table of all buildable fields
+--    :arg players: Table of all players
+--    :arg wc_descname: String with the win condition's descname
+--    :arg wc_version: Number with the win condition's descname
+--
+function calculate_territory_points(fields, players, wc_descname, wc_version)
+   -- A player might have been defeated since the last calculation
+   check_player_defeated(players, lost_game.title, lost_game.body, wc_descname, wc_version)
+
+   local points = {} -- tracking points of teams and players without teams
+   local territory_was_kept = false
+
+   territory_points.all_player_points = count_owned_fields_for_all_players(fields, players)
+   local ranked_players = rank_players(territory_points.all_player_points, players)
+
+   -- Check if we have a winner. The table was sorted, so we can simply grab the first entry.
+   local winning_points = -1
+   if ranked_players[1].points > ( #fields / 2 ) then
+      winning_points = ranked_players[1].points
+   end
+
+   -- Calculate which team or player is the current winner, and whether the winner has changed
+   for tidx, teaminfo in ipairs(ranked_players) do
+      local is_winner = teaminfo.points == winning_points
+      if teaminfo.team ~= 0 then
+         points[#points + 1] = { team_str:format(teaminfo.team), teaminfo.points }
+         if is_winner then
+            print("NOCOM Winner is team " .. teaminfo.team .. " with " .. teaminfo.points .. " points")
+            territory_was_kept = winning_teams[teaminfo.team] ~= nil
+            winning_teams[teaminfo.team] = true
+            territory_points.last_winning_team = teaminfo.team
+            territory_points.last_winning_player = -1
+         else
+            winning_teams[teaminfo.team] = nil
+         end
+      end
+
+      for pidx, playerinfo in ipairs(teaminfo.players) do
+         if teaminfo.points ~= playerinfo.points then
+            winning_players[playerinfo.number] = nil
+         elseif is_winner and teaminfo.team == 0 then
+            print("NOCOM Winner is player " .. playerinfo.number .. " with " .. playerinfo.points .. " points")
+            territory_was_kept = winning_players[playerinfo.number] ~= nil
+            winning_players[playerinfo.number] = true
+            territory_points.last_winning_player = playerinfo.number
+            territory_points.last_winning_team = -1
+         else
+            winning_players[playerinfo.number] = nil
+         end
+         if teaminfo.team == 0 and players[playerinfo.number] ~= nil then
+            points[#points + 1] = { players[playerinfo.number].name, playerinfo.points }
+         end
+      end
+   end
+
+   -- Set the remaining time according to whether the winner is still the same
+   if territory_was_kept then
+      -- Still the same winner
+      territory_points.remaining_time = territory_points.remaining_time - 30
+      print("NOCOM Territory was kept by " .. territory_points.last_winning_team .. " - " .. territory_points.last_winning_player .. ". Remaining time: " .. territory_points.remaining_time)
+   elseif winning_points == -1 then
+      -- No winner. This value is used to calculate whether to send a report to players.
+      territory_points.remaining_time = 10
+   else
+      -- Winner changed
+      territory_points.remaining_time = 20 * 60 -- 20 minutes
+      print("NOCOM NEW aqcuisition by " .. territory_points.last_winning_team .. " - " .. territory_points.last_winning_player .. ". Remaining time: " .. territory_points.remaining_time)
+   end
+   territory_points.points = points
+end
+
+-- RST
+-- .. function:: territory_status(fields, has_had)
+--
+--    Returns a string containing the current land percentages of players/teams
+--    for messages to the players
+--
+--    :arg fields: Table of all buildable fields
+--    :arg has_had: Use "has" for an interim message, "had" for a game over message.
+--
+--    :returns: a richtext-formatted string with information on current points for each player/team
+--
+function territory_status(fields, has_had)
+   local function _percent(part, whole)
+      return (part * 100) / whole
+   end
+
+   local msg = ""
+   for i=1,#territory_points.points do
+      if (has_had == "has") then
+         msg = msg ..
+            li(
+               (wc_has_territory):bformat(
+                  territory_points.points[i][1],
+                  _percent(territory_points.points[i][2], #fields),
+                  territory_points.points[i][2],
+                  #fields))
+      else
+         msg = msg ..
+            li(
+               (wc_had_territory):bformat(
+                  territory_points.points[i][1],
+                  _percent(territory_points.points[i][2], #fields),
+                  territory_points.points[i][2],
+                  #fields))
+      end
+
+   end
+   return p(msg)
+end
+
+-- RST
+-- .. function:: winning_status_header()
+--
+--    Returns a string containing a status message header for a winning player
+--
+--    :returns: a richtext-formatted string with header information for a winning player
+--
+function winning_status_header()
+   set_textdomain("win_conditions")
+   local remaining_minutes = math.max(0, math.floor(territory_points.remaining_time / 60))
+
+   local message = p(_"You own more than half of the map’s area.")
+   message = message .. p(ngettext("Keep it for %i more minute to win the game.",
+             "Keep it for %i more minutes to win the game.",
+             remaining_minutes))
+         :format(remaining_minutes)
+   return message
+end
+
+-- RST
+-- .. function:: losing_status_header(players)
+--
+--    Returns a string containing a status message header for a losing player
+--
+--    :arg players: Table of all players
+--
+--    :returns: a richtext-formatted string with header information for a losing player
+--
+function losing_status_header(players)
+   set_textdomain("win_conditions")
+   local winner_name = "Error"
+   if territory_points.last_winning_team >= 0 then
+      winner_name = team_str:format(territory_points.last_winning_team)
+   elseif territory_points.last_winning_player >= 0 then
+      winner_name = players[territory_points.last_winning_player].name
+   end
+   local remaining_minutes = math.max(0, math.floor(territory_points.remaining_time / 60))
+
+   local message = p(_"%s owns more than half of the map’s area."):format(winner_name)
+   message = message .. p(ngettext("You’ve still got %i minute to prevent a victory.",
+             "You’ve still got %i minutes to prevent a victory.",
+             remaining_minutes))
+         :format(remaining_minutes)
+   return message
+end
+
+-- RST
+-- .. function:: territory_game_over(fields, players, wc_descname, wc_version)
+--
+--    Updates the territory points and sends game over reports
+--
+--    :arg fields: Table of all buildable fields
+--    :arg players: Table of all players
+--
+function territory_game_over(fields, players, wc_descname, wc_version)
+   calculate_territory_points(fields, players, wc_descname, wc_version)
+
+   for idx, pl in ipairs(players) do
+      pl.see_all = 1
+
+      local wonmsg = won_game_over.body .. game_status.body
+      local lostmsg = lost_game_over.body .. game_status.body
+      for i=1,#territory_points.points do
+         if territory_points.points[i][1] == team_str:format(pl.team) or territory_points.points[i][1] == pl.name then
+            if territory_points.points[i][2] >= territory_points.points[1][2] then
+               pl:send_message(won_game_over.title, wonmsg .. territory_status(territory_points.points, fields, "had"))
+               wl.game.report_result(pl, 1, make_extra_data(pl, wc_descname, wc_version, {score=territory_points.all_player_points[pl.number]}))
+            else
+               pl:send_message(lost_game_over.title, lostmsg .. territory_status(territory_points.points, fields, "had"))
+               wl.game.report_result(pl, 0, make_extra_data(pl, wc_descname, wc_version, {score=territory_points.all_player_points[pl.number]}))
+            end
+         end
+      end
+   end
+end

=== modified file 'data/scripting/win_conditions/territorial_lord.lua'
--- data/scripting/win_conditions/territorial_lord.lua	2017-05-12 13:50:26 +0000
+++ data/scripting/win_conditions/territorial_lord.lua	2018-09-28 18:09:51 +0000
@@ -6,6 +6,7 @@
 include "scripting/messages.lua"
 include "scripting/table.lua"
 include "scripting/win_conditions/win_condition_functions.lua"
+include "scripting/win_conditions/territorial_functions.lua"
 
 set_textdomain("win_conditions")
 
@@ -30,186 +31,45 @@
       -- set the objective with the game type for all players
       broadcast_objective("win_condition", wc_descname, wc_desc)
 
+      -- Configure how long the winner has to hold on to the territory
+      local time_to_keep_territory = 20 * 60 -- 20 minutes
+      -- time in secs, if == 0 -> victory
+      territory_points.remaining_time = time_to_keep_territory
+
       -- 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 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
-
-      local 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
+      local fields = get_buildable_fields()
 
       local function _send_state()
          set_textdomain("win_conditions")
-         local candidate = currentcandidate
-         if candidateisteam then
-            candidate = (_"Team %i"):format(currentcandidate)
-         end
-         local msg1 = p(_"%s owns more than half of the map’s area."):format(candidate)
-         msg1 = msg1 .. p(ngettext("You’ve still got %i minute to prevent a victory.",
-                   "You’ve still got %i minutes to prevent a victory.",
-                   remaining_time / 60))
-               :format(remaining_time / 60)
-
-         local msg2 = p(_"You own more than half of the map’s area.")
-         msg2 = msg2 .. p(ngettext("Keep it for %i more minute to win the game.",
-                   "Keep it for %i more minutes to win the game.",
-                   remaining_time / 60))
-               :format(remaining_time / 60)
 
          for idx, player in ipairs(plrs) do
-            if candidateisteam and currentcandidate == player.team
-               or not candidateisteam and currentcandidate == player.name then
-               send_message(player, game_status.title, msg2, {popup = true})
+            local msg = ""
+            if territory_points.last_winning_team == player.team or territory_points.last_winning_player == player.number then
+               msg = msg .. winning_status_header() .. vspace(8)
             else
-               send_message(player, game_status.title, msg1, {popup = true})
+               msg = msg .. losing_status_header(plrs) .. vspace(8)
             end
+            msg = msg .. vspace(8) .. game_status.body .. territory_status(fields, "has")
+         send_message(player, game_status.title, msg, {popup = true})
          end
       end
 
-      -- Start a new coroutine that checks for defeated players
-      run(function()
-         while remaining_time ~= 0 do
-            sleep(5000)
-            check_player_defeated(plrs, lost_game.title, lost_game.body, wc_descname, wc_version)
-         end
-      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()
+         calculate_territory_points(fields, plrs, wc_descname, wc_version)
 
          -- 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)
-                  wl.game.report_result(p, 1, make_extra_data(p, wc_descname, wc_version, {score=_landsizes[p.number]}))
-               else
-                  p:send_message(lost_game_over.title, lost_game_over.body)
-                  wl.game.report_result(p, 0, make_extra_data(p, wc_descname, wc_version, {score=_landsizes[p.number]}))
-               end
-            end
+         if territory_points.remaining_time == 0 then
+            territory_game_over(fields, plrs, wc_descname, wc_version)
             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 )
+         if (territory_points.last_winning_team >= 0 or territory_points.last_winning_player >= 0) and territory_points.remaining_time >= 0 and territory_points.remaining_time % 300 == 0 then
             _send_state()
          end
       end

=== modified file 'data/scripting/win_conditions/territorial_time.lua'
--- data/scripting/win_conditions/territorial_time.lua	2017-05-12 13:50:26 +0000
+++ data/scripting/win_conditions/territorial_time.lua	2018-09-28 18:09:51 +0000
@@ -9,6 +9,7 @@
 include "scripting/messages.lua"
 include "scripting/table.lua"
 include "scripting/win_conditions/win_condition_functions.lua"
+include "scripting/win_conditions/territorial_functions.lua"
 
 set_textdomain("win_conditions")
 
@@ -25,9 +26,7 @@
    "that area for at least 20 minutes, or the one with the most territory " ..
    "after 4 hours, whichever comes first."
 )
-local wc_has_territory = _"%1$s has %2$3.0f%% of the land (%3$i of %4$i)."
-local wc_had_territory = _"%1$s had %2$3.0f%% of the land (%3$i of %4$i)."
-local team_str = _"Team %i"
+
 
 return {
    name = wc_name,
@@ -39,217 +38,40 @@
       broadcast_objective("win_condition", wc_descname, wc_desc)
 
       -- 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
+      local fields = get_buildable_fields()
 
       -- variables to track the maximum 4 hours of gametime
       local remaining_max_time = 4 * 60 * 60 -- 4 hours
 
-      -- 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 teams
-      local teamnumbers = {} -- array with team numbers
-      for idx,pl in ipairs(plrs) do
-         local team = pl.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
-
-      local function _calc_points()
-         local teampoints = {}     -- points of teams
-         local points = {} -- tracking points of teams and players without 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, pl in ipairs(plrs) do
-            local team = pl.team
-            if team == 0 then
-               if maxplayerpoints < _landsizes[pl.number] then
-                  maxplayerpoints = _landsizes[pl.number]
-                  maxpointsplayer = pl
-               end
-               points[#points + 1] = { pl.name, _landsizes[pl.number] }
-            else
-               if not teampoints[team] then -- init the value
-                  teampoints[team] = 0
-               end
-               teampoints[team] = teampoints[team] + _landsizes[pl.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
-         end
-         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 == team_str:format(t) then
-                  remaining_time = remaining_time - 30
-               else
-                  currentcandidate = team_str:format(t)
-                  candidateisteam = true
-                  remaining_time = 20 * 60 -- 20 minutes
-               end
-            end
-            points[#points + 1] = { team_str:format(t), teampoints[t] }
-         end
-         if not foundcandidate then
-            remaining_time = 10
-         end
-         return points
-      end
-
-      local function _percent(part, whole)
-         return (part * 100) / whole
-      end
-
-      -- Helper function to get the points that the leader has
-      local function _maxpoints(points)
-         local max = 0
-         for i=1,#points do
-            if points[i][2] > max then max = points[i][2] end
-         end
-         return max
-      end
-
-      -- Helper function that returns a string containing the current
-      -- land percentages of players/teams.
-      local function _status(points, has_had)
-         local msg = ""
-         for i=1,#points do
-            if (has_had == "has") then
-               msg = msg ..
-                  li(
-                     (wc_has_territory):bformat(
-                        points[i][1],
-                        _percent(points[i][2], #fields),
-                        points[i][2],
-                        #fields))
-            else
-               msg = msg ..
-                  li(
-                     (wc_had_territory):bformat(
-                        points[i][1],
-                        _percent(points[i][2], #fields),
-                        points[i][2],
-                        #fields))
-            end
-
-         end
-         return p(msg)
-      end
-
-      local function _send_state(points)
+      local function _send_state()
          set_textdomain("win_conditions")
-         local msg1 = p(_"%s owns more than half of the map’s area."):format(currentcandidate)
-         msg1 = msg1 .. p(ngettext("You’ve still got %i minute to prevent a victory.",
-                   "You’ve still got %i minutes to prevent a victory.",
-                   remaining_time // 60))
-               :format(remaining_time // 60)
-         msg1 = p(msg1)
-
-         local msg2 = p(_"You own more than half of the map’s area.")
-         msg2 = msg2 .. p(ngettext("Keep it for %i more minute to win the game.",
-                   "Keep it for %i more minutes to win the game.",
-                   remaining_time // 60))
-               :format(remaining_time // 60)
-         msg2 = p(msg2)
-
-         for idx, pl in ipairs(plrs) do
+
+         local remaining_max_minutes = remaining_max_time // 60
+         for idx, player in ipairs(plrs) do
             local msg = ""
-            if remaining_time < remaining_max_time and _maxpoints(points) > ( #fields / 2 ) then
-               if candidateisteam and currentcandidate == team_str:format(pl.team)
-                  or not candidateisteam and currentcandidate == pl.name then
-                  msg = msg .. msg2 .. vspace(8)
+            if territory_points.remaining_time < remaining_max_time and
+               (territory_points.last_winning_team >= 0 or territory_points.last_winning_player >= 0) then
+               if territory_points.last_winning_team == player.team or territory_points.last_winning_player == player.number then
+                  msg = msg .. winning_status_header() .. vspace(8)
                else
-                  msg = msg .. msg1 .. vspace(8)
+                  msg = msg .. losing_status_header(plrs) .. vspace(8)
                end
                -- TRANSLATORS: Refers to "You own more than half of the map’s area. Keep it for x more minute(s) to win the game."
                msg = msg .. p((ngettext("Otherwise the game will end in %i minute.",
                             "Otherwise the game will end in %i minutes.",
-                            remaining_max_time // 60))
-                  :format(remaining_max_time // 60))
+                            remaining_max_minutes))
+                  :format(remaining_max_minutes))
             else
                msg = msg .. p((ngettext("The game will end in %i minute.",
                             "The game will end in %i minutes.",
-                            remaining_max_time // 60))
-                  :format(remaining_max_time // 60))
+                            remaining_max_minutes))
+                  :format(remaining_max_minutes))
             end
-            msg = msg .. vspace(8) .. game_status.body .. _status(points, "has")
-            send_message(pl, game_status.title, msg, {popup = true})
+            msg = msg .. vspace(8) .. game_status.body .. territory_status(fields, "has")
+            send_message(player, game_status.title, msg, {popup = true})
          end
       end
 
-      -- Start a new coroutine that checks for defeated players
-      run(function()
-         while remaining_time ~= 0 and remaining_max_time > 0 do
-            sleep(5000)
-            check_player_defeated(plrs, lost_game.title,
-               lost_game.body, wc_descname, wc_version)
-         end
-      end)
-
       -- here is the main loop!!!
       while true do
          -- Sleep 30 seconds == STATISTICS_SAMPLE_TIME
@@ -259,44 +81,22 @@
 
          -- Check if a player or team is a candidate and update variables
          -- Returns the names and points for the teams and players without a team
-         local points = _calc_points()
+         calculate_territory_points(fields, plrs, wc_descname, wc_version)
 
          -- Game is over, do stuff after loop
-         if remaining_time <= 0 or remaining_max_time <= 0 then break end
+         if territory_points.remaining_time <= 0 or remaining_max_time <= 0 then break end
 
          -- at the beginning send remaining max time message only each 30 minutes
          -- if only 30 minutes or less are left, send each 5 minutes
          -- also check if there is a candidate and we need to send an update
          if ((remaining_max_time < (30 * 60) and remaining_max_time % (5 * 60) == 0)
                or remaining_max_time % (30 * 60) == 0)
-               or remaining_time % 300 == 0 then
-            _send_state(points)
+               or territory_points.remaining_time % 300 == 0 then
+            _send_state()
          end
       end
 
-      local points = _calc_points()
-      table.sort(points, function(a,b) return a[2] > b[2] end)
-
       -- Game has ended
-      for idx, pl in ipairs(plrs) do
-         pl.see_all = 1
-
-         maxpoints = points[1][2]
-         local wonmsg = won_game_over.body
-         wonmsg = wonmsg .. game_status.body
-         local lostmsg = lost_game_over.body
-         lostmsg = lostmsg .. game_status.body
-         for i=1,#points do
-            if points[i][1] == team_str:format(pl.team) or points[i][1] == pl.name then
-               if points[i][2] >= maxpoints then
-                  pl:send_message(won_game_over.title, wonmsg .. _status(points, "had"))
-                  wl.game.report_result(pl, 1, make_extra_data(pl, wc_descname, wc_version, {score=_landsizes[pl.number]}))
-               else
-                  pl:send_message(lost_game_over.title, lostmsg .. _status(points, "had"))
-                  wl.game.report_result(pl, 0, make_extra_data(pl, wc_descname, wc_version, {score=_landsizes[pl.number]}))
-               end
-            end
-         end
-      end
+      territory_game_over(fields, plrs, wc_descname, wc_version)
    end
 }

=== modified file 'data/scripting/win_conditions/win_condition_functions.lua'
--- data/scripting/win_conditions/win_condition_functions.lua	2017-05-11 17:29:55 +0000
+++ data/scripting/win_conditions/win_condition_functions.lua	2018-09-28 18:09:51 +0000
@@ -144,3 +144,96 @@
    local plrs = wl.Game().players
    plrs[1]:add_objective(name, title, body)
 end
+
+
+-- RST
+-- .. function:: rank_players(all_player_points, plrs)
+--
+--    Rank the players and teams according to the highest points
+--
+--    :arg all_player_points:    A table of ``playernumber = points`` entries for all players
+--    :arg plrs:                 A table of all Player objects
+--
+--    :returns: A table with ranked player and team points, sorted by points descending. Example:
+--
+--    .. code-block:: lua
+--
+--       {
+--          -- A player without team
+--          {
+--             team = 0,
+--             points = 1000,
+--             players = {
+--                { "number" = 5, "points" = 1000 }
+--             }
+--          },
+--          -- This team has a draw with player 5
+--          {
+--             team = 1,
+--             points = 1000,
+--             players = {
+--                { "number" = 2, "points" = 500 }
+--                { "number" = 3, "points" = 400 }
+--                { "number" = 4, "points" = 100 }
+--          },
+--          -- Another player without team
+--          {
+--             team = 0,
+--             points = 800,
+--             players = {
+--                { "number" = 1, "points" = 800 }
+--             }
+--          },
+--       }
+--
+function rank_players(all_player_points, plrs)
+   local ranked_players_and_teams = {}
+   local team_points = {}
+
+   -- Add points for players without teams and calculate team points
+   for idx, player in ipairs(plrs) do
+      local player_points = all_player_points[player.number]
+      local team = player.team
+      if team == 0 then
+         -- Player without team - add it directly
+         local team_table = {
+            team = 0,
+            points = player_points,
+            players = {
+               { number = player.number, points = player_points }
+            }
+         }
+         table.insert(ranked_players_and_teams, team_table)
+      else
+         -- Team player - add to team points
+         if not team_points[team] then
+            team_points[team] = 0
+         end
+         team_points[team] = team_points[team] + player_points
+      end
+   end
+
+   -- Add points for teams and their players
+   for team, points in pairs(team_points) do
+      local team_table = {
+         team = team,
+         points = points,
+         players = {}
+      }
+      for idx, player in ipairs(plrs) do
+         if player.team == team then
+            table.insert(team_table.players, { number = player.number, points = all_player_points[player.number] })
+         end
+      end
+      table.insert(ranked_players_and_teams, team_table)
+   end
+
+   -- Sort the players by points descending
+   for ids, team in pairs(ranked_players_and_teams) do
+      table.sort(team.players, function(a,b) return a["points"] > b["points"] end)
+   end
+
+   -- Sort the teams by points descending
+   table.sort(ranked_players_and_teams, function(a,b) return a["points"] > b["points"] end)
+   return ranked_players_and_teams
+end

=== modified file 'src/scripting/lua_map.cc'
--- src/scripting/lua_map.cc	2018-09-11 07:24:27 +0000
+++ src/scripting/lua_map.cc	2018-09-28 18:09:51 +0000
@@ -5940,6 +5940,7 @@
    PROP_RO(LuaField, initial_resource_amount),
    PROP_RO(LuaField, claimers),
    PROP_RO(LuaField, owner),
+   PROP_RO(LuaField, is_buildable),
    {nullptr, nullptr, nullptr},
 };
 
@@ -6267,6 +6268,23 @@
 }
 
 /* RST
+   .. attribute:: is_buildable
+
+      Returns :const:`true` if a flag or building could be built on this field,
+      independently of whether anybody currently owns this field.
+*/
+int LuaField::get_is_buildable(lua_State* L) {
+	const NodeCaps caps = fcoords(L).field->nodecaps();
+	const bool is_buildable = (caps & BUILDCAPS_FLAG)
+			|| (caps & BUILDCAPS_SMALL)
+			|| (caps & BUILDCAPS_MEDIUM)
+			|| (caps & BUILDCAPS_BIG)
+			|| (caps & BUILDCAPS_MINE);
+	lua_pushboolean(L, is_buildable);
+	return 1;
+}
+
+/* RST
    .. attribute:: claimers
 
       (RO) An :class:`array` of players that have military influence over this

=== modified file 'src/scripting/lua_map.h'
--- src/scripting/lua_map.h	2018-09-02 11:44:52 +0000
+++ src/scripting/lua_map.h	2018-09-28 18:09:51 +0000
@@ -1413,6 +1413,7 @@
 	int get_initial_resource_amount(lua_State*);
 	int get_claimers(lua_State*);
 	int get_owner(lua_State*);
+	int get_is_buildable(lua_State*);
 
 	/*
 	 * Lua methods


Follow ups