Pastebiniä käytetään pidempien tekstien säilömiseen jotka pitää laittaa talteen tai joita esim. ei voi sanoa irkissä tms kätevästi ilman hirveää floodimista. Lyhykäisiä tunnisteita saa arvaamalla satunnaisesti selville, joten ei kannata pasteta mitään erityisen yksityistä.
Tekstiä mahtuu maksimissaan 64 kilotavua per paste eli älä ihmettele jos tosi pitkät pastet katkeaa, jos taas TOSI isoja pasteja tunkkaa tänne niin tulee jopa virhe eikä paste mene edes läpi. Myäskään viagra- tai cialis-sanoja sisältäviä pasteja ei hyväksytä, sillä erinäiset spämmibotit puskevat niitä vähän väliä.
Pasten nimi (vapaaehtoinen):
Värjäys: ABAPActionScriptActionScript 3AdaANTLRANTLR With ActionScript TargetANTLR With C# TargetANTLR With CPP TargetANTLR With Java TargetANTLR With ObjectiveC TargetANTLR With Perl TargetANTLR With Python TargetANTLR With Ruby TargetApacheConfAppleScriptaspx-csaspx-vbAsymptoteautohotkeyAwkBase MakefileBashBash SessionBatchfileBBCodeBefungeBlitzMaxBooBrainfuckBroCC#C++c-objdumpCFEngine3cfstatementCheetahClojureCMakeCoffeeScriptColdfusion HTMLCommon LispCoqcpp-objdumpCSSCSS+Django/JinjaCSS+Genshi TextCSS+MakoCSS+MyghtyCSS+PHPCSS+RubyCSS+SmartyCythonDd-objdumpDarcs PatchDartDebian Control fileDebian SourcelistDelphiDiffDjango/JinjaDTDDuelDylaneCECLElixirElixir iex sessionEmbedded RagelERBErlangErlang erl sessionEvoqueFactorFancyFantomFelixFortranFSharpGASGenshiGenshi TextGettext CatalogGherkinGLSLGnuplotGoGoodData-CLGosuGosu TemplateGroffGroovyHamlHaskellhaXeHTMLHTML+CheetahHTML+Django/JinjaHTML+EvoqueHTML+GenshiHTML+MakoHTML+MyghtyHTML+PHPHTML+SmartyHTML+VelocityHTTPHybrisINIIoIokeIRC logsJadeJavaJava Server PageJavaScriptJavaScript+CheetahJavaScript+Django/JinjaJavaScript+Genshi TextJavaScript+MakoJavaScript+MyghtyJavaScript+PHPJavaScript+RubyJavaScript+SmartyJSONKotlinLighttpd configuration fileLiterate HaskellLLVMLogtalkLuaMakefileMakoMAQLMasonMatlabMatlab sessionMiniDModelicaModula-2MoinMoin/Trac Wiki markupMOOCodeMoonScriptMuPADMXMLMyghtyMySQLNASMNemerleNewLispNewspeakNginx configuration fileNimrodNumPyobjdumpObjective-CObjective-JOCamlOctaveOocOpaOpenEdge ABLPerlPHPPL/pgSQLPostgreSQL console (psql)PostgreSQL SQL dialectPostScriptPOVRayPowerShellPrologPropertiesProtocol BufferPyPy LogPythonPython 3Python 3.0 TracebackPython console sessionPython TracebackRagelRagel in C HostRagel in CPP HostRagel in D HostRagel in Java HostRagel in Objective C HostRagel in Ruby HostRaw token dataRConsoleREBOLRedcodereStructuredTextRHTMLRubyRuby irb sessionSSassScalaScalate Server PageScamlSchemeScilabSCSSSmalltalkSmartySnobolSQLsqlite3conSquidConfStandard MLsystemverilogTclTcshTeaTeXText onlyUrbiScriptValaVB.netVelocityverilogvhdlVimLXMLXML+CheetahXML+Django/JinjaXML+EvoqueXML+MakoXML+MyghtyXML+PHPXML+RubyXML+SmartyXML+VelocityXQueryXSLTYAML
<!DOCTYPE html> <meta charset="UTF-8"> <title>Kuurupiilo</title> <script> (function() { function error() { alert("Selaimestasi puuttuu tarvittavia JavaScript-ominaisuuksia. Hanki nykyaikainen selain."); } var timeout = setTimeout(error, 0); eval("{ class a { async b(x = {}) {} static get c() { const a = b => `c` } } }"); performance.now(); if ((Function.prototype)() !== undefined) throw null; clearTimeout(timeout); }()); </script> <script> "use strict"; function debugErr() { console.error(Array.from(arguments)); } function debug() { console.log(Array.from(arguments)); } function javaStringHash(s) { let hash = 0; for (let i = 0; i < s.length; ++i) { const c = s.charCodeAt(i); hash = (((hash << 5) - hash) + c) | 0; } return hash; } function* xorshift32(x) { x = x | 0; while (true) { x ^= x << 13; x ^= x >>> 17; x ^= x << 5; x = x >>> 0; // uint yield x; } } function pow2(x) { return x * x; } function hypot2(x, y) { return x * x + y * y; } class Clock { static seconds() { return performance.now() / 1000; } static ms() { return performance.now(); } static promiseTimeout(ms) { return ms > 0 && new Promise(done => setTimeout(done, ms)); } static promiseTimeMs(ms) { return Clock.promiseTimeout(ms - Clock.ms()); } } class Timer { constructor() { this.seconds = 0; this.started = 0; } start() { if (this.started) return; this.started = performance.now(); } stop() { if (!this.started) return; this.seconds += (performance.now() - this.started) / 1000; this.started = 0; } } function $(el, s) { if (s == null) { s = el; el = document; } return el.querySelector(s); } function $$(el, s) { if (s == null) { s = el; el = document; } return el.querySelectorAll(s); } const dom_helpers_ = {}; function $$$(s) { const m = s.match(/^<([a-zA-Z0-9:]+)/); const tag = m && m[1] || ""; const parents = { "td": "tr", "th": "tr", "tr": "tbody", "tbody": "table", "thead": "table", "caption": "table", "option": "select", "head": "html", "body": "html" }; const parent = parents[tag.toLowerCase()] || "div"; const helpers = dom_helpers_; const helper = helpers[parent] || (helpers[parent] = document.createElement(parent)); helper.innerHTML = s; return helper.removeChild(helper.firstChild); } class Queue { constructor() { this.queue = []; this.waiting = []; this.closed = false; } pop() { if (this.queue.length) { return this.queue.shift(); } } async popAsync() { if (this.queue.length) { return this.queue.shift(); } if (this.closed) { return; } return new Promise(resolve => this.waiting.push(resolve)); } push(value) { if (this.waiting.length) { this.waiting.shift()(value); return true; } if (!this.closed) { this.queue.push(value); return true; } return false; } pushThrow(value) { if (!this.push(value)) { throw new Error("Queue is closed!"); } } close() { this.closed = true; while (this.waiting.length) { this.waiting.shift()(undefined); } } } class NullSinkQueue { push() {} pop() {} popAsync() {} pushThrow() {} close() {} } function new_ManualPromise() { let x, p = new Promise((resolve, reject) => {x = {resolve, reject}}); p.resolve = x.resolve; p.reject = x.reject; return p; } class Client { constructor(options) { this.recvQueue = new Queue(); this.sendQueue = new Queue(); this.name = options.name || "*"; this.nameShown = options.nameShown || this.name; this.version = options.version || null; this.closed = false; this.closedPromise = new_ManualPromise(); } handleInput(data) { if (data.startsWith('{"summary":')) { if (!this.summary) { this.summary = JSON.parse(data).summary; this.fixSummary(); } return false; } return this.recvQueue.push(data); } fixSummary() { const summary = this.summary; if (!summary) { return; } if (!summary.time) { if ("timeUser" in summary || "timeSystem" in summary) { summary.time = (summary.timeUser || 0) + (summary.timeSystem || 0); } } if (summary.time) { summary.time = Math.round(summary.time * 1000) / 1000; } } async recvAsync() { return this.recvQueue.popAsync(); } recv() { return this.recvQueue.pop(); } send(data) { return this.sendQueue.push(data); } close() { this.recvQueue.close(); this.sendQueue.close(); this.closed = true; this.closedPromise.resolve(); } } function createWebSocket(address, handleError) { try { let received = false, ws = new WebSocket(address); ws.addEventListener("message", ev => { received = true; }); ws.addEventListener("close", ev => { if (ev.code != 1000) { handleError("Virhe yhteydessä. (WebSocket-virhekoodi: " + ev.code + "). Lisätietoja voi näkyä selaimen konsolissa (F12 / Ctrl+Shift+J)."); } else if (!received) { handleError("Yhteys katkesi ilman yhtään viestiä. Tarkasta asetukset."); } }); return ws; } catch (ex) { console.log(ex); let msg = "Tarkasta osoite."; if (location && location.href && location.href.substr(0, 4) == "http") { msg = "Yhteys kotikoneelle ei ehkä toimi oikein, kun sivu on ladattu netistä."; msg += " Tallenna tämä sivu omalle koneellesi ja kokeile sieltä uudestaan."; msg += " Tarkasta myös osoite."; } handleError("WebSocket-yhteys osoitteeseen " + address + " ei onnistu. " + msg + " Lisätietoja virheestä: ”" + ex.message + "”"); } } class WebSocketClient extends Client { constructor(options) { super(options); const ws = createWebSocket(options.address, s => options.handleError(this.nameShown + ": " + s)); if (ws) { ws.addEventListener("message", ev => { if (!this.handleInput(ev.data)) { setTimeout(() => ws.close(), 1000); } }); ws.addEventListener("close", ev => { this.close(); }); ws.addEventListener("open", async ev => { for (let s; (s = await this.sendQueue.popAsync()) != null;) { ws.send(s); } setTimeout(() => ws.close(), 1000); }); } else { this.close(); } } static getGreeting(address) { const promise = new_ManualPromise(); try { const ws = new WebSocket(address); ws.addEventListener("open", () => ws.send("ping")); ws.addEventListener("close", () => promise.reject("Ei vastaa!")); ws.addEventListener("message", e => { promise.resolve(e.data); ws.close(); }); setTimeout(() => ws.close(), 1000); } catch (ex) { promise.reject(ex); } return promise; } } class SandboxFrame { constructor(name, code) { this.clients = new Map(); this.name = name; this.handleError = s => console.log(name + ": " + s); if (!SandboxFrame.frames) { SandboxFrame.frames = new Map(); window.addEventListener("message", SandboxFrame.handleGlobalMessage); } if (SandboxFrame.frames.get(this.name)) { SandboxFrame.frames.get(this.name).close(); } SandboxFrame.frames.set(this.name, this); let html = `<!DOCTYPE html><meta charset="UTF-8"><script>${this.workerScript(code)}\n<\/script>`; let src = "data:text/html;base64," + btoa(unescape(encodeURIComponent(html))); this.frame = document.createElement("iframe"); this.frame.sandbox = "allow-scripts"; this.frame.className = "sandboxed-worker-frame"; this.frame.dataset.name = name; document.body.appendChild(this.frame); this.frame.src = src; } static get(name) { return SandboxFrame.frames && SandboxFrame.frames.get(name); } static create(name, code) { return new SandboxFrame(name, code); } static handleGlobalMessage(ev) { for (let item of SandboxFrame.frames) { if (ev.source == item[1].frame.contentWindow) { item[1].handleMessage(ev.data); return; } } } handleMessage(msg) { const arr = msg.split(":", 2); if (arr.length != 2) { this.handleError("invalid: " + msg); return; } const id = arr[0], cmd = arr[1], data = msg.substr(id.length + cmd.length + 2); const client = this.clients.get(id); if (!client) { return; } else if (cmd == "msg") { client.handleInput(data); } else if (cmd == "exit") { client.close(); if (data) { this.handleError("exit: " + data); } } else { client.close(); this.handleError("invalid: " + msg); } } close() { this.clients.forEach(c => c.close()); if (this.frame) { this.frame.remove(); } SandboxFrame.frames.delete(this.name); } post(msg) { this.frame.contentWindow.postMessage(msg, "*"); } postClient(client, s) { this.post(client.sandboxFrameClientId + ":msg:" + s); } closeClient(client) { if (this.clients.delete(client.sandboxFrameClientId)) { this.post(client.sandboxFrameClientId + ":exit:"); } } startClient(client) { this.closeClient(client); client.sandboxFrameClientId = Clock.ms() + "-" + Math.random(); this.clients.set(client.sandboxFrameClientId, client); this.post(client.sandboxFrameClientId + ":start:"); } workerScript(code) { return `((function sandboxedWorkerInit(Queue, Timer, ext_startWorker) { let workers = {}, mainWindow; function postMessage(data) { mainWindow.postMessage(data, "*"); } function startWorker(id) { const w = workers[id] = new Queue(); let timer = new Timer(); async function input() { timer.stop(); let s = await w.popAsync(); timer.start(); if (s == null) { throw new Error("End of input."); } return s; } function output(s) { postMessage(id + ":msg:" + s); } async function runWorker() { let exitMsg = ""; try { timer.start(); await ext_startWorker(input, output); } catch (ex) { exitMsg = ex; } timer.stop(); postMessage(id + ":msg:" + JSON.stringify({summary: {time: timer.seconds}})); postMessage(id + ":exit:" + exitMsg); delete workers[id]; } runWorker(); } function handleMessage(e) { mainWindow = e.source; let m = e.data.split(":", 3); let id = m[0], cmd = m[1], data = m[2]; if (cmd == "start" && !workers[id]) { startWorker(id); } else if (cmd == "exit" && workers[id]) { workers[id].close(); delete workers[id]; } else if (cmd == "msg" && workers[id]) { workers[id].push(data); } } window.addEventListener("message", handleMessage); })(${Queue}, ${Timer}, ${code}));`; } } class SandboxFrameClient extends Client { constructor(options) { super(options); this.sandboxFrame = options.sandboxFrame; this.sandboxFrame.startClient(this); this.sendLoop(); } async sendLoop() { for (let s; (s = await this.sendQueue.popAsync()) != null;) { this.sandboxFrame.postClient(this, s); } setTimeout(() => this.close(), 1000); } close() { super.close(); this.sandboxFrame.closeClient(this); } } class FunctionClient extends Client { constructor(options) { super(options); const timer = new Timer(); (async () => { timer.start(); await options.function( async () => { timer.stop(); const s = await this.sendQueue.popAsync(); timer.start(); return s; }, s => this.recvQueue.pushThrow(s) ); timer.stop(); this.summary = {time: timer.seconds}; this.fixSummary(); this.close(); })(); } } class ReplayClient extends Client { constructor(options) { super(options); this.sendQueue = new NullSinkQueue(); this.handleInput("" + this.name); // greeting this.replayCounter = 0; this.replayData = options.replayData; } } class InteractiveClient extends Client { constructor(options) { super(options); this.sendQueue = new NullSinkQueue(); this.handleInput("" + this.name); // greeting } } class Game { constructor(options) { this.ui = options.ui; this.started = false; this.finished = false; this.exiting = false; this.exited = false; this.exitPromise = new_ManualPromise(); this.exitPromise.then(options.onExit); this.timeUpdated = 0; this.timeDeltaMin = 0; this.init(options); this.ui && this.ui.init(this); } pause(value) { if (value === false) { this.unpause(); } else if (!this.paused) { this.paused = true; this.pausePromise = new_ManualPromise(); } } unpause() { if (this.paused) { this.paused = false; this.pausePromise.resolve(); } } sendAll(data) { this.sendAllIn(this.players, data); } sendAllIn(list, data) { list.forEach(player => player.client.send(data)); } sendAllBut(list, data) { this.players.forEach(player => list.includes(player) || player.client.send(data)); } async closeAllClients() { const closed = Promise.all(this.players.map(p => p.client.closedPromise)); this.sendAll(this.exitMessage); await Promise.race([closed, Clock.promiseTimeout(500)]); this.players.forEach(p => p.client && p.client.close()); await closed; } async run(clients) { if (this.started || this.exiting) return; this.started = true; try { this.sendAll(Game.GREETING); for (let player of this.players) { player.greeting = await player.client.recvAsync(); } this.start(); this.ui && this.ui.start(); while (!this.exiting && !this.finished) { await this.update(); this.ui && this.ui.update(); } this.exiting = true; await this.closeAllClients(); } finally { this.exited = true; this.ui && this.ui.exit(); this.exitPromise.resolve(); } } async exit() { if (this.exiting) return; this.exiting = true; await this.closeAllClients(); if (!this.started) { this.exited = true; this.exitPromise.resolve(); } await this.exitPromise; } // NOTICE: Start editing here! static get GREETING() { return "2019-kuurupiilo"; } static fillOptions(options) { options.seekerCount = options.seekerCount || 3; options.boardSeed = options.boardSeed || (Math.random() * 0x80000000 | 1); } static makeIdentifier(options) { Game.fillOptions(options); const names = options.players.map(p => typeof p == "string" ? p : p.name); const seekers = names.slice(0, options.seekerCount); const others = names.slice(options.seekerCount); const str = seekers.join("-") + "," + others.join("-") + "," + options.boardSeed; return str.replace(/[^-a-zA-Z0-9.,]/g, ''); } init(options) { this.identifier = Game.makeIdentifier(options); this.seekerCount = options.seekerCount; this.boardSeed = options.boardSeed; this.players = options.players; this.maxSeen = 50; this.seekerDelay = 200; this.playerMaxMove = 8; this.playerRadius = 32; this.coordMax = 1100; this.roundsBetweenShrink = 4800; this.obstacleCountMax = 40; this.obstacleMinRadius = 60; this.obstacleMaxRadius = 180; this.boardCenterRadius = this.playerRadius * 2 + this.playerMaxMove * 2; this.boardRadius = this.coordMax - this.boardCenterRadius; this.obstacles = []; const rng = xorshift32(this.boardSeed); const random = (min, max) => rng.next().value % (max - min + 1) + min; for (let fail = 0; fail < 500 && this.obstacles.length < this.obstacleCountMax; ++fail) { // Create obstacle at board edge, move closer to center. const br = this.boardRadius; const r = random(this.obstacleMinRadius, this.obstacleMaxRadius); const xx = random(-br, br); let o = [{x: xx, y: br}, {x: xx, y: -br}, {x: br, y: xx}, {x: -br, y: xx}][random(0, 3)]; const fits = o => { if (hypot2(o.x, o.y) < pow2(r + this.boardCenterRadius)) { return false; } return !this.obstacles.some(p => hypot2(p.x - o.x, p.y - o.y) <= pow2(p.r + r + this.boardCenterRadius)); } o = this.binarySearchCoord(o, {x: 0, y: 0}, fits); if (hypot2(o.x, o.y) <= pow2(this.boardRadius - r)) { o.r = r; this.obstacles.push(o); } } this.players = options.players.map((client, i) => ({ client: client, number: i + 1, x: 0, y: 0, target: {x: 0, y: 0}, r: this.playerRadius, speed: this.playerMaxMove, moving: false, seeker: i < this.seekerCount, seenRounds: 0, aliveRounds: 0, points: 0, finished: false, inputEnd: false, input: [], replay: "" })); // Sanity check for radiusContainsLine. // 14-digit numbers fit in doubles without rounding, luckily... const MAX = ((2 * this.coordMax) ** 2 * 3) ** 2; if (MAX == MAX + 1) { console.log("NOTICE: Possible integer overflow!"); } } radiusContains(xo, yo, ro, x, y) { // NOTICE: Containment with <= radius. const dx = xo - x, dy = yo - y; return dx * dx + dy * dy <= ro * ro; } radiusContainsLine(xo, yo, ro, x0, y0, x1, y1) { // NOTICE: Containment with <= radius. const x0o = xo - x0, y0o = yo - y0; const x1o = xo - x1, y1o = yo - y1; const x01 = x1 - x0, y01 = y1 - y0; if (x0o * x01 + y0o * y01 >= x01 * x01 + y01 * y01) { return x1o * x1o + y1o * y1o <= ro * ro; } if (x1o * -x01 + y1o * -y01 >= x01 * x01 + y01 * y01) { return x0o * x0o + y0o * y0o <= ro * ro; } const a = y1 - y0, b = x0 - x1, c = x1 * y0 - y1 * x0; return pow2(a * xo + b * yo + c) <= pow2(ro) * hypot2(a, b); } canSee(p1, p2) { if (p1 == p2) { return true; } for (const o of this.obstacles) { if (this.radiusContainsLine(o.x, o.y, o.r, p1.x, p1.y, p2.x, p2.y)) { return false; } } return true; } updateState() { for (const p of this.players) { p.canSeeStraight = this.players.filter(o => this.canSee(p, o)); p.dataString = p.number + " " + p.x + " " + p.y + " " + p.target.x + " " + p.target.y + " " + (p.seeker ? -1 : p.seenRounds); } if (this.round >= this.seekerDelay) { this.seekerCanSee = this.players.filter(o => this.players.some(p => p.seeker && p.canSeeStraight.includes(o))); } else { this.seekerCanSee = this.players.filter(p => p.seeker); } let countHiders = 0; for (const p of this.players) { p.canSeeSomehow = p.seeker ? this.seekerCanSee : p.canSeeStraight; if (!p.seeker && !p.finished) { countHiders += 1; } if (!p.finished) { p.aliveRounds += 1; } } if (this.round >= this.seekerDelay) { for (const p of this.players) { if (p.finished) { continue; } else if (p.seeker) { p.points -= countHiders; } else { p.points += this.seekerCount; if (!this.seekerCanSee.includes(p)) { p.seenRounds = 0; } else { p.seenRounds += 1; if (p.seenRounds == this.maxSeen) { p.client.send("0"); p.finished = true; } } } } } this.finished |= this.players.every(p => p.seeker || p.finished); } movePlayers() { for (const p of this.players) { if (!p.seeker || this.round >= this.seekerDelay) { this.movePlayer(p); } } } getNearestObstacle(p, t) { let best, bestDist2; for (const o of this.obstacles) { if (this.radiusContainsLine(o.x, o.y, o.r + p.r, p.x, p.y, t.x, t.y)) { const dist2 = (o.x - p.x) * (t.x - p.x) + (o.y - p.y) * (t.y - p.y); if (!best || bestDist2 > dist2) { best = o; bestDist2 = dist2; } } } return best; } binarySearchCoord(pmin, pmax, test) { if (test(pmax)) { return pmax; } while (hypot2(pmin.x - pmax.x, pmin.y - pmax.y) > 2) { const p = {x: pmin.x + ((pmax.x - pmin.x) / 2 | 0), y: pmin.y + ((pmax.y - pmin.y) / 2 | 0)}; if (test(p)) { pmin = p; } else { pmax = p; } } return pmin; } binaryExtendCoord(p, d, test) { p = {x: p.x, y: p.y}; for (let i = 1; !test(p); i *= 2) { p.x += d.x * i; p.y += d.y * i; } return p; } movePlayer(p) { let t = p.target; if (!p.moving || p.x == t.x && p.y == t.y) { p.moving = false; return; } const o = this.getNearestObstacle(p, t); const testInObstacle = t => o && this.radiusContains(o.x, o.y, o.r + p.r, t.x, t.y); const notInObstacle = t => !testInObstacle(t); const canMove = (x, y) => !testInObstacle({x, y}); // If target is in obstacle, get a new target just outside it. if (o && t.x == o.x && t.y == o.y) { t = this.binarySearchCoord(p, t, notInObstacle); } if (testInObstacle(t)) { const surelyOut = this.binaryExtendCoord(t, {x: t.x - o.x, y: t.y - o.y}, notInObstacle); t = this.binarySearchCoord(surelyOut, t, notInObstacle); } // On same side of obstacle? if (o && (p.x - o.x) * (t.x - o.x) + (p.y - o.y) * (t.y - o.y) < 0) { // Rotate target 90 degrees around obstacle, see which way is closer. const t1max = this.binaryExtendCoord(o, {x: +(t.y - p.y), y: -(t.x - p.x)}, notInObstacle); const t2max = this.binaryExtendCoord(o, {x: -(t.y - p.y), y: +(t.x - p.x)}, notInObstacle); const t1 = this.binarySearchCoord(o, t1max, testInObstacle); const t2 = this.binarySearchCoord(o, t2max, testInObstacle); const choose1 = hypot2(p.x - t1.x, p.y - t1.y) < hypot2(p.x - t2.x, p.y - t2.y); t = this.binarySearchCoord(t, choose1 ? t1 : t2, t => this.getNearestObstacle(p, t) == o); } const targetDist2 = hypot2(p.x - t.x, p.y - t.y); const pointDist2 = Math.min(targetDist2, pow2(p.speed)); // Close enough to reach target? if (pointDist2 == targetDist2 && canMove(t.x, t.y)) { p.x = t.x; p.y = t.y; p.moving = false; return; } // Find the point which is closest to target and reachable. let best, bestDot = 0; function tryPoint(x, y) { const dot = x * (t.x - p.x) + y * (t.y - p.y); if (dot > bestDot && canMove(p.x + x, p.y + y)) { bestDot = dot; best = {x, y}; } } // Create points which are as far as possible. for (let x = 0; x*x < pointDist2; ++x) { const y = Math.sqrt(pointDist2 - x*x) | 0; tryPoint(+x, +y); tryPoint(+y, -x); tryPoint(-x, -y); tryPoint(-y, +x); } // Go to that point. if (best) { p.x += best.x; p.y += best.y; return; } p.target.x = p.x; p.target.y = p.y; p.moving = false; } async update() { // Send data for a new round. const playersNeedInput = []; for (const p of this.players) { // Seekers get data (and send input) only after seekerDelay. if (!p.seeker || this.round >= this.seekerDelay) { playersNeedInput.push(p); const arr = p.canSeeSomehow; p.client.send(arr.length + " " + arr.map(p => p.dataString).join(" ")); } } // Handle timing and pausing and waiting for input. await Promise.all(playersNeedInput.map(p => this.playerInputAsync(p))); await Clock.promiseTimeMs(this.timeUpdated + this.timeDeltaMin); await this.pausePromise; this.timeUpdated = Clock.ms() | 0; // Game logic: Move, update, (shrink). this.movePlayers(); this.updateState(); if (!this.finished) { this.round += 1; if (this.round % this.roundsBetweenShrink == 0) { this.obstacles.filter(o => (o.r = (o.r * 3 / 4) | 0) > this.playerMaxMove); } } } start() { this.round = 0; this.timeUpdated = Clock.ms() | 0; this.updateState(); const t = [ this.players.length, 0, // p.number this.obstacles.length, this.obstacles.map(o => o.x + " " + o.y + " " + o.r).join(" ") ]; for (const p of this.players) { t[1] = p.number; p.client.send(t.join(" ")); } } get exitMessage() { return "0"; } async playerInputAsync(player) { if (player.inputEnd || player.input.length) { return; } let s = player.client.recv(); if (s == null) { if (player.client instanceof ReplayClient) { this.playerReplayPoll(player); } else if (player.client instanceof InteractiveClient) { if (this.ui) { this.ui.playerInteractivePoll(player); } else { player.client.close(); } } s = await player.client.recvAsync(); } if (s == "=") { return; } const m = s && s.match(/^(-?[0-9]{1,4}) (-?[0-9]{1,4})$/); const target = m && {x: +m[1], y: +m[2]}; if (target && Math.abs(target.x) <= this.coordMax && Math.abs(target.y) <= this.coordMax) { if (player.target.x != target.x || player.target.y != target.y) { player.moving = true; player.target = target; player.replay += (player.replay ? " " : "") + this.round + " " + target.x + " " + target.y; } } else { player.inputEnd = true; player.client.send(this.exitMessage); player.client.close(); } } get summary() { if (!this.exited || !this.started) { return null; } if (this.summary_) { return JSON.parse(this.summary_); } let summary = { "greeting": Game.GREETING, "identifier": this.identifier, "boardSeed": this.boardSeed, "players": [], "replay": [] }; if (!this.finished) { summary.unfinished = true; } for (let i = 0; i < this.players.length; ++i) { let p = this.players[i]; let c = p.client; let s = { "name": p.client.name, "version": p.client.version, "greeting": p.greeting, "points": p.points }; if (!p.client.version) { delete s.version; } if (p.client instanceof InteractiveClient) { s.interactive = true; } if (c.summary) { s.summary = c.summary; s.summary.timeLimit = (5000 + p.aliveRounds * 2) / 1000; if (s.summary.time && s.summary.time > s.summary.timeLimit) { s.summary.timeLimitExceeded = true; } if (s.summary.memoryKB && s.summary.memoryKB > 128 * 1024) { s.summary.memoryLimitExceeded = true; } } summary.players.push(s); summary.replay.push(p.replay); } this.summary_ = JSON.stringify(summary); return summary; } playerReplayPoll(p) { const c = p.client; const m = c.replayData.match(/([0-9]+) (-?[0-9]+) (-?[0-9]+)/); if (!m) { c.close(); return; } const round = +m[1], x = +m[2], y = +m[3]; if (round < this.round) { c.close(); return; } if (round > this.round) { c.handleInput("="); return; } c.replayData = c.replayData.substr(m[0].length); c.handleInput(x + " " + y); } } class UIError extends Error { get name() { return "Virhe"; } } const GameUI = new class { firstInit() { this.output = $("#game-output"); this.board = $("#game-board"); this.canvas = $$$("<canvas></canvas>"); this.roundsPerSecond = $("#game-rounds-per-second"); this.roundsPerSecond.addEventListener("input", e => this.setRoundsPerSecond()); this.pauseButton = $("#game-pause"); this.endButton = $("#game-end"); this.storeButton = $("#game-store"); this.playerHidePartially = $("#game-player-hide-partially"); this.updateStatusButtons(); this.endButton.addEventListener("click", e => this.handleEnd()); this.pauseButton.addEventListener("click", e => this.handlePause()); this.board.tabIndex = 0; this.board.addEventListener("keydown", e => this.handleKeyPress(e)); this.board.addEventListener("click", e => this.handleClick(e)); this.playerHidePartially.addEventListener("change", e => this.update()); } setRoundsPerSecond(value) { if (value == null) { value = this.roundsPerSecond.value; } else { this.roundsPerSecond.value = value; } if (this.game) { this.game.timeDeltaMin = 1000 / (value || 1); } } updateStatusButtons() { const running = this.game && this.game.started && !this.game.exited; const paused = running && this.game.paused; this.endButton.disabled = !running; this.pauseButton.disabled = !running; this.pauseButton.textContent = this.pauseButton.dataset[paused ? "continue" : "pause"]; this.storeButton.disabled = !this.game || !this.game.finished; } init(game) { if (this.game && !this.game.exited) { throw new UIError("GameUI: Edellinen peli on kesken!"); } this.game = game; this.game.timeDeltaMin = 1000 / (parseInt(this.roundsPerSecond.value) || 1); this.scale = 0.25; this.offset = Math.ceil(this.game.coordMax * this.scale); this.width = this.offset * 2; this.height = this.offset * 2; this.canvas.width = this.width; this.canvas.height = this.height; this.board.style.width = this.width + "px"; this.board.style.height = this.height + "px"; this.playersPolling = new Set(); this.playerPolling = null; this.playerSelected = null; this.output.textContent = "Tuloste:\n\n"; this.output.classList.add("hidden"); this.board.textContent = ""; this.board.appendChild(this.canvas); const letters = "qwertadsfgzxcvbyuiophjklönm".split(""); const interactive = []; for (const p of this.game.players) { if (letters.length && p.client instanceof InteractiveClient) { p.domLetter = letters.shift(); interactive.push(p.domLetter); } else { p.domLetter = p.number; } p.domElement = $$$(`<span class='game-player'>${p.domLetter}</span>`); p.domElement.dataset.playerNumber = p.number; p.domElement.classList.add(p.seeker ? "game-player-seeker" : "game-player-other"); this.board.appendChild(p.domElement); this.updateElement(p); } if (!interactive.length) { $("#game-human-letters").textContent = "pelaajan kirjaimella"; } else if (interactive.length == 1) { $("#game-human-letters").textContent = "(kirjain " + interactive.join(", ") + ")"; } else { $("#game-human-letters").textContent = "(kirjaimet " + interactive.join(", ") + ")"; } const rng = xorshift32(this.game.boardSeed); for (const o of this.game.obstacles) { o.domElement = $$$(`<span class="game-obstacle"></span>`); o.domElement.style.backgroundColor = (0x1333 + (rng.next().value & 0xbbb)).toString(16).replace(/^./, "#"); this.board.appendChild(o.domElement); this.updateElement(o); } } start() { this.updateStatusButtons(); for (const p of this.game.players) { p.domElement.title = p.client.nameShown; } this.board.scrollIntoView(); this.board.focus(); this.update(); } handleEnd() { if (this.game) { this.game.exit(); this.game.pause(false); } } handlePause() { if (this.game) { this.game.pause(!this.game.paused); this.updateStatusButtons(); } } playerInteractivePoll(p) { this.playersPolling.add(p); if (!this.playerSelected) { this.selectPlayer(p); } else { this.selectPlayer(this.playerSelected); // In case it was selected but not polling. } if (p.interactiveTarget) { p.client.handleInput(p.interactiveTarget.x + " " + p.interactiveTarget.y); } } selectPlayer(p) { if (this.playerSelected && p != this.playerSelected) { this.playerSelected.domElement.classList.remove("game-player-selected"); } this.playerSelected = p; if (this.playersPolling.has(p)) { this.playerPolling = p; } else { this.playerPolling = null; } if (p) { p.domElement.classList.add("game-player-selected"); } this.update(); } handleClick(e) { if (!this.game) return; if (this.playerPolling) { const rect = e.currentTarget.getBoundingClientRect(); const x = Math.round((e.clientX - rect.left - this.offset) / this.scale); const y = -Math.round((e.clientY - rect.top - this.offset) / this.scale); const p = this.playerPolling; const send = !p.interactiveTarget; if (Math.abs(x) <= this.game.coordMax && Math.abs(y) <= this.game.coordMax) { p.interactiveTarget = {x, y}; if (send) { p.client.handleInput(x + " " + y); } } } else { const p = this.game.players.find(p => p.domElement == e.target); if (p) { this.selectPlayer(p); } } } handleKeyPress(e) { if (!this.game) return; if (e.key == "Escape") { if (this.playerPolling) { this.playerPolling.client.close(); } return; } const p = e.key != "Tab" ? this.game.players.find(p => p.domLetter == e.key.toLowerCase()) : this.game.players[(this.game.players.indexOf(this.playerSelected) + 1) % this.game.players.length]; if (p) { e.preventDefault(); this.selectPlayer(p); } } updateElement(p) { p.domElement.style.left = (p.x - p.r) * this.scale + this.offset + "px"; p.domElement.style.top = (-p.y - p.r) * this.scale + this.offset + "px"; p.domElement.style.boxSizing = "border-box"; p.domElement.style.width = (p.r * 2 * this.scale) + "px"; p.domElement.style.height = (p.r * 2 * this.scale) + "px"; } drawShadow(o, p, ctx) { const distShadowEnd = (this.offset + this.offset) * 2 / this.scale; const distObject = Math.hypot(o.y - p.y, o.x - p.x); const dirObject = Math.atan2(o.y - p.y, o.x - p.x); let dirVisible = dirObject + Math.PI, dirObstructed = dirObject; while (dirVisible - dirObstructed > 0.001) { const dir = (dirVisible + dirObstructed) / 2; const e = { x: p.x + distShadowEnd * Math.cos(dir), y: p.y + distShadowEnd * Math.sin(dir) }; if (!this.game.radiusContainsLine(o.x, o.y, o.r, p.x, p.y, e.x, e.y)) { dirVisible = dir; } else { dirObstructed = dir; } } const halfAngle = dirObstructed - dirObject; const distTangentPoint = distObject * Math.cos(halfAngle); ctx.beginPath(); const coordX = x => this.scale * x + this.offset; const coordY = y => this.scale * -y + this.offset; ctx.moveTo(coordX(p.x + distShadowEnd * Math.cos(dirObject - halfAngle)), coordY(p.y + distShadowEnd * Math.sin(dirObject - halfAngle))); ctx.lineTo(coordX(p.x + distTangentPoint * Math.cos(dirObject - halfAngle)), coordY(p.y + distTangentPoint * Math.sin(dirObject - halfAngle))); ctx.lineTo(coordX(p.x + distTangentPoint * Math.cos(dirObject + halfAngle)), coordY(p.y + distTangentPoint * Math.sin(dirObject + halfAngle))); ctx.lineTo(coordX(p.x + distShadowEnd * Math.cos(dirObject + halfAngle)), coordY(p.y + distShadowEnd * Math.sin(dirObject + halfAngle))); ctx.lineTo(coordX(p.x + distShadowEnd * Math.cos(dirObject)), coordY(p.y + distShadowEnd * Math.sin(dirObject))); ctx.fill(); } handleGamepad() { if (!this.game) return; if (this.playerPolling) { const p = this.playerPolling; const send = !p.interactiveTarget; const pads = navigator.getGamepads(); if (pads.length == 0) return; const pad = pads[0]; let steer_x = pad.axes[0]; let steer_y = -pad.axes[1]; const dead_zone = 0.005; if (Math.abs(steer_x) < dead_zone) { steer_x = 0; } if (Math.abs(steer_y) < dead_zone) { steer_y = 0; } const steer_magnitude = Math.sqrt(steer_x * steer_x + steer_y * steer_y); const x = parseInt(p.x + 16 * steer_x / steer_magnitude); const y = parseInt(p.y + 16 * steer_y / steer_magnitude); if (Math.abs(x) <= this.game.coordMax && Math.abs(y) <= this.game.coordMax) { p.interactiveTarget = {x, y}; if (send) { p.client.handleInput(x + " " + y); } } } } update() { if (!this.game) return; if (this.animationFrameRequest) { cancelAnimationFrame(this.animationFrameRequest); } this.animationFrameRequest = requestAnimationFrame(() => this.updateReal()); } updateReal() { this.animationFrameRequest = null; this.handleGamepad() const ctx = this.canvas.getContext('2d'); ctx.lineWidth = 0; ctx.fillStyle = "#ddd"; ctx.clearRect(0, 0, this.offset * 2, this.offset * 2); ctx.globalCompositeOperation = "multiply"; for (const o of this.game.obstacles) { this.updateElement(o); this.playerSelected && this.drawShadow(o, this.playerSelected, ctx); } const hideCompletely = !this.playerHidePartially.checked; for (const p of this.game.players) { this.updateElement(p); if (p.finished) { p.domElement.classList.add("game-player-finished"); } const notVisible = !!(this.playerSelected && !this.playerSelected.canSeeSomehow.includes(p)); p.domElement.classList.toggle("game-player-not-visible", notVisible); p.domElement.classList.toggle("game-player-not-visible-complete", notVisible && hideCompletely); } } exit() { this.updateStatusButtons(); this.output.textContent += JSON.stringify(this.game.summary, null, " ") + "\n"; this.output.classList.remove("hidden"); } uiShowError(msg) { this.output.textContent += msg + "\n"; this.output.classList.remove("hidden"); this.output.scrollIntoView(); } static endCurrent() { if (Game.current) { Game.current.end(); } } static killCurrent() { if (Game.current) { Game.current.finalize(); } } } class KilpailuProxyError extends Error { get name() { return "KilpailuProxy-virhe"; } } class KilpailyProxyRequestError extends KilpailuProxyError {} class KilpailyProxyConfigError extends KilpailuProxyError {} class KilpailyProxyPasswordError extends KilpailuProxyError {} class KilpailyProxyFileError extends KilpailuProxyError {} class KilpailuProxy { constructor(gameManager) { this.gameManager = gameManager; this.programs = []; this.requests = []; } connect(address, password) { if (this.ws) { throw new KilpailuProxyError("Logiikkavirhe! KilpailuProxy on jo yhdistetty."); } this.address = address; this.password = password; this.closedPromise = new_ManualPromise(); const promise = new_ManualPromise(); const failTimeoutId = setTimeout(() => promise.reject("KilpailuProxy ei vastaa."), 1000); const ws = createWebSocket(this.address, promise.reject); if (!ws) { return promise; } this.ws = ws; const handleFirstMessage = ev => { ws.removeEventListener("message", handleFirstMessage); ws.addEventListener("message", ev => this.handleMessage(JSON.parse(ev.data))); this.handleFirstMessage(ev.data, promise, ws); clearTimeout(failTimeoutId); }; ws.addEventListener("message", handleFirstMessage); ws.addEventListener("close", ev => this.close()); return promise; } async handleFirstMessage(data, promise, ws) { try { const msg = JSON.parse(data); if (msg.KilpailuProxy) { this.storage = msg.storage; this.programs = msg.programs || []; for (const p of this.programs) { p.address = p.address || this.address + p.name; p.version = p.version || WebSocketClient.getGreeting(p.address); } for (const p of this.programs) { p.version = await p.version || ""; this.gameManager.addProgramFromProxy(p.name, p.version, p.address); } promise.resolve(); return; } console.error(msg); } catch (ex) { console.error(ex); } promise.reject("KilpailuProxy antaa virheellistä dataa!"); ws.close(); } handleMessage(msg) { this.requests.shift()(msg); } async fileRequest(req) { req.password = this.password; try { this.ws.send(JSON.stringify(req)); } catch (ex) { throw new KilpailuProxyError("Yhteys on katkennut!"); } const r = await new Promise(resolve => this.requests.push(resolve)); if (!r || !r.request) { throw new KilpailyProxyRequestError("Määrittämätön virhe tiedostopyynnössä."); } if (!r.password) { throw new KilpailyProxyPasswordError("Väärä salasana tiedostopyynnössä."); } if (!r.config) { throw new KilpailyProxyConfigError(`KilpailuProxy ei salli tiedoston ${req.action == "read" ? "lukemista" : "kirjoittamista"}.`); } if (!r.success) { throw new KilpailyProxyFileError(r.data); } return r.data; } async loadFile(name) { return await this.fileRequest({action: "read", file: name}); } async storeFile(name, data) { return await this.fileRequest({action: "write", file: name, data}); } close() { if (this.ws) { this.ws.close(); this.ws = null; } for (const p of this.programs) { this.gameManager.removeProgramFromProxy(p.name); } this.programs = []; if (this.closedPromise) { this.closedPromise.resolve(); } } } class GameManager { constructor(options = {}) { this.other = options.other; this.useFromOther = true; this.programs = new Map(); this.proxyAddress = null; this.onAddProgramFromProxy = options.onAddProgramFromProxy || Function.prototype; this.onRemoveProgramFromProxy = options.onRemoveProgramFromProxy || Function.prototype; } getPrograms() { let a = Array.from(this.programs.keys()); if (this.other && this.useFromOther) { a = a.concat(this.other.getPrograms().filter(p => this.useFromOther === true || this.useFromOther.includes(p))); } return this.constructor.sortPrograms(a); } static sortPrograms(arr) { arr.sort((a_, b_) => { const a = a_.name || a_, b = b_.name || b_; const aa = a.toLowerCase(), bb = b.toLowerCase(); return aa == bb ? (a == b ? 0 : (a < b ? -1 : 1)) : (aa < bb ? -1 : 1); }); return arr; } getProgram(name) { return this.programs.get(name) || this.other && this.useFromOther && this.other.getProgram(name); } hasProgram(name) { return !this.programs.has(name); } addProgram(name, version, create) { const isNew = !this.programs.has(name); this.programs.set(name, {name, version, create}); return isNew; } removeProgram(name) { return this.programs.delete(name); } addProgramAddress(name, version, address) { if (!address.match(/^wss?:\/\//)) { throw new UIError("Osoite " + address + " ei kelpaa!"); } return this.addProgram(name, version, options => { options.address = address; return new WebSocketClient(options); }); } addProgramFromProxy(name, version, address) { if (this.addProgramAddress(name, version, address)) { const p = this.programs.get(name); p.proxy = true; this.onAddProgramFromProxy(p); } } removeProgramFromProxy(name) { const p = this.programs.get(name); if (p && p.proxy) { this.removeProgram(name); this.onRemoveProgramFromProxy(p); } } addProgramJs(name, version, code) { version = version || "hash-" + javaStringHash(code.toString().trim()); return this.addProgramFrame(name, version, SandboxFrame.create(name, code)); } addProgramFrame(name, version, sandboxFrame) { return this.addProgram(name, version, options => { options.sandboxFrame = sandboxFrame; return new SandboxFrameClient(options); }); } addProgramFunction(name, version, func) { version = version || "hash-" + javaStringHash(func.toString().trim()); return this.addProgram(name, version, options => { const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; if (func instanceof AsyncFunction) { options.function = func; } else { options.function = async(input, output) => { const exit = () => { throw null; }; const mainfunc = func(output, exit); try { while (1) mainfunc(await input()); } catch (ex) { } } } return new FunctionClient(options); }); } newClient(name, options) { if (options.replayData != null) { return new ReplayClient(options); } if (options.interactive) { return new InteractiveClient(options); } const p = this.getProgram(name); if (!p) { throw new UIError("Ohjelmaa " + name + " ei ole."); } options.version = p.version || ""; return p.create(options); } newGame(options) { const counter0 = {}, counter = {}; options.players.forEach(p => counter0[p.name] = (counter0[p.name] || 0) + 1); options.players = options.players.map(p => { const name = p.name; if (counter0[p.name] > 1) { const n = counter[p.name] = (counter[p.name] || 0) + 1; p.nameShown = p.name + " #" + n; } p.handleError = p.handleError || options.handleError; return this.newClient(name, p); }); return new Game(options); } async loadSummary(id) { if (!this.proxy) return; const data = await this.proxy.loadFile(id + ".json"); try { const summary = JSON.parse(data); if (summary.greeting == Game.GREETING) { return summary; } } catch (ex) { } throw new UIError("Väärää dataa tiedostossa."); } async storeSummary(summary) { if (!this.proxy) return; await this.proxy.storeFile(summary.identifier + ".json", JSON.stringify(summary)); } async connectProxy(address, password) { this.disconnectProxy(); try { this.proxy = new KilpailuProxy(this); await this.proxy.connect(address, password); } catch (ex) { this.proxy = null; console.error(ex); throw ex; } } disconnectProxy() { for (const p of this.programs.values()) { if (p.proxy) { this.removeProgram(p.name, true); } } if (this.proxy) { this.proxy.close(); this.proxy = null; } } } class TournamentManager extends GameManager { constructor(options) { super(options); this.useFromOther = false; this.gameVisible = false; } useExistingPrograms() { this.useFromOther = true; } setGameVisible(value) { this.gameVisible = value; } setRoundsPerSecond(value) { GameUI.setRoundsPerSecond(value); } *getCombinations(items, count, options) { const permutations = options && options.permutations; const multiple = options && options.multiple; const exited = () => this.exited; function* recursion(all, count) { if (exited()) { return; } for (let i = 0; count && i < all.length; ++i) { const first = all[i]; if (count == 1) { yield [first]; continue; } let rest = [].concat( (permutations ? all.slice(0, i) : []), (multiple ? [all[i]] : []), all.slice(i+1) ); for (const tail of recursion(rest, count - 1)) { yield [first].concat(tail); } } } if (!permutations) { items = this.constructor.sortPrograms(items.concat()); } if (options.not) { items = items.filter(i => !options.not.includes(i)); } yield* recursion(items, count); } async runGame(options) { if (this.exited) { throw new UIError("Turnaus on lopetettu."); } if (this.gameVisible) { UI.changeTab("tab-game"); } options.handleError = s => this.log(s); options.ui = this.gameVisible ? GameUI : false; options.players = options.players.map(name => ({name})); this.game = this.newGame(options); await this.game.run(); const summary = this.game.summary; return summary; } async loadSummary(id) { try { return await super.loadSummary(id); } catch (ex) { if (!(ex instanceof KilpailyProxyFileError)) { throw ex; } } } async runGameCached(options) { const id = Game.makeIdentifier(options); let summary = await this.loadSummary(id); const needsUpdate = !summary || options.players.some((name, i) => { const p = this.getProgram(name), o = summary.players[i]; return !p || !o || o.name != p.name || o.version != p.version; }); if (needsUpdate) { options.logProgress && this.log("+ " + id + " ..."); summary = await this.runGame(options); await this.storeSummary(summary); } else { options.logProgress && this.log("- " + id + " (cache)"); } return summary; } async exit() { if (this.exited) return; this.log("Lopetetaan..."); this.exited = true; this.game && await this.game.exit(); } log(s) { const el = $("#tournament-status"); this.log_ = this.log_ || ["", "", ""]; if (this.log_.slice(-3).every(line => line == s)) { if (this.log_every_) { return; } this.log_every_ = true; el.value += "+++ " + s + "\n"; el.scrollTop = el.scrollHeight; return; } this.log_every_ = false; this.log_.push(s); if (this.log_.length > 500) { this.log_ = this.log_.slice(-300); el.value = this.log_.join("\n") + "\n"; } else { el.value += s + "\n"; } el.scrollTop = el.scrollHeight; } showLog() { UI.changeTab("tab-tournament"); const el = $("#tournament-status"); el.scrollIntoView(); el.scrollTop = el.scrollHeight; } async start(code) { await this.exit(); this.exited = false; try { $("#tournament-status").setCustomValidity(""); $("#tournament-status").value = ""; $("#tournament-script").value; const f = eval("(" + code + ")"); await f(this); this.log("Valmis!"); } catch (ex) { if (ex instanceof KilpailuProxyError) { ex = "Virhe: " + ex.message; } else if (ex instanceof Error) { console.error(ex); ex = ex.lineNumber + " : " + ex.columnNumber + " : " + ex; } this.log(ex); $("#tournament-status").setCustomValidity(ex); this.showLog(); } UI.changeTab("tab-tournament"); $("#tournament-status").scrollIntoView(); this.exited = true; } } const UI = new class { setGameRoundsPerSecond(input) { const value = input.value.match(/^[0-9]+$/) ? parseInt(input.value) : 1; if (value) { GameUI.setRoundsPerSecond(value); } } async exitGameAndTournament() { this.tournamentManager && await this.tournamentManager.exit(); this.game && await this.game.exit(); } async handleStartGame() { await this.exitGameAndTournament(); try { if (!$("#start-rounds-per-second").validity.valid) throw "Tarkasta asetukset!"; if (!$("#start-board-seed").validity.valid) throw "Tarkasta asetukset!"; if ($$("#players .player-name").length < 4) throw "Tarkasta pelaajien määrä!"; } catch (ex) { this.showError($("#start-status"), ex, 4500); return; } let data = { boardSeed: +$("#start-board-seed").value, players: [], handleError: s => GameUI.uiShowError(s), ui: GameUI }; for (const name of $$("#players .player-name")) { data.players.push(name.value ? {name: name.value} : {name: "Ihminen", interactive: true}); } this.setGameRoundsPerSecond($("#start-rounds-per-second")); this.startGame(data, $("#start-status")); } async startGame(data, statusElement) { try { this.game = this.gameManager.newGame(data); } catch (ex) { this.showError(statusElement, ex, 4500); return; } this.changeTab("tab-game"); await this.game.run(); } async handleStoreGame() { try { if (!this.game) { throw new UIError("Pelaa ensin."); } if (!this.game.finished) { throw new UIError("Peli on kesken."); } if (this.game.summary.players.some(p => p.interactive)) { throw new UIError("Pelissä on ihmispelaajia, ei tallenneta."); } if (!this.gameManager.proxy) { throw new UIError("KilpailuProxy puuttuu."); } await this.gameManager.storeSummary(this.game.summary); this.showStatus($("#game-store-status"), "OK!", 1500); } catch (ex) { this.showError($("#game-store-status"), ex, 3500); } } async handleReplayGame() { await this.exitGameAndTournament(); let data; try { data = JSON.parse($("#replay-data").value); if (data.greeting != Game.GREETING || typeof data.players != typeof []) { throw null; } } catch (ex) { this.showError($("#replay-status"), "Viallista dataa!", 1500); return; } for (let i = 0; i < data.players.length; ++i) { const p = data.players[i]; if (data.replay && data.replay[i] != null) { p.replayData = data.replay[i]; } } data.ui = GameUI; this.setGameRoundsPerSecond($("#replay-rounds-per-second")); this.startGame(data, $("#replay-status")); } async updateTournamentButton() { const b = $("#tournament-start-stop"); if (this.tournamentManager && !this.tournamentManager.exited) { b.textContent = b.dataset.textStop; } else { b.textContent = b.dataset.textStart; } } async handleTournament() { if (this.tournamentManager && !this.tournamentManager.exited) { await this.tournamentManager.exit(); this.updateTournamentButton(); return; } const code = $("#tournament-script").value; if (!this.validateScript(code, $("#tournament-script"), $("#tournament-status"))) { return; } await this.exitGameAndTournament(); this.tournamentManager = new TournamentManager({other: this.gameManager}); this.updateTournamentButton(); await this.tournamentManager.start(code); this.updateTournamentButton(); } changeTab(name) { $$(".tab").forEach(t => t.classList.toggle("tab-visible", t.id == name)); } addPlayer(val) { let p = $("#player-template").cloneNode(true); $("#players").appendChild(p); $(p, ".player-name").value = val || ""; $(p, ".player-remove").addEventListener("click", function() { if (p.parentNode.childNodes.length > 4) { p.remove(); } }) } updateProgramList() { const template = $("#player-template .player-name"); for (let el of $$("#players .player-name")) { const value = el.value; el.innerHTML = template.innerHTML; el.value = value; } } addProgram(name, text) { const opt = $$$("<option></option>"); opt.value = name; opt.textContent = text || name; $("#player-template .player-name").appendChild(opt); this.updateProgramList(); } removeProgram(name) { this.gameManager.removeProgram(name); $$(".player-name option").forEach(el => el.value == name && el.remove()); } addFinalProgram(name, version, number, func) { const opt = $$$("<option></option>"); opt.value = name; if (func) { this.gameManager.addProgramFunction(name, version, func); opt.textContent = number + ". " + name; } else { opt.textContent = number + ". " + name + " :("; opt.disabled = true; } $("#player-template .player-name").appendChild(opt); this.updateProgramList(); } initPrograms() { this.addProgram("", "Ihminen"); let players = 0; CompetitionPrograms.forEach(entry => { if (entry.place) { this.addFinalProgram(entry.name, entry.version, entry.place, entry.func); } else { this.gameManager.addProgramFunction(entry.name, entry.version, entry.func); this.addProgram(entry.name); } if (entry.func && entry.defaultGame) { this.addPlayer(entry.name); players += 1; } }); while (players++ < 4) { this.addPlayer("esim"); } } async show(el, s, ms) { el.textContent = s; el.classList.add("show"); el.dataset.showCount = (el.dataset.showCount || "") + "x"; await Clock.promiseTimeout(ms); if (!(el.dataset.showCount = el.dataset.showCount.substr(1))) { el.classList.remove("show"); } } showStatus(el, s, ms) { el.classList.remove("error"); this.show(el, s, ms); } showError(el, s, ms) { el.classList.add("error"); this.show(el, s, ms); } async handleNewWsProgram() { if (!$("#new-ws-program-name").validity.valid || !$("#new-ws-program-address").validity.valid) { return; } const name = $("#new-ws-program-name").value; const address = $("#new-ws-program-address").value; try { const version = await WebSocketClient.getGreeting(address) || ""; if (this.gameManager.addProgramAddress(name, version, address)) { this.addProgram(name, name + " / socket"); } this.showStatus($("#new-ws-program-status"), "OK!", 1500); } catch (ex) { this.showError($("#new-ws-program-status"), ex, 1500); } } validateScript(code, input, status) { try { new Function(code); return true; } catch (ex) { const s = ex.lineNumber + " : " + ex.columnNumber + " : " + ex; input.setCustomValidity(s); if (status instanceof HTMLTextAreaElement || status instanceof HTMLInputElement) { status.value = s; } else { this.showError(status, s, 1500); } return false; } } handleNewJsProgram() { if (!$("#new-js-program-name").validity.valid) { return; } const name = $("#new-js-program-name").value; const code = $("#new-js-program-code").value; if (!this.validateScript("(" + code + ")", $("#new-js-program-code"), $("#new-js-program-status"))) { return; } $("#new-js-program-code").setCustomValidity(""); if (this.gameManager.addProgramJs(name, "", code)) { this.addProgram(name, name + " / js"); } this.showStatus($("#new-js-program-status"), "OK!", 1500); } async handleProxyConnect() { $("#KilpailuProxy-status").textContent = "Yhdistetään..."; $("#KilpailuProxy-status").classList.remove("error"); try { await this.gameManager.connectProxy($("#KilpailuProxy-ws").value, $("#KilpailuProxy-password").value); const programs = this.gameManager.proxy.programs.map(x => x.name).join(", "); const status = "Yhdistetty. " + (programs ? `Ohjelmat: ${programs}.` : " Ei ohjelmia."); $("#KilpailuProxy-status").textContent = status; const p = this.gameManager.proxy; await this.gameManager.proxy.closedPromise; if (this.gameManager.proxy == p) { $("#KilpailuProxy-status").textContent = "Yhteys katkesi!"; $("#KilpailuProxy-status").classList.add("error"); } } catch (ex) { $("#KilpailuProxy-status").textContent = ex; $("#KilpailuProxy-status").classList.add("error"); } } init() { this.gameManager = new GameManager({ onAddProgramFromProxy: p => p.proxy && this.addProgram(p.name, p.name + " / proxy"), onRemoveProgramFromProxy: p => this.removeProgram(p.name) }); if (location.href.match(/^file:/)) { $$(".remove-if-url-file").forEach(e => e.remove()); } this.changeTab("tab-start"); for (const b of $$(".tab-button")) { b.addEventListener("click", e => { this.changeTab(b.dataset.tab); e.preventDefault(); }); } $$("input, textarea, select").forEach(e => { e.autocomplete = "off"; e.value = e.defaultValue; e.checked = e.defaultChecked; }); const ws_a = $("#new-ws-program-address"), ws_n = $("#new-ws-program-name"); ws_a.addEventListener("input", () => { if (ws_n.dataset.changed) return; ws_n.value = ws_a.value.replace(/.*\//, ''); }); ws_n.addEventListener("input", () => ws_n.dataset.changed = "1"); $("#KilpailuProxy-connect").addEventListener("click", () => { this.handleProxyConnect() }); $("#new-ws-program-button").addEventListener("click", () => { this.handleNewWsProgram() }); $("#new-js-program-button").addEventListener("click", () => { this.handleNewJsProgram() }); GameUI.firstInit(); $("#start-button").addEventListener("click", () => { this.handleStartGame() }); $("#players-add").addEventListener("click", () => { this.addPlayer() }); $("#replay-file").addEventListener("change", async function() { $("#replay-data").value = await this.files[0].text(); this.value = ""; }); $("#replay-button").addEventListener("click", () => { this.handleReplayGame() }); $("#game-store").addEventListener("click", () => { this.handleStoreGame() }); $("#game-content").addEventListener("dragstart", e => { e.preventDefault() }); $("#game-content").addEventListener("selectstart", e => { e.preventDefault() }); this.initPrograms(); $("#tournament-script-file").addEventListener("change", async function() { $("#tournament-script").value = await this.files[0].text(); this.value = ""; }); const resetTournamentScript = () => $("#tournament-script").value = tournamentExampleFunction.toString(); resetTournamentScript(); $("#tournament-script-default").addEventListener("click", () => { resetTournamentScript(); }); $("#tournament-start-stop").addEventListener("click", () => { this.handleTournament() }); } } async function tournamentExampleFunction(lib) { // Yhdistetään KilpailuProxyyn (osoite, salasana); siellä olevat ohjelmat tulevat mukaan. await lib.connectProxy("ws://127.0.0.1:50001/", ""); // Voidaan ottaa mukaan myös ohjelmat, jotka ovat jo pelisivun valikossa. // lib.useExistingPrograms(); // Haetaan lista kaikista käytössä olevista ohjelmista. let programs = lib.getPrograms(); // Voidaan rajoittaa lista vain tiettyihin ohjelmiin. // programs = programs.filter(name => ["esim", "JsAivo"].includes(name)); // Asetetaan pelin nopeus, jos peli halutaan näyttää. lib.setGameVisible(true); lib.setRoundsPerSecond(20); // Valmistellaan tuloksia. const results = programs.map(name => ({name, pointsHider: 0, pointsSeeker: 0, pointsPenalty: 0})); // Luodaan kaikki kolmen ohjelman yhdistelmät, ei useaa samaa ohjelmaa, ei eri järjestyksiä. const games = lib.getCombinations(programs, 3, {multiple: false, permutations: false}); for (const seekers of games) { // Muut saavat piiloutua. const others = programs.filter(p => !seekers.includes(p)); const players = seekers.concat(others); const options = { players, boardSeed: javaStringHash(players.join(",")), logProgress: true }; const summary = await lib.runGameCached(options); // Tässä voi käsitellä pelin tulosta esimerkiksi tilastoja varten. for (const p of summary.players) { const result = results.find(o => o.name == p.name); if (seekers.includes(p.name)) { result.pointsSeeker += p.points; } else { result.pointsHider += p.points; } if (p.summary && p.summary.timeLimitExceeded) { lib.log(` HUOMIO! ${p.name} ylitti aikarajan!`); result.pointsPenalty -= Math.round(1000 * (p.summary.time - p.summary.timeLimit)); } if (p.summary && p.summary.memoryLimitExceeded) { lib.log(` HUOMIO! ${p.name} ylitti muistirajan!`); } } } // Järjestellään tulokset. results.forEach(p => p.points = p.pointsSeeker + p.pointsHider + p.pointsPenalty); results.sort((a, b) => b.points - a.points); results.forEach(p => p.place = 1 + results.findIndex(o => o.points == p.points)); // Näytetään tulokset tekstimuodossa. const strlen = s => s.toString().length; const nameLength = Math.max.apply(0, results.map(p => strlen(p.name))); const pointLength = Math.max.apply(0, results.map(p => strlen(p.pointsSeeker))) + 2; const placeLength = strlen(results.length + 1); for (const p of results) { let place = p.place.toString().padStart(placeLength, " "); let name = p.name.padStart(nameLength, " "); let p1 = p.points.toString().padStart(pointLength, " "); let p2 = p.pointsSeeker.toString().padStart(pointLength, " "); let p3 = p.pointsHider.toString().padStart(pointLength, " "); let p4 = (-p.pointsPenalty).toString().padStart(pointLength, " "); lib.log(`${place}. ${name} ${p1} = ${p2} + ${p3} ( - ${p4} )`); } } window.onload = e => { UI.init(); }; window.onunload = Function.prototype; </script> <style> .tab, .hidden { display: none; } .tab-visible { display: block; } .sandboxed-worker-frame { display: block; position: fixed; border: 0; left: 0; top: 0; width: 1px; height: 1px; } .status:not(.show) { visibility: hidden; opacity: 0; transition: visibility 1s, opacity 1s linear; } .status.show { visibility: visible; opacity: 1.0; } .status.error { text-shadow: #c33 0 0 0.2em; } .game-board { position: relative; width: 512px; height: 512px; overflow: hidden; border: 1px solid black; } .game-player, .game-obstacle { display: block; position: absolute; cursor: default; text-align: center; border: 1px solid black; width: 16px; height: 16px; border-radius: 50%; font-size: 80%; line-height: 16px; } .game-player::before { display: block; position: absolute; left: 1.4em; content: attr(title); white-space: nowrap; text-shadow: 1px 1px 1px white; z-index: 3; } .game-obstacle { z-index: 2; border: 2px solid black; } .game-player-seeker { border-color: #633; background-color: #c99; } .game-player-other { border-color: #336; background-color: #99c; } .game-view { display: flex; flex-flow: row-reverse wrap; justify-content: center; } .game-view > * { flex: 0 1; margin: 1em; } .game-info { flex: 1; } .game-player-not-visible { opacity: 0.2; } .game-player-not-visible-complete { display: none; } .game-player-selected { box-shadow: 0 0 2px 2px #3c3; } .game-player-finished { opacity: 0.5; box-shadow: 0 0 2px 4px gray; } </style> <h1>Kuurupiilo</h1> <p>Versio 2019-12-02</p> <p id="tablist"> Sivut: <button class="tab-button" data-tab="tab-setup">Asetukset</button> <button class="tab-button" data-tab="tab-start">Uusi peli</button> <button class="tab-button" data-tab="tab-game">Peli</button> <button class="tab-button" data-tab="tab-tournament">Turnaus</button> <button class="tab-button" data-tab="tab-replay">Katselu</button> </p> <hr> <div id="tab-setup" class="tab"> <p><span class="remove-if-url-file">Jotta voit testata omaa tekoälyäsi, <a href="" download="kuurupiilo-testaus.html">tallenna tämä sivu</a> ja avaa se omalta koneeltasi.</span> Oman tekoälyn testaamista varten lataa <a href="https://www.ohjelmointiputka.net/tiedostot/KilpailuProxy.zip">KilpailuProxy</a>, lue sen ohjeet mukana tulevasta HTML-tiedostosta ja käynnistä tekoälysi sen avulla.</p> <p>Jos et halua lisätä omaa tekoälyä, <a class="tab-button" data-tab="tab-start" href="#tab-start">siirry pelaamaan</a>.</p> <h3>KilpailuProxy</h3> <p>Syötä KilpailuProxyn hallintaosoite alla olevaan tekstikenttään. Jos yhteys toimii, asetuksiin määritellyt ohjelmat tulevat seuraavan sivun pelaajavalikkoon.</p> <p> <input id="KilpailuProxy-ws" type="text" required pattern="wss?://.*" value="ws://127.0.0.1:50001/"> <input id="KilpailuProxy-password" type="password" placeholder="salasana, jos on"> <button id="KilpailuProxy-connect">Yhdistä!</button> </p> <p>Status: <span id="KilpailuProxy-status" class="status show">-</span></p> <h3>Muut WebSocket-ohjelmat</h3> <p>Tässä voit lisätä muita WebSocket-ohjelmia, esimerkiksi muiden kilpailijoiden antamia osoitteita.</p> <p> <input id="new-ws-program-name" required pattern="[-_0-9a-zA-Z]+" value="nimi"> @ <input id="new-ws-program-address" required pattern="wss?://.*" value="ws://127.0.0.1:50001/echo"> → <button id="new-ws-program-button">Lisää!</button> <span id="new-ws-program-status" class="status"></span> </p> <h3>JavaScript-ohjelmat</h3> <p>Tässä voit lisätä valikkoon JavaScript-ohjelmia, esimerkiksi muiden kilpailijoiden antamia harjoitusvastuksia. Ohjelman pitää olla yksi asynkroninen JavaScript-funktio. Sille annetaan parametreina asynkroninen lukufunktio ja tulostusfunktio. JavaScriptissa funktion sisällä voi olla muita funktioita ja näitä varten ”globaaleja” muuttujia. Laatikossa on tästä yksinkertainen esimerkki.</p> <p> <input type="text" id="new-js-program-name" required pattern="[-_0-9a-zA-Z]+" placeholder="nimi" value="jstesti"> → <button id="new-js-program-button">Lisää!</button> <span id="new-js-program-status" class="status"></span> </p> <p><textarea id="new-js-program-code" required rows="15" cols="100">async function jstesti(input, output) { // Alkutervehdys. output("jstesti v1"); const tervehdys = await input(); if (tervehdys == "ping") { return; } // TODO: Pelin aloitustiedot kannattaisi myös purkaa. const aloitustiedot = await input(); const numero = +aloitustiedot[1]; // Pelataan, kunnes tulee lopetusviesti. for (let kierros = 0;; ++kierros) { const rivi = await input(); if (!rivi || rivi == "0") { break; } // TODO: Käsitellään syöte, lasketaan jokin fiksu oma siirto. if (kierros % 200 == 0) { const x = Math.trunc(Math.cos(numero + (kierros % 357) * Math.PI / 180) * 1080); const y = Math.trunc(Math.sin(numero + (kierros % 357) * Math.PI / 180) * 1080); output(x + " " + y); } else { output("="); } } } </textarea></p> <details> <summary>Ohjelman runko Node.js:lle, SpiderMonkeylle ja selaimeen (kaikki samassa)</summary> <pre> (function() { // Ohjelma async-funktiona, kuten yllä esimerkissä. async function jstesti(input, output) { // ... } // Ohjelman suoritus. const ohjelma = jstesti; if (typeof window == "undefined") (async function run() { let finished = false, lines = [], promises = []; const exit = s => { try { process.exit(s) } catch (ex) {} try { quit(s) } catch (ex) {} }; const output = s => { try { process.stdout.write(s + "\n") } catch (ex) {} try { print(s) } catch (ex) {} }; const addInput = s => { if (promises.length) { promises.shift()(s); } else { lines.push(s); } }; const input = () => { if (finished) return; try { addInput(readline()); } catch (ex) {} if (lines.length) return lines.shift(); return new Promise(r => promises.push(r)); }; try { require("readline").createInterface({input: process.stdin}).on("line", addInput).on("close", () => finished = true); } catch (ex) { } try { await ohjelma(input, output); exit(0); } catch (ex) { output("ERROR: " + ex); console.error(ex); exit(1); } })(); return ohjelma; }()) </pre> </details> </div> <div id="tab-start" class="tab"> <h3>Pelin asetukset</h3> <p>Pelin nopeus: <input type="number" id="start-rounds-per-second" value="20" min="1" max="999999"> kierrosta sekunnissa</p> <p>Pelilaudan siemenluku: <input type="number" id="start-board-seed" value="0" min="0" max="2147483647"> (0 = satunnainen)</p> <h3>Pelaajat</h3> <ul id="players-template-ul" style="display: none"> <li id="player-template"> <select class="player-name"> <option disabled>Muokkaa listaa Asetukset-sivulta</option> </select> <button class="player-remove">Poista</button> </li> </ul> <ul id="players"></ul> <p> <button class="tab-button" data-tab="tab-setup">KilpailuProxy & ohjelmien lisäys</button> <button id="players-add">Lisää pelaaja</button> <button id="start-button">Aloita</button> <span id="start-status" class="status"></span> </p> <h3>Pelin säännöt</h3> <p>Pelaajat aloittavat pelialueen keskeltä. Kolme ensimmäistä pelaajaa ovat etsijöitä, ja muut piiloutuvat. Pelialueella on tätä varten pyöreitä esteitä. Piiloutujilla on 200 kierrosta etumatkaa, ennen kuin etsijöiden peli alkaa. Tämän jälkeen pelaajan on parasta vältellä etsijöitä: pelaaja putoaa pelistä, jos pysyy etsijöiden näköpiirissä yhtämittaisesti 50 kierroksen ajan.</p> <p>Pelimaailmassa koordinaatit ovat -1100 ≤ x, y ≤ 1100. Pelaajat aloittavat koordinaateista (0, 0). Pelaajaa kuvaa ympyrä, jonka säde on 32. Pelaaja liikkuu pelissä ilmoittamalla koordinaatit, joihin haluaa päästä. Kohdesijainnin on oltava pelialueella. Pelaajaa liikutetaan automaattisesti kohteeseen päin, kunnes pelaaja on päämäärässään tai valitsee uuden kohteen. Jos pelaajan valitsema kohde on esteen sisällä, pelaajaa kuljetetaan lähimpään kohtaan esteen reunalle, ja kun pelaaja saavuttaa kohdettaan lähimmän pisteen, pelaaja pysähtyy siihen. Pelaaja liikkuu enintään 8 yksikköä per kierros, ja pelaaja siirtyy aina kokonaislukukoordinaatteihin. Alkupisteestä voi siis päästä esimerkiksi kohtaan (8, 0) tai (7, 3) tai (4, -6). Pelaajien ei tarvitse väistää toisiaan.</p> <p>Esteet ovat ympyröitä, esteen säde on enintään 180, ja esteitä on enintään 40. Pelaaja väistää automaattisesti esteet niin, että ympyrät eivät osu toisiinsa. Toisin sanoen pelaajan sijainnin ja esteen sijainnin välinen etäisyys on aina suurempi kuin pelaajan säteen ja esteen säteen summa. Etäisyyden voi tarkastaa koordinaateista Pythagoraan lauseella.</p> <p>Pelaaja näkee toisen, jos pelaajien koordinaattien välinen jana ei kulje minkään esteen kautta eikä edes sivua estettä. Joka kierroksen alussa pelaaja saa tiedon kaikista niistä pelaajista, jotka itse näkee. Etsijä saa tiedon kaikista, jotka edes joku etsijöistä näkee, eli etsijät voivat tehdä helpommin yhteistyötä. Pelaajasta kerrottavat tiedot ovat pelaajan numero, pelaajan nykyiset koordinaatit (x, y), pelaajan kulkukohteen koordinaatit (kohde_x, kohde_y) ja kierrosmäärä, kuinka kauan pelaaja on parhaillaan ollut etsijöiden näkyvillä. Jos pelaaja on etsijöiltä piilossa, kierrosmääränä on 0. Etsijän kohdalla kierrosmääränä on aina -1. Pelaaja jatkaa kesken olevan liikkumisensa loppuun silloinkin, kun on jo muuten pudonnut pelistä. Jos pelaajan kohde olisi esteen sisällä ja pelaaja on jo mahdollisimman lähellä kohdetta, pelaajan kohteeksi merkitään pelaajan saavuttama sijainti.</p> <p>Aina 4800 kierroksen jälkeen esteet pienenevät: esteen säteeksi tulee 3/4 aiemmasta säteestä (pyöristettynä pienempään kokonaislukuun), ja jos säde on tämän myötä 8 tai alle, este poistetaan kokonaan. Esteiden pienentäminen varmistaa sen, että kaikki piiloutujat joutuvat näkyville lopulta – viimeistään, kun viimeinenkin este katoaa. <em>Huomio!</em> Etsijöiden peli alkaa 200 kierrosta myöhemmin, eli etsijän näkökulmasta esteet pienenevät ensimmäisen kerran jo 4600 kierroksen jälkeen!</p> <p>Pisteitä jaetaan siitä alkaen, kun etsijät pääsevät liikkeelle. Piiloutuja saa joka kierroksella 3 pistettä, kunnes putoaa pelistä. Etsijä saa joka kierroksella miinuspisteitä sen verran, kuin piiloutujia on vielä mukana. Peli päättyy, kun kaikki piiloutujat ovat pudonneet pelistä.</p> <h3>Ohjelman toiminta</h3> <p>Tekoäly on käynnissä aina yhden pelin ajan. Se on komentoriviohjelma, joka lukee kilpailuohjelman syötettä (muiden siirtoja) kuten yleensä käyttäjän syötettä (näppäimistöä) ja tulostaa joka kierroksella oman siirtonsa kuten ruudullekin. Huomio! Useissa kielissä pitää tulostuksen jälkeen kutsua flush-funktiota tai vastaavaa.</p> <p>Ohjelman suoritus alkaa tervehdyksestä. Ohjelman pitää ensin tulostaa rivi, jolla on ohjelman nimi ja versio vapaassa muodossa.</p> <p>Ohjelmalle syötetään rivi <em>2019-kuurupiilo</em> merkiksi, että kyseessä on tämän sivun mukainen peli. (Jos ohjelmalle syötetään sen sijaan tervehdysrivi <em>ping</em>, ohjelman tulee vain sammua heti. Testaussivu hakee tällä tavalla ohjelman versionumeron turnausta varten.)</p> <p>Tämän jälkeen ohjelmalle syötetään yhdellä rivillä välilyönneillä eroteltuina seuraavat kokonaisluvut: pelaajien määrä, pelaajan oma numero, esteiden määrä ja järjestyksessä jokaisen esteen tiedot (x-koordinaatti, y-koordinaatti, säde). Pelaajien numerointi alkaa ykkösestä. Ensimmäiset pelaajat (1, 2, 3) ovat etsijöitä. Ohjelma voi olettaa, että pelissä on enintään 100 pelaajaa. Esteitä on enintään 40.</p> <p>Joka kierroksella ohjelmalle syötetään yksi rivi. Jos rivi on 0, peli päättyy ja ohjelman täytyy sulkeutua. Muuten rivin ensimmäinen luku kertoo, monenko pelaajan tiedot rivillä on. Tätä seuraavat järjestyksessä kustakin pelaajasta kuusi lukua: pelaajan numero, x-koordinaatti, y-koordinaatti, kulkukohteen x-koordinaatti ja y-koordinaatti sekä tieto, monellako peräkkäisellä kierroksella pelaaja on ollut etsijöiden näkyvillä. Etsijän kohdalla kierrosmääränä on -1. Myös pelistä pudonneet pelaajat näkyvät, kierrosmääränä on silloin 50. Pelaaja näkee aina vähintään itsensä, ja etsijät näkevät aina toisensa sijainnista riippumatta.</p> <p>Joka kierroksella ohjelman täytyy tulostaa välilyönnillä erotettuina x-koordinaatti ja y-koordinaatti, joita kohti pelaaja kulkee, tai pelkkä =-merkki, jos pelaaja jatkaa edelleen aiempaan kohteeseensa. Koordinaatit ilmoitetaan kokonaislukuina, ja niiden täytyy olla pelialueen sisällä.</p> <p>Jos tekoälyn peli keskeytyy virheen tai kaatumisen tai muun syyn vuoksi, pelaajaa ei voi enää ohjata mutta pisteet kertyvät pelin loppuun saakka normaalien sääntöjen mukaan. Jos tekoäly ylittää aikarajan, se saa sakkopisteen jokaisesta aikarajan ylittävästä millisekunnista.</p> <h3>Huomioita testaussivusta</h3> <p>Ohjelma tehdään kilpailuun komentoriviohjelmana varsinaisten kilpailuohjeiden mukaisilla kielillä. Ohjelma yhdistetään tähän sivuun KilpailuProxylla, kuten Asetukset-sivulla neuvotaan.</p> <p>Pelin koordinaatisto on oikeakätinen, eli ruudulla y-koordinaatti kasvaa alhaalta ylös ja x-koordinaatti vasemmalta oikealle. Testaussivulla näkyvän pelin mittakaava on 1:4, eli yksi pikseli on 4 pelin mittayksikköä.</p> <p>Reitti voi näyttää ruudulla mutkittelevalta, koska kokonaislukulaskujen takia lyhin reitti ei kulje aina suoraan. Toisaalta usean peräkkäisen esteen kiertäminen ei välttämättä suju automaattisesti tehokkainta tai toivottua reittiä, joten omaa ohjelmaa tehdessä voi olla hyvä kiinnittää huomiota myös reitinvalintaan.</p> <p>Testaussivun antamat tiedot ajan- ja muistinkäytöstä riippuvat koneesta eivätkä siis suoraan vastaa kisakoneella saatavia tuloksia.</p> </div> <div id="tab-game" class="tab"> <div class="game-view"> <div class="game-info"> <p>Jos ihmispelaajia on monta, näppäimistöllä <span id="game-human-letters"></span> valitaan ohjattava pelaaja. Ruudulta klikataan kulkusuunta. Peli alkaa, kun kaikki piiloutujat ovat alkaneet liikkua. Jos ihmisiä ei ole pelissä, voit klikkaamalla valita tekoälyn, jonka näkökulmasta peli näytetään.</p> <p>Pelin nopeus: <input type="number" id="game-rounds-per-second" value="20"> kierrosta sekunnissa</p> <p><label>Piiloutuneet näkyvillä: <input type="checkbox" id="game-player-hide-partially" value="1" checked></label></p> <p><button id="game-pause" data-pause="Tauko" data-continue="Jatka">Tauko</button> <button id="game-end">Lopeta</button></p> <p><button id="game-store">Tallenna tulos</button> <span id="game-store-status" class="status"></span></p> <p>Pelin yhteenveto tulee lopuksi peliruudun alapuolelle.</p> </div> <div id="game-content"> <div class="game-board" id="game-board"></div> </div> </div> <pre id="game-output"></pre> </div> <div id="tab-tournament" class="tab"> <p>Turnauksen kulku koodataan JavaScriptilla seuraavan esimerkin mukaisesti. Ottelutiedot tallennetaan KilpailuProxyn kautta, joten tarkista sen asetukset!</p> <p>Turnausskripti: <button id="tournament-script-default">Oletus</button> Tiedosto: <input type="file" id="tournament-script-file"></p> <p><textarea id="tournament-script" rows="15" cols="100"></textarea></p> <p><button id="tournament-start-stop" data-text-start="Aja!" data-text-stop="Lopeta">Aja!</button></p> <p><textarea id="tournament-status" rows="25" cols="100"></textarea></p> </div> <div id="tab-replay" class="tab"> <p>Tässä voit ladata aiemman pelin katsottavaksi. Tähän tarvitaan JSON-data, joka näkyy pelisivun alareunassa ottelun päättyessä tai jonka KilpailuProxy tallentaa.</p> <p>Pelin nopeus: <input type="number" id="replay-rounds-per-second" value="20"> kierrosta sekunnissa</p> <p>Tiedosto: <input type="file" id="replay-file"></p> <p>Data:<br><textarea rows="10" cols="100" id="replay-data"></textarea></p> <p><button id="replay-button">Aloita</button> <span id="replay-status" class="status"></span></p> </div> <script> 'use strict'; const CompetitionPrograms = [ {place: 0, defaultGame: true, name: "esim", version: "v1", func: async function esim(input, output) { // Tervehdys. output("esim v1"); const tervehdys = await input(); if (tervehdys == "ping" || tervehdys != "2019-kuurupiilo") { return; } // Pelin asetukset ja esteet. const asetusrivi = await input(); const asetukset = asetusrivi.split(" ").map(s => +s); const pelaajia = asetukset[0], numero = asetukset[1]; const esteet = [], pelaajat = []; for (let i = 3; i < asetukset.length; i += 3) { esteet.push({x: asetukset[i], y: asetukset[i+1], r: asetukset[i+2]}); } // Alustetaan pelaajien tiedot. for (let i = 0; i < pelaajia; ++i) { pelaajat.push({x: 0, y: 0, r: 32, kohdeX: 0, kohdeY: 0, nähtynä: 0, päivitetty: 0, etsijä: i < 3}); } const itse = pelaajat[numero - 1]; // Ajetaan peliä. for (let kierros = itse.etsijä ? 200 : 0;; ++kierros) { // Kierroksen syöte: näkyvien pelaajien sijainnit. const rivi = await input(); if (!rivi || rivi == "0") { break; } const luvut = rivi.split(" ").map(s => +s); for (let i = 1; i < luvut.length; i += 6) { const n = luvut[i] - 1; pelaajat[n].x = luvut[i+1]; pelaajat[n].y = luvut[i+2]; pelaajat[n].kohdeX = luvut[i+3]; pelaajat[n].kohdeY = luvut[i+4]; pelaajat[n].nähtynä = luvut[i+5]; pelaajat[n].päivitetty = kierros; } // Lasketaan ja ilmoitetaan oma siirto. // Esimerkki vaihtaa suuntaa aina, kun on saavuttanut kohteensa. if (itse.x == itse.kohdeX && itse.y == itse.kohdeY) { itse.kohdeX = Math.trunc(Math.cos(numero + (kierros % 357) * Math.PI / 180) * 1080); itse.kohdeY = Math.trunc(Math.sin(numero + (kierros % 357) * Math.PI / 180) * 1080); output(itse.kohdeX + " " + itse.kohdeY); } else { output("="); } // Pienennetään esteitä, jos on sen aika. if (kierros % 4800 == 4799) { for (let e of esteet) { e.r = Math.trunc(e.r * 3 / 4); if (e.r <= 8) { // Merkitään säde nollaksi, jos este poistuu. e.r = 0; } } } } }}, {place: 0, defaultGame: true, name: "dummy", version: "v1", func: async function dummy(input, output) { output("dummy v1"); const tervehdys = await input(); const asetusrivi = await input(); for (let kierros = 0;; ++kierros) { const rivi = await input(); if (!rivi || rivi == "0") { break; } output("="); } }} ]; </script>