Module:TacticsDrillsSolution

local cargo = mw.ext.cargo local List = require 'Module:ListUtil' local Hash = require 'Module:HashUtil' local MapLayout = require 'Module:MapLayout' local parse = require 'Module:ObjectArg'.parse local error = require 'Module:Error'.error local escq = require 'Module:EscQ'.main1

local map = MapLayout._map local ally = MapLayout._ally local enemy = MapLayout._enemy

local NUMBERS = {"one", "two", "three", "four"}

local parsePos = function (pos) if type(pos) == "table" then return pos[1], pos[2] end local x, y = mw.ustring.match(pos, "^([a-z])([0-9])$") if y then return x:byte-("a"):byte+1, tonumber(y) end end

local mapImage = function (frame, data) local terrain = {} local walls = {} for pos, life in pairs(data.walls) do		local x, y = parsePos(pos) walls[(y-1) * 6 + x] = life end for pos, life in pairs(walls) do		local dir = "" if life == 0 then local x, y = (pos-1) % 6, math.ceil(pos / 6) - 1 dir = "Debris_" .. ((x + y) % 2 == 0 and "A" or "B") else dir = dir .. (walls[pos + 6] and walls[pos + 6] ~= 0 and "N" or "") dir = dir .. (walls[pos + 1] and walls[pos + 1] ~= 0 and "E" or "") dir = dir .. (walls[pos - 6] and walls[pos - 6] ~= 0 and "S" or "") dir = dir .. (walls[pos - 1] and walls[pos - 1] ~= 0 and "W" or "") dir = #dir > 0 and dir or "Pillar" end terrain[pos] = frame:expandTemplate{ title = "Wall", args = { style = data.wallStyle, type = dir, hp = life }} end for _, unit in ipairs(data.allies) do		terrain[(unit.pos[2]-1) * 6 + unit.pos[1]] = tostring(ally{unit.name}) end for _, unit in ipairs(data.enemies) do		terrain[(unit.pos[2]-1) * 6 + unit.pos[1]] = tostring(enemy{unit.name}) end local aValue = ("a"):byte terrain = Hash.from_pairs(terrain, function (v, i)		if v == "" then return end		return string.char(aValue + ((i-1) % 6)) .. tostring(math.ceil(i / 6)), v	end) terrain.baseMap = data.baseMap terrain.backdrop = data.backdrop terrain.type = "TD" return map(terrain) end

local capitalize = function (s) return s:gsub('(%w)(%w*)', function (s1, s2) return s1:upper .. s2:lower end) end local describeInstruction = function (unit, newPos, action, target, options) if action == 'duo' then return ("Use %s's Duo skill."):format(unit.nameShort) end

local sMove, sAct = "", "" local deltaX, deltaY = newPos[1] - unit.pos[1], newPos[2] - unit.pos[2]

if deltaX ~= 0 or deltaY ~= 0 then if options.warp then sMove = ("Warp %s at position %s%d"):format(unit.nameShort, string.char(("a"):byte + newPos[1] - 1), newPos[2]) elseif deltaX == 0 or deltaY == 0 then sMove = ("Move %s %s space%s %s"):format(unit.nameShort, NUMBERS[math.abs(deltaX ~= 0 and deltaX or deltaY)], math.abs(deltaX ~= 0 and deltaX or deltaY) == 1 and '' or 's',				deltaY > 0 and 'up' or deltaY < 0 and 'down' or deltaX > 0 and 'right' or 'left') else sMove = ("Move %s %s space%s %s and %s space%s %s"):format(unit.nameShort,				NUMBERS[math.abs(deltaY)], math.abs(deltaY) == 1 and  or 's', deltaY > 0 and 'up' or 'down',				NUMBERS[math.abs(deltaX)], math.abs(deltaX) == 1 and  or 's', deltaX > 0 and 'right' or 'left') end end if action then if List.any({"attack", "heal", "rally"}, function (act) return act == action end) then sAct = ("%s %s%s"):format(action, (target.wall or target.name == target.nameShort) and 'the ' or '',				target.wall and "wall" or target.nameShort) else sAct = ("use %s on %s%s"):format(capitalize(action), target.name == target.nameShort and 'the ' or '',				target.nameShort) end end if sMove == '' then return ("%s%s with %s%s."):format(sAct:sub(1, 1):upper, sAct:sub(2),			unit.name == unit.nameShort and 'the ' or '', unit.nameShort) elseif sAct == '' then return sMove .. "."	else return ("%s and %s."):format(sMove, sAct) end end

local getPosition = function (unit, move) if not move then return end local newx, newy = parsePos(move) if not newy then newx, newy = move:match("^%s*(%-?%d+),(%-?%d+)%s*$") newx, newy = unit.pos[1] + tonumber(newx), unit.pos[2] + tonumber(newy) end if newx 6 or newy 8 then return nil, ('The new position would be out of the map: (%d,%d)'):format(newx, newy) end return {newx, newy} end

