Files
2026-09-24 05:17:32 -04:00

756 lines
28 KiB
Lua

-- Drives five src/online/Client instances through a v2 tournament against a
-- real ../pokeserver over TCP: tour_create with a party, joins, tour_start,
-- tour_bye, tour_match -> a child room -> a full Gen 1 LinkBattle over
-- roomSession(), reports, room_result, bracket advance, tour_over; an outside
-- spectator watching through match_start_spectate and side-tagged room_msg; a
-- mid-battle socket kill with resume and replay; a report("error") forfeit;
-- the server-side shot clock; and tour_closed on a kick. Runs under love
-- because plain luajit on macOS has no luasocket.
--
-- POKEPORT_TOUCH=0 POKEPORT_DRIVER=tests/drivers/online_tour_smoke.lua love .
--
-- headless, with a wall-clock cap (script -q is useless on macOS):
-- perl -e 'alarm 900; exec @ARGV' \
-- python3 -c 'import pty,sys; pty.spawn(sys.argv[1:])' \
-- /Applications/love.app/Contents/MacOS/love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local PORT = tonumber(os.getenv("POKEPORT_LINK_PORT") or "") or 17778
local SERVER = os.getenv("POKESERVER_DIR") or "../pokeserver"
local SHOT_MS = 6000
local failures = 0
local function check(cond, msg)
if cond then
U.log("ok ", msg)
else
failures = failures + 1
U.log("FAIL", msg)
end
end
local function finish()
U.log(failures == 0 and "online tournament smoke passed"
or (failures .. " online tournament smoke check(s) failed"))
love.event.quit(failures == 0 and 0 or 1)
while true do coroutine.yield() end
end
local probe = io.open(SERVER .. "/server.js", "r")
if not probe then
U.log("FAIL", SERVER .. "/server.js is not checked out")
love.event.quit(1)
return
end
probe:close()
local pidFile = "/tmp/pokeserver_tour_" .. PORT .. ".pid"
os.execute(("(cd %q && PORT=%d HTTP_PORT=%d POKESERVER_TEST_SHOT_MS=%d " ..
"node server.js >/tmp/pokeserver_tour.log 2>&1 & echo $! > %q)")
:format(SERVER, PORT, PORT + 1, SHOT_MS, pidFile))
local function stopServer()
local handle = io.open(pidFile, "r")
if handle then
local pid = handle:read("*l")
handle:close()
if pid and pid ~= "" then os.execute("kill " .. pid .. " >/dev/null 2>&1") end
end
os.remove(pidFile)
end
local Net = require("src.link.Net")
local function reachable()
local sock = Net.new()
if not sock:connectTCP("127.0.0.1:" .. PORT) then return false end
for _ = 1, 30 do
sock:update()
if sock.closed then
pcall(function() sock:close() end)
return false
end
if not sock.connecting then
pcall(function() sock:close() end)
return true
end
end
pcall(function() sock:close() end)
return false
end
local up = false
for _ = 1, 300 do
up = reachable()
if up then break end
U.wait(2)
end
if not up then
check(false, "the spawned pokeserver answered on 127.0.0.1:" .. PORT)
stopServer()
return finish()
end
U.log("pokeserver up on", PORT, "shot clock", SHOT_MS .. "ms")
local Data = game.data
local Input = require("src.core.Input")
local LinkBattle = require("src.link.LinkBattle")
local Pokemon = require("src.pokemon.Pokemon")
local Protocol = require("src.link.Protocol")
local SaveData = require("src.core.SaveData")
local Version = require("src.core.Version")
local PROFILE = {
engine = 1, version = "red", engineVersion = Version.engine,
apiVersion = Version.modApi, fingerprint = "tour-smoke",
rulesetId = "gen1_faithful", kind = "vanilla", rule = { partySize = 1 },
}
local RULE = { partySize = 1 }
local function copy(v)
if type(v) ~= "table" then return v end
local out = {}
for k, item in pairs(v) do out[k] = copy(item) end
return out
end
local EVENTS = { "match_start", "match_end", "tour_match", "tour_spectate",
"tour_bye", "tour_over", "error" }
local bots = {}
local function newBot(name, species)
package.loaded["src.online.Client"] = nil
local C = require("src.online.Client")
C.reset()
local bot = { name = name, C = C, ev = {}, seen = {} }
C.configure({
relayAddress = "127.0.0.1:" .. PORT,
connect = function(address)
local net = Net.new()
if not net:connectTCP(address or ("127.0.0.1:" .. PORT)) then
return nil, net.error or "no relay"
end
net.mode = "onlineLobby"
net.v2 = true
bot.net = net
return net
end,
})
for _, event in ipairs(EVENTS) do
local key = event
C.on(key, function(payload)
bot.ev[key] = payload
bot.seen[key] = (bot.seen[key] or 0) + 1
end)
end
C.on("tournament", function(t)
if t and t.stage == "finished" and not bot.champFromState then
bot.champFromState = t.champion
end
end)
if species then
local save = SaveData.newGame()
save.player.name = name
save.party = { Pokemon.new(Data, species, 50) }
bot.save = save
bot.party = Protocol.packParty(save.party)
end
bots[#bots + 1] = bot
return bot
end
local function pump(n)
for _ = 1, n or 1 do
for _, bot in ipairs(bots) do bot.C.update(1 / 60) end
coroutine.yield()
end
end
local function waitFor(fn, frames, what)
for _ = 1, frames or 600 do
if fn() then return true end
pump(1)
end
if what then check(false, "timed out waiting for " .. tostring(what)) end
return false
end
local function connectAll(list)
for _, bot in ipairs(list) do
bot.C.connect({ name = bot.name, profiles = { PROFILE } })
end
local ok = waitFor(function()
for _, bot in ipairs(list) do
if bot.C.state() ~= "online" then return false end
end
return true
end, 900, "every client to come online")
return ok
end
local function headless(bot)
local stack = { list = {} }
function stack:push(state, ...)
table.insert(self.list, state)
if state.enter then state:enter(...) end
end
function stack:pop() return table.remove(self.list) end
function stack:top() return self.list[#self.list] end
function stack:update(dt)
local top = self:top()
if top and top.update then top:update(dt) end
end
return { data = Data, input = Input, stack = stack, save = bot.save }
end
-- ------------------------------------------------------- the main bracket
local org = newBot("ORG")
local watch = newBot("WATCH")
local p1 = newBot("PONE", "CHARIZARD")
local p2 = newBot("PTWO", "BLASTOISE")
local p3 = newBot("PTHREE", "VENUSAUR")
local players = { p1, p2, p3 }
if not connectAll(bots) then
stopServer()
return finish()
end
check(org.C.you() ~= nil, "the real relay welcomes every v2 client")
local made = org.C.createTournament({ profile = PROFILE, rule = RULE,
playing = false, shotClock = 3,
maxSpectators = 8 })
waitFor(function() return made.done end, 600, "tour_create to answer")
check(made.id ~= nil,
"the real relay creates a tournament: " .. tostring(made.error))
if not made.id then
stopServer()
return finish()
end
local tourId = made.id
local created = org.C.tournament()
check(created and created.stage == "registering" and
created.creator == org.C.you().id and #created.players == 0 and
#created.spectators == 1,
"a playing:false creator is a spectator holding creator powers")
check(created and created.shotClock == 3 and created.rule and
created.rule.partySize == 1,
"tour_state carries the shot clock and the rule")
for _, bot in ipairs(players) do
bot.join = bot.C.joinTournament({ tour = tourId, as = "player",
party = bot.party, partyDigest = "d" .. bot.name })
end
watch.join = watch.C.joinTournament({ tour = tourId, as = "spectator" })
waitFor(function()
local t = org.C.tournament()
return t and #t.players == 3 and #t.spectators == 2
end, 900, "three players and two spectators to register")
local roster = org.C.tournament()
check(roster and #roster.players == 3,
"three players register with their teams up front")
check(roster and #roster.spectators == 2,
"the creator and the outside spectator both watch")
for _, bot in ipairs(players) do
check(bot.join.error == nil,
bot.name .. " joined: " .. tostring(bot.join.error))
end
local short = newBot("SHORT", "PIKACHU")
connectAll({ short })
local twoMons = copy(short.party)
twoMons[2] = copy(short.party[1])
local refused = short.C.joinTournament({ tour = tourId, as = "player",
party = twoMons, partyDigest = "dx" })
waitFor(function() return refused.done end, 600, "the bad party to be refused")
check(refused.reason == "party_ineligible",
"a party that breaks the rule is refused: " .. tostring(refused.reason))
short.C.disconnect()
org.C.startTournament()
waitFor(function()
local t = org.C.tournament()
return t and t.stage == "running" and #t.bracket >= 1
end, 900, "the tournament to start")
local running = org.C.tournament()
check(running and running.stage == "running",
"the creator starts the tournament")
check(running and running.bracket[1] and #running.bracket[1].matches == 2,
"three players make a two-slot first round: " ..
tostring(running and running.bracket[1] and #running.bracket[1].matches))
waitFor(function()
for _, bot in ipairs(players) do
if bot.ev.tour_bye then return true end
end
return false
end, 900, "a walkover player to be told tour_bye")
local byeBots = 0
for _, bot in ipairs(players) do
if bot.ev.tour_bye then byeBots = byeBots + 1 end
end
check(byeBots == 1, "exactly one player gets tour_bye: " .. byeBots)
local played = {}
local function liveSeats()
local seated = {}
for _, bot in ipairs(players) do
local tm = bot.ev.tour_match
local start = bot.ev.match_start
if tm and start and start.match == tm.match and not played[tm.match] then
seated[#seated + 1] = bot
end
end
return seated
end
local function playLiveMatch(label)
waitFor(function() return #liveSeats() == 2 end, 1800,
"two players to be seated in " .. label)
local seats = liveSeats()
if #seats ~= 2 then return nil end
local a, b = seats[1], seats[2]
local tourA, tourB = a.ev.tour_match, b.ev.tour_match
local startA, startB = a.ev.match_start, b.ev.match_start
local token = tourA.match
played[token] = true
check(tourA.match == tourB.match,
label .. ": both players share the bracket token (" .. token .. ")")
check(tourA.room == tourB.room and startA.room == tourA.room,
label .. ": match_start is bound to the child room from tour_match")
check(startA.match == token,
label .. ": the child room reuses the bracket token as its match")
check(startA.seed ~= nil and startA.seed == startB.seed,
label .. ": both seats share one seed")
check(startA.theirParty ~= nil and startB.theirParty ~= nil,
label .. ": each seat is handed the other party without room_ready")
local watchers = {}
for _, bot in ipairs(bots) do
if bot ~= a and bot ~= b and bot.C.state() == "online" then
watchers[#watchers + 1] = bot
end
end
waitFor(function()
for _, bot in ipairs(watchers) do
local spec, start = bot.ev.tour_spectate, bot.ev.match_start
if not (spec and spec.match == token and start and start.match == token) then
return false
end
end
return true
end, 900, label .. ": every other member to be told to spectate")
for _, bot in ipairs(watchers) do
local spec, start = bot.ev.tour_spectate, bot.ev.match_start
check(spec ~= nil and spec.match == token and start ~= nil and
start.match == token and start.role == "spectator",
label .. ": " .. bot.name .. " watches through match_start_spectate")
check(start ~= nil and start.room == tourA.room,
label .. ": " .. bot.name .. " is put in the child room")
bot.sides = {}
end
local host, hostStart = a, startA
local guest, guestStart = b, startB
if startA.role ~= "host" then
host, hostStart, guest, guestStart = b, startB, a, startA
end
check(hostStart.role == "host" and guestStart.role == "guest",
label .. ": the relay seats one host and one guest")
local gameH, gameG = headless(host), headless(guest)
local battleH = LinkBattle.newHost(gameH, host.C.roomSession(), {
myParty = copy(host.party), theirParty = copy(hostStart.theirParty),
theirName = hostStart.peerName, seed = hostStart.seed,
ruleset = hostStart.ruleset, keepNetOpen = true,
})
local battleG = LinkBattle.newGuest(gameG, guest.C.roomSession(), {
myParty = copy(guest.party), theirParty = copy(guestStart.theirParty),
theirName = guestStart.peerName, seed = guestStart.seed,
ruleset = guestStart.ruleset, keepNetOpen = true,
})
check(battleH ~= nil and battleG ~= nil,
label .. ": LinkBattle builds over the tournament child room")
if not (battleH and battleG) then return nil end
local resH, resG
battleH.onFinish = function(r) resH = r end
battleG.onFinish = function(r) resG = r end
gameH.stack:push(battleH)
gameG.stack:push(battleG)
for frame = 1, 24000 do
if resH and resG then break end
if frame % 600 == 0 then U.log(label, "battle frame", frame) end
Input.pressed = { a = true }
gameH.stack:update(1 / 60)
gameG.stack:update(1 / 60)
Input.pressed = {}
for _, bot in ipairs(watchers) do
local session = bot.C.roomSession()
if session then
for _, msg in ipairs(session:poll()) do
if msg.type == "spectate" and msg.side then
bot.sides[msg.side] = (bot.sides[msg.side] or 0) + 1
end
end
end
end
pump(1)
end
check(resH ~= nil and resG ~= nil,
label .. ": the battle finishes over the relay (" ..
tostring(resH) .. " / " .. tostring(resG) .. ")")
check(battleH.player.mon.hp == battleG.enemy.mon.hp and
battleH.enemy.mon.hp == battleG.player.mon.hp,
label .. ": both simulations agree on HP across the relay")
for _, bot in ipairs(watchers) do
check((bot.sides.host or 0) > 0 and (bot.sides.guest or 0) > 0,
label .. ": " .. bot.name .. " saw both sides' room_msg tagged (" ..
tostring(bot.sides.host) .. "/" .. tostring(bot.sides.guest) .. ")")
end
host.C.report(resH)
guest.C.report(resG)
local function ended(bot)
return bot.ev.match_end and bot.ev.match_end.match == token
and bot.ev.match_end or nil
end
waitFor(function() return ended(host) and ended(guest) end, 1200,
label .. ": room_result on both seats")
local endHost, endGuest = ended(host), ended(guest)
check(endHost ~= nil,
label .. ": room_result names the bracket token and reaches both seats")
local winnerId = nil
if endHost then
winnerId = endHost.winnerId
check(endHost.how == "agreed",
label .. ": the relay agreed the two reports (" ..
tostring(endHost.how) .. ")")
check(endGuest ~= nil and endGuest.winnerId == winnerId,
label .. ": both seats are told the same winner")
end
waitFor(function()
local t = org.C.tournament()
if not t then return false end
for _, round in ipairs(t.bracket) do
for _, m in ipairs(round.matches) do
if m.match == token and m.state == "done" then return true end
end
end
return false
end, 900, label .. ": the bracket to record the result")
local recorded = nil
local t = org.C.tournament()
for _, round in ipairs(t and t.bracket or {}) do
for _, m in ipairs(round.matches) do
if m.match == token then recorded = m end
end
end
check(recorded ~= nil and recorded.state == "done" and
recorded.winner == winnerId and recorded.how == "agreed",
label .. ": the bracket records the winner and how")
local loserId = nil
if winnerId then
loserId = winnerId == host.C.you().id and guest.C.you().id
or host.C.you().id
end
local eliminated = false
for _, entry in ipairs(t and t.players or {}) do
if entry.id == loserId and entry.eliminated then eliminated = true end
end
check(eliminated, label .. ": the loser is marked eliminated in tour_state")
return token
end
local firstToken = playLiveMatch("round 1")
if not firstToken then
stopServer()
return finish()
end
local secondToken = playLiveMatch("round 2")
check(secondToken ~= nil and secondToken ~= firstToken,
"the bracket advances into a second match: " .. tostring(secondToken))
waitFor(function() return org.ev.tour_over ~= nil end, 1200,
"tour_over after the final")
local over = org.ev.tour_over
check(over ~= nil, "a three-player bracket ends in tour_over")
if over then
check(over.tour == tourId, "tour_over names the tournament")
check(org.champFromState ~= nil and org.champFromState == over.championId,
"tour_state.champion is the id tour_over calls championId (" ..
tostring(org.champFromState) .. ")")
local championName = nil
for _, entry in ipairs(org.C.tournament().players) do
if entry.id == over.championId then championName = entry.name end
end
check(championName ~= nil and championName == over.champion,
"tour_over.champion is that id's name (" .. tostring(over.champion) ..
")")
check(watch.ev.tour_over ~= nil and
watch.ev.tour_over.championId == over.championId,
"the outside spectator is told the same champion")
check(org.C.tournament().stage == "finished",
"the model stays finished until we leave")
end
for _, bot in ipairs(bots) do
if bot.C.state() == "online" then bot.C.leaveTournament() end
end
pump(10)
for _, bot in ipairs(bots) do bot.C.disconnect() end
pump(5)
bots = {}
-- --------------------------------------------------- resume mid-battle
local rOrg = newBot("RORG")
local rA = newBot("RONE", "CHARIZARD")
local rB = newBot("RTWO", "BLASTOISE")
if not connectAll(bots) then
stopServer()
return finish()
end
local rMade = rOrg.C.createTournament({ profile = PROFILE, rule = RULE,
playing = false, shotClock = 9,
maxSpectators = 4 })
waitFor(function() return rMade.done end, 600, "the resume tournament")
rA.C.joinTournament({ tour = rMade.id, as = "player", party = rA.party,
partyDigest = "dra" })
rB.C.joinTournament({ tour = rMade.id, as = "player", party = rB.party,
partyDigest = "drb" })
waitFor(function()
local t = rOrg.C.tournament()
return t and #t.players == 2
end, 900, "both resume-test players to register")
rOrg.C.startTournament()
waitFor(function() return rA.ev.match_start and rB.ev.match_start end, 1200,
"the resume-test match to start")
check(rA.ev.match_start ~= nil and rB.ev.match_start ~= nil,
"resume: the bracket seats both players")
if not (rA.ev.match_start and rB.ev.match_start) then
stopServer()
return finish()
end
local rHost = rA.ev.match_start.role == "host" and rA or rB
local rGuest = rHost == rA and rB or rA
local sessH, sessG = rHost.C.roomSession(), rGuest.C.roomSession()
local function instrument(bot, session)
bot.sent, bot.recv = 0, 0
local rawSend, rawPoll = session.send, session.poll
session.send = function(self, msg)
bot.sent = bot.sent + 1
return rawSend(self, msg)
end
session.poll = function(self)
local out = rawPoll(self)
bot.recv = bot.recv + #out
return out
end
end
instrument(rHost, sessH)
instrument(rGuest, sessG)
local rGameH, rGameG = headless(rHost), headless(rGuest)
local rBattleH = LinkBattle.newHost(rGameH, sessH, {
myParty = copy(rHost.party), theirParty = copy(rHost.ev.match_start.theirParty),
theirName = rHost.ev.match_start.peerName, seed = rHost.ev.match_start.seed,
ruleset = rHost.ev.match_start.ruleset, keepNetOpen = true,
})
local rBattleG = LinkBattle.newGuest(rGameG, sessG, {
myParty = copy(rGuest.party), theirParty = copy(rGuest.ev.match_start.theirParty),
theirName = rGuest.ev.match_start.peerName, seed = rGuest.ev.match_start.seed,
ruleset = rGuest.ev.match_start.ruleset, keepNetOpen = true,
})
local rResH, rResG
rBattleH.onFinish = function(r) rResH = r end
rBattleG.onFinish = function(r) rResG = r end
rGameH.stack:push(rBattleH)
rGameG.stack:push(rBattleG)
local sessionBefore = rGuest.C.sessionId()
local killedAt, reconnected = nil, false
for frame = 1, 24000 do
if rResH and rResG then break end
if frame == 240 and not killedAt then
killedAt = frame
pcall(function() rGuest.net.tcpSocket:close() end)
end
if killedAt and not reconnected and rGuest.C.state() == "online"
and frame > killedAt + 30 then
reconnected = true
end
Input.pressed = { a = true }
rGameH.stack:update(1 / 60)
rGameG.stack:update(1 / 60)
Input.pressed = {}
pump(1)
end
check(killedAt ~= nil, "resume: the guest's socket was killed mid-battle")
check(reconnected, "resume: the guest reconnected on its own backoff")
check(rGuest.C.sessionId() == sessionBefore,
"resume: the guest walked back into the same relay session")
check(rResH ~= nil and rResG ~= nil,
"resume: the battle still finishes (" .. tostring(rResH) .. " / " ..
tostring(rResG) .. ")")
check(rBattleH.player.mon.hp == rBattleG.enemy.mon.hp and
rBattleH.enemy.mon.hp == rBattleG.player.mon.hp,
"resume: both simulations agree on HP, so the replay caused no desync")
for _ = 1, 30 do
if sessH then sessH:poll() end
if sessG then sessG:poll() end
pump(1)
end
check(rHost.recv == rGuest.sent,
("resume: the host got each guest message once (%d sent, %d delivered)")
:format(rGuest.sent, rHost.recv))
check(rGuest.recv == rHost.sent,
("resume: room_replay redelivered nothing twice (%d sent, %d delivered)")
:format(rHost.sent, rGuest.recv))
U.log("resume: duplicate room messages dropped by the client:",
rGuest.C.duplicates())
rHost.C.report(rResH)
rGuest.C.report(rResG)
waitFor(function() return rOrg.ev.tour_over ~= nil end, 1200,
"resume: tour_over")
check(rOrg.ev.tour_over ~= nil,
"resume: the tournament finishes after the resumed match")
for _, bot in ipairs(bots) do bot.C.disconnect() end
pump(5)
bots = {}
-- ------------------------------------------------------------- forfeit
local fOrg = newBot("FORG")
local fA = newBot("FONE", "CHARIZARD")
local fB = newBot("FTWO", "BLASTOISE")
if not connectAll(bots) then
stopServer()
return finish()
end
local fMade = fOrg.C.createTournament({ profile = PROFILE, rule = RULE,
playing = false, shotClock = 9,
maxSpectators = 4 })
waitFor(function() return fMade.done end, 600, "the forfeit tournament")
fA.C.joinTournament({ tour = fMade.id, as = "player", party = fA.party,
partyDigest = "dfa" })
fB.C.joinTournament({ tour = fMade.id, as = "player", party = fB.party,
partyDigest = "dfb" })
waitFor(function()
local t = fOrg.C.tournament()
return t and #t.players == 2
end, 900, "both forfeit-test players to register")
fOrg.C.startTournament()
waitFor(function() return fA.ev.match_start and fB.ev.match_start end, 1200,
"the forfeit-test match to start")
fA.C.report("error")
waitFor(function() return fA.ev.match_end and fB.ev.match_end end, 1200,
"room_result after the forfeit")
check(fA.ev.match_end ~= nil and fA.ev.match_end.how == "forfeit",
"report(\"error\") forfeits the match: " ..
tostring(fA.ev.match_end and fA.ev.match_end.how))
check(fA.ev.match_end and fA.ev.match_end.winnerId == fB.C.you().id,
"the other seat wins the forfeited match")
waitFor(function() return fOrg.ev.tour_over ~= nil end, 1200,
"tour_over after the forfeit")
check(fOrg.ev.tour_over ~= nil and
fOrg.ev.tour_over.championId == fB.C.you().id,
"the forfeit hands the bracket to the other player")
for _, bot in ipairs(bots) do bot.C.disconnect() end
pump(5)
bots = {}
-- ----------------------------------------------------------- shot clock
local sOrg = newBot("SORG")
local sA = newBot("SONE", "CHARIZARD")
local sB = newBot("STWO", "BLASTOISE")
if not connectAll(bots) then
stopServer()
return finish()
end
local sMade = sOrg.C.createTournament({ profile = PROFILE, rule = RULE,
playing = false, shotClock = 3,
maxSpectators = 4 })
waitFor(function() return sMade.done end, 600, "the shot-clock tournament")
sA.C.joinTournament({ tour = sMade.id, as = "player", party = sA.party,
partyDigest = "dsa" })
sB.C.joinTournament({ tour = sMade.id, as = "player", party = sB.party,
partyDigest = "dsb" })
waitFor(function()
local t = sOrg.C.tournament()
return t and #t.players == 2
end, 900, "both shot-clock players to register")
sOrg.C.startTournament()
waitFor(function() return sA.ev.match_start and sB.ev.match_start end, 1200,
"the shot-clock match to start")
local sHost = sA.ev.match_start.role == "host" and sA or sB
local sStalled = sHost == sA and sB or sA
sHost.C.roomSession():send({ type = "action", kind = "move", slot = 1 })
waitFor(function() return sHost.ev.match_end ~= nil end,
math.floor(SHOT_MS / 1000 * 60) + 900,
"the shot clock to fire on the stalled player")
check(sHost.ev.match_end ~= nil and sHost.ev.match_end.how == "timeout",
"an unanswered turn forfeits on the shot clock: " ..
tostring(sHost.ev.match_end and sHost.ev.match_end.how))
check(sHost.ev.match_end and sHost.ev.match_end.winnerId == sHost.C.you().id,
"the player who acted wins the timed-out match")
check(sStalled.ev.match_end ~= nil and
sStalled.ev.match_end.winnerId == sHost.C.you().id,
"the stalled player is told the same result")
for _, bot in ipairs(bots) do bot.C.disconnect() end
pump(5)
bots = {}
-- ----------------------------------------------------------- kick
local kOrg = newBot("KORG")
local kA = newBot("KONE", "CHARIZARD")
if not connectAll(bots) then
stopServer()
return finish()
end
local kMade = kOrg.C.createTournament({ profile = PROFILE, rule = RULE,
playing = false, shotClock = 6,
maxSpectators = 4 })
waitFor(function() return kMade.done end, 600, "the kick tournament")
kA.C.joinTournament({ tour = kMade.id, as = "player", party = kA.party,
partyDigest = "dka" })
waitFor(function()
local t = kOrg.C.tournament()
return t and #t.players == 1
end, 900, "the kick-test player to register")
kOrg.C.kickFromTournament(kA.C.you().id)
waitFor(function() return kA.ev.error ~= nil end, 900,
"tour_closed on the kicked player")
check(kA.ev.error ~= nil and kA.ev.error.scope == "tournament" and
kA.ev.error.reason == "kicked",
"a kicked player is told tour_closed: " ..
tostring(kA.ev.error and kA.ev.error.reason))
check(kA.C.tournament() == nil,
"the kicked player's tournament model is cleared")
waitFor(function()
local t = kOrg.C.tournament()
return t and #t.players == 0
end, 900, "the kicked player to leave tour_state")
check(kOrg.C.tournament() and #kOrg.C.tournament().players == 0,
"the kicked player is out of tour_state")
kOrg.C.closeTournament()
waitFor(function() return kOrg.ev.error ~= nil end, 900,
"tour_closed on the creator")
check(kOrg.ev.error ~= nil and kOrg.ev.error.reason == "closed",
"the creator can close the tournament: " ..
tostring(kOrg.ev.error and kOrg.ev.error.reason))
for _, bot in ipairs(bots) do bot.C.disconnect() end
pump(5)
stopServer()
return finish()
end