local executeInstruction = function (data, unit, isAlly, newPos, action, target, others) newPos = newPos or unit.pos

local text = "" if isAlly then text = describeInstruction(unit, newPos, action, target, others) end

if not action or action == "duo" then unit.pos = newPos elseif target.wall then if others.canto then local cantoPos; cantoPos, err = getPosition({pos=newPos}, others.canto) if err then return err end unit.pos, newPos = newPos, cantoPos text = text .. " Then " .. describeInstruction(unit, newPos, nil, nil, {}):gsub("M", "m", 1) end data.walls[target.wall] = data.walls[target.wall] - 1 unit.pos = newPos else local targetPosX, targetPosY = target.pos[1], target.pos[2] local vectorX, vectorY = targetPosX-newPos[1], targetPosY-newPos[2]

if others.drawback then newPos = {newPos[1] - vectorX, newPos[2] - vectorY} targetPosX, targetPosY = targetPosX - vectorX, targetPosY - vectorY elseif others.hitandrun then newPos = {newPos[1] - vectorX, newPos[2] - vectorY} elseif others.pivot then newPos = {newPos[1] + vectorX*2, newPos[2] + vectorY*2} elseif others.reposition then targetPosX, targetPosY = targetPosX - vectorX*2, targetPosY - vectorY*2 elseif others.shove then targetPosX, targetPosY = targetPosX + vectorX, targetPosY + vectorY elseif others.smite then targetPosX, targetPosY = targetPosX + vectorX*2, targetPosY + vectorY*2 elseif others.swap then newPos = {newPos[1] + vectorX, newPos[2] + vectorY} targetPosX, targetPosY = targetPosX - vectorX, targetPosY - vectorY end

if newPos[1] < 1 or newPos[1] > 6 or newPos[2] < 1 or newPos[2] > 8 then return error(("The unit (%s) would be out of map after action: (%d,%d)."):format(unit.nameShort, newPos[1], newPos[2])) elseif targetPosX < 1 or targetPosX > 6 or targetPosY < 1 or targetPosY > 8 then return error(("The target (%s) would be out of map after action: (%d,%d)."):format(target.nameShort, targetPosX, targetPosY)) end

if others.canto then local cantoPos, err = getPosition({pos=newPos}, others.canto) if err then return err end unit.pos, newPos = newPos, cantoPos text = text .. " Then " .. describeInstruction(unit, newPos, nil, nil, {}):gsub("M", "m", 1) end unit.pos = newPos target.pos = {targetPosX, targetPosY}

end if others.ko then List.delete_if(data.enemies, function (row) return row.pos == (isAlly and target or unit).pos end) end return text end

local getUnit = function (units, name, pos) pos = pos and {parsePos(pos)} local u = nil for i, unit in ipairs(units) do		if pos and pos[1] == unit.pos[1] and pos[2] == unit.pos[2] then if name and unit.name ~= name and unit.nameShort ~= name then return nil, ('The given position does not match the given unit: "%s" is searched while "%s" is on the cell at (%d,%d).'):format(name, unit.name, pos[1], pos[2]) end return unit elseif not pos and name and (unit.name == name or unit.nameShort == name) then if u then return nil, ('The given name (%s) cannot be interpreted because several units share this name.'):format(name) end u = unit end end return u end

local getCloseUnit = function (unitPos, unitRange, units, name, targetPos) if targetPos then return getUnit(units, name, targetPos) else local deltax, deltay = 0, unitRange for i = 1, unitRange * 4 do			local pos = {unitPos[1] + deltax, unitPos[2] + deltay} if pos[1] >= 1 and pos[1] <= 6 and pos[2] >= 1 and pos[2] <= 8 then local result, err = getUnit(units, name, pos) if result then return result, err end end if i <= unitRange then deltax, deltay = deltax + 1, deltay - 1 elseif i <= unitRange * 2 then deltax, deltay = deltax - 1, deltay - 1 elseif i <= unitRange * 3 then deltax, deltay = deltax - 1, deltay + 1 else deltax, deltay = deltax + 1, deltay + 1 end end end return nil, ("Failed to find requested unit (%s)."):format(name) end

local getAction = function (data, args, unit, isAlly, pos) pos = pos or unit.pos local options = { ko = args.ko and true, warp = args.warp and true,

drawback = args["draw back"] and true, hitandrun = args["hit and run"] and true, pivot = args.pivot and true, reposition = args.reposition and true, shove = args.shove and true, smite = args.smite and true, swap = args.swap and true,

canto = args.canto, }	local action, target, err

if args.duo then action = "duo" elseif args.attack then action = "attack" if data.walls[args.attack] then if type(data.walls[args.attack]) == "number" and data.walls[args.attack] > 0 then target = {wall=args.attack} else err = "Cannot attack an unbreakable nor breaked wall (" .. args.attack .. ")." end else target, err = getCloseUnit(pos, unit.range, isAlly and data.enemies or data.allies, args.attack, args.target) end elseif args.heal then action = "heal" target, err = getCloseUnit(pos, 1, isAlly and data.allies or data.enemies, args.heal, args.target) if not target then target = getCloseUnit(pos, 2, isAlly and data.allies or data.enemies, args.heal, args.target) end else action = args.assist and args.assist:lower if not action then _, action = List.find_if(				{"draw back", "pivot", "reposition", "shove", "smite", "swap",				"rally", "dance", "sing", "ardent sacrifice", "harsh command"},				function (assist) return args[assist] end			) end if action then options[action] = true target, err = getCloseUnit(pos, 1, isAlly and data.allies or data.enemies, args[action], args.target) end end return action, target, options, err end

local parseInstruction = function(data, instruction, instructIndex) local isAlly = true local unit, err = getUnit(data.allies, instruction.unit or instruction.hero, instruction.pos)

if err then return error(err) end if not unit then unit, err = getUnit(data.enemies, instruction.unit or instruction.enemy, instruction.pos) if err then return error(err) elseif not unit then return error(("Unknow unit on instruction %d."):format(instructIndex)) end isAlly = false end

local newPos, err = getPosition(unit, instruction.move or instruction.warp) if err then return error(err) end local action, target, options, err = getAction(data, instruction, unit, isAlly, newPos) if err then return error(err) end return executeInstruction(data, unit, isAlly, newPos, action, target, options) end

local parseInstructions = function (data, instructions) local s = "" for i, instruction in ipairs(instructions) do		local instructionText = parseInstruction(data, instruction, i)		if #instructionText > 0 then s = s .. '' .. instructionText .. '' end end return '' .. s .. '' end

local getReinforcementUnits = function (data, units, turn) for _, unit in ipairs(units) do		if #unit.Spawn ~= 0 then spawn = parse(unit.Spawn) if spawn.turn == turn then table.insert(data[unit.isAlly == "1" and "allies" or "enemies"], {					name = unit.Unit,					nameShort = unit.UnitShort,					pos = {parsePos(unit.Pos)},					range = tonumber(unit.Range),				}) end end end end

local parseUnits = function (initUnits) local allies, enemies = {}, {} for _, unit in ipairs(initUnits) do		if #unit.Spawn == 0 then local tbl = { name = unit.Unit, nameShort = unit.UnitShort, pos = {parsePos(unit.Pos)}, range = tonumber(unit.Range), }			if unit.isAlly == "1" then allies[#allies+1] = tbl else enemies[#enemies+1] = tbl end end end Hash.map(List.group_by(allies, function (u) return u.nameShort end), function (group)		if #group > 1 then for _, unit in ipairs(group) do			unit.name, unit.nameShort = unit.nameShort, unit.name		end end	end) Hash.map(List.group_by(enemies, function (u) return u.nameShort end), function (group)		if #group > 1 then for _, unit in ipairs(group) do			unit.name, unit.nameShort = unit.nameShort, unit.name		end end	end) return allies, enemies end

local main = function (args, frame) local initUnits = cargo.query('MapUnits,Units,WeaponTypes',		"IFNULL(CONCAT(Units.Name, ': ', Units.Title),Units.Name)=Unit,"..		"Units.Name=UnitShort,Pos,Spawn,IF(MapUnits.Properties__full LIKE '%is_ally%',1,0)=isAlly," ..		"if(WeaponTypes.Classes__full LIKE '%Close%',1,2)=Range", {			join = "MapUnits.Unit=Units.WikiName,Units.WeaponType=WeaponTypes.WikiName",			where = ("MapUnits._pageName='%s'"):format(escq(args.stage or tostring(mw.title.getCurrentTitle))),			groupBy = "MapUnits._ID",			orderBy = "MapUnits.Slot",			limit = 100		}) local data = { baseMap = args.baseMap, backdrop = args.backdrop, wallStyle = args.wallStyle, walls = parse(args.wall or '{}') }	data.allies, data.enemies = parseUnits(initUnits) local turn = 1

local s = "" while args['turn' .. tostring(turn)] do s = s .. (" Turn %d "):format(turn) s = s .. parseInstructions(data, parse(args['turn' .. tostring(turn)])) getReinforcementUnits(data, initUnits, turn+1) local tbl = mw.html.create('table'):addClass('wikitable'):addClass('default'):addClass('mw-collapsed'):addClass('mw-collapsible') tbl:tag('tr'):tag('th'):wikitext(('Map visual at the end of turn %d'):format(turn)) tbl:tag('tr'):tag('td'):wikitext(mapImage(frame, data)) s = s .. tostring(tbl) turn = turn + 1 end if not args['turn1'] or #parse(args['turn1']) == 0 then s = s .. ""	end return s end

return require 'Module:MakeMWModule'.makeMWModule { main = main }