From 8a86905f73234e210c6fd2b0f2884d8e3d235428 Mon Sep 17 00:00:00 2001 From: Lennard Sprong Date: Thu, 30 Apr 2026 08:52:45 +0200 Subject: [PATCH 1/2] Add The Longest --- src-ui/list.html | 1 + src/pzpr/variety.js | 1 + src/res/failcode.en.json | 3 + src/variety/longest.js | 374 +++++++++++++++++++++++++++++++++++++++ test/script/longest.js | 28 +++ 5 files changed, 407 insertions(+) create mode 100644 src/variety/longest.js create mode 100644 test/script/longest.js diff --git a/src-ui/list.html b/src-ui/list.html index a294602a9..ae4f7568a 100644 --- a/src-ui/list.html +++ b/src-ui/list.html @@ -324,6 +324,7 @@

パズルの種類のリスト
  • +
  • diff --git a/src/pzpr/variety.js b/src/pzpr/variety.js index e8c9a2c51..3addcffcf 100755 --- a/src/pzpr/variety.js +++ b/src/pzpr/variety.js @@ -278,6 +278,7 @@ lixloop: [0, 0, "LIX Loop", "LIX Loop", "yajilin"], lohkous: [0, 0, "Lohkous", "Lohkous"], lollipops: [0, 0, "ペロペロキャンディ", "Lollipops"], + longest: [0, 0, "短辺消失", "The Longest"], lookair: [0, 0, "るっくえあ", "Look-Air"], loopsp: [1, 0, "環状線スペシャル", "Loop Special", "pipelink"], loute: [0, 0, "エルート", "L-route"], diff --git a/src/res/failcode.en.json b/src/res/failcode.en.json index a2594aacc..21d522de5 100644 --- a/src/res/failcode.en.json +++ b/src/res/failcode.en.json @@ -70,9 +70,11 @@ "bdDeadEnd": "There is a dead-end line.", "bdDoorsGt.oneroom": "Two rooms have more than one door between them.", "bdEqual.antmill": "A square clue does not overlap two cells of the same color.", + "bdGivenUnused.longest": "A given line is not part of the longest border of an adjacent block.", "bdIgnoreBP.bdblock": "A point has no line.", "bdIgnoreBP.kramma": "A point has no line.", "bdNotChassis.shwolf": "A line doesn't connect to the chassis.", + "bdNotGiven.longest": "A region's longest border uses non-given lines.", "bdPassStar.nuriuzu": "A star overlaps a shaded cell.", "bdPassStar.tentaisho": "A line goes over a star.", "bdPierceGt1.blindrush": "A network of walls is pierced more than once.", @@ -195,6 +197,7 @@ "bkNotAllMUPassed.dotchi2": "A line doesn't pass all white pearls.", "bkNotAllMUPassed.moonsun": "A line doesn't pass all of the marks of the sun.", "bkNotClosed.balloon": "A balloon is not enclosed by borders.", + "bkNotClosed.longest": "A region is not enclosed by borders.", "bkNotHRect.tatamibari": "A tatami is not a horizontally long rectangle.", "bkNotLshape.loute": "An area is not L-shaped or its width is not 1.", "bkNotLshape3.shikaku": "An area whose size is a multiple of three is not L-shaped.", diff --git a/src/variety/longest.js b/src/variety/longest.js new file mode 100644 index 000000000..26d5b8720 --- /dev/null +++ b/src/variety/longest.js @@ -0,0 +1,374 @@ +(function(pidlist, classbase) { + if (typeof module === "object" && module.exports) { + module.exports = [pidlist, classbase]; + } else { + pzpr.classmgr.makeCustom(pidlist, classbase); + } +})(["longest"], { + //--------------------------------------------------------- + // マウス入力系 + MouseEvent: { + inputModes: { + edit: ["info-line"], + play: ["border", "subline"] + }, + mouseinput_auto: function() { + if (this.puzzle.playmode) { + if (this.mousestart || this.mousemove) { + if (this.btn === "left" && this.isBorderMode()) { + this.inputborder(); + } else { + this.inputQsubLine(); + } + } + } else if (this.puzzle.editmode) { + if (this.mousestart || this.mousemove) { + this.inputborder(); + } + } + } + }, + + Border: { + enableLineNG: true, + + isLineNG: function() { + return this.ques === 1; + }, + + rebuildRooms: function() { + var room1 = this.sidecell[0].room, + room2 = this.sidecell[1].room; + if (room1) { + this.board.roommgr.setExtraData(room1); + } + if (room2 && room1 !== room2) { + this.board.roommgr.setExtraData(room2); + } + }, + + posthook: { + ques: function() { + this.rebuildRooms(); + }, + qans: function() { + this.rebuildRooms(); + } + } + }, + Board: { + cols: 5, + rows: 5, + + hasborder: 2 + }, + AreaRoomGraph: { + enabled: true, + setExtraData: function(component) { + this.common.setExtraData.call(this, component); + component.closed = true; + + var rect = component.clist.getRectSize(); + + var long = 0; + component.longest = new Set(); + + var current = new this.klass.BorderList(); + + for (var dir = 0; dir <= 1; dir++) { + var mina = dir ? rect.x1 : rect.y1, + minb = dir ? rect.y1 : rect.x1, + maxa = dir ? rect.x2 : rect.y2, + maxb = dir ? rect.y2 : rect.x2; + + for (var a = mina - 1; a <= maxa + 1; a += 2) { + for (var b = minb; b <= maxb + 2; b += 2) { + var border = dir ? this.board.getb(a, b) : this.board.getb(b, a); + var relevant = + !border.isnull && + (border.sidecell[0].room === component || + border.sidecell[1].room === component); + + if (border.isBorder() && relevant) { + current.add(border); + } else if (current.length > 0) { + if (current.length > long) { + component.longest.clear(); + long = current.length; + } + if (current.length === long) { + current.each(function(c) { + component.longest.add(c); + }); + } + + current = new this.klass.BorderList(); + } + + if (relevant && component.closed) { + if (!border.isBorder() && !border.inside) { + component.closed = false; + } else if (border.isBorder() && border.inside) { + for (var i = 0; i < 2; i++) { + var cross = border.sidecross[i]; + if ( + cross.lcnt === 1 && + cross.bx !== this.board.minbx && + cross.by !== this.board.minby && + cross.bx !== this.board.maxbx && + cross.by !== this.board.maxby + ) { + component.closed = false; + } + } + } + } + } + } + } + } + }, + + Graphic: { + gridcolor_type: "LIGHT", + margin: 0.5, + errcolor1: "rgb(255, 80, 80)", + errcolor2: "rgb(192, 0, 0)", + + paint: function() { + this.drawBGCells(); + this.drawDashedGrid(false); + + this.addlw = 0; + this.drawQansBorders(); + this.addlw = 2; + this.drawQuesBorders(); + + this.drawLines(); + + this.drawBaseMarks(); + + this.drawBorderQsubs(); + }, + getBorderColor_ques: function(border) { + if (border.isLineNG()) { + return border.error === 1 ? this.errcolor2 : "black"; + } + + return null; + } + }, + + Encode: { + decodePzpr: function(type) { + this.decodeMejilink(); + }, + encodePzpr: function(type) { + this.encodeMejilink(); + }, + + decodeMejilink: function() { + var bstr = this.outbstr, + bd = this.board, + twi = [16, 8, 4, 2, 1]; + var pos = bstr + ? Math.min(((bd.border.length + 4) / 5) | 0, bstr.length) + : 0, + id = 0; + for (var i = 0; i < pos; i++) { + var ca = parseInt(bstr.charAt(i), 32); + for (var w = 0; w < 5; w++) { + if (!!bd.border[id]) { + bd.border[id].ques = ca & twi[w] ? 1 : 0; + id++; + } + } + } + this.outbstr = bstr.substr(pos); + }, + encodeMejilink: function() { + var count = 0, + bd = this.board; + var bdinside = 2 * bd.cols * bd.rows - bd.cols - bd.rows; + for (var id = bdinside; id < bd.border.length; id++) { + if (bd.border[id].isLineNG()) { + count++; + } + } + var num = 0, + pass = 0, + cm = "", + twi = [16, 8, 4, 2, 1]; + for ( + var id = 0, max = count === 0 ? bdinside : bd.border.length; + id < max; + id++ + ) { + if (bd.border[id].isLineNG()) { + pass += twi[num]; + } + num++; + if (num === 5) { + cm += pass.toString(32); + num = 0; + pass = 0; + } + } + if (num > 0) { + cm += pass.toString(32); + } + this.outbstr += cm; + } + }, + //--------------------------------------------------------- + FileIO: { + decodeData: function() { + this.decodeBorder(function(border, ca) { + if (ca === "2") { + border.ques = 0; + border.qans = 1; + } else if (ca === "-1") { + border.ques = 0; + border.qsub = 1; + } else if (ca === "1") { + border.ques = 1; + } else { + border.ques = 0; + } + }); + }, + encodeData: function() { + this.encodeBorder(function(border) { + if (border.qans === 1) { + return "2 "; + } else if (border.ques === 1) { + return "1 "; + } else if (border.qsub === 1) { + return "-1 "; + } else { + return "0 "; + } + }); + } + }, + + AnsCheck: { + checklist: [ + "checkLongestIsAnswer", + "checkLongestUnused+", + "checkOutsideBorder+", + "checkBorderDeadend" + ], + + checkLongestIsAnswer: function() { + var result = true; + var rooms = this.board.roommgr.components; + for (var r = 0; r < rooms.length; r++) { + var room = rooms[r]; + var current = true; + if (!room.closed) { + continue; + } + + var longest = Array.from(room.longest); + for (var i = 0; i < longest.length; i++) { + if (longest[i].qans === 1) { + current = false; + longest[i].seterr(1); + } + if (!current && this.checkOnly) { + break; + } + } + + if (!current) { + result = false; + if (this.checkOnly) { + break; + } + room.clist.seterr(1); + } + } + if (!result) { + this.failcode.add("bdNotGiven"); + this.board.border.setnoerr(); + } + }, + + checkLongestUnused: function() { + var result = true, + borders = this.board.border; + for (var id = 0; id < borders.length; id++) { + var border = borders[id]; + if (!border.ques) { + continue; + } + var room1 = border.sidecell[0].room, + room2 = border.sidecell[1].room; + if (room1 && (!room1.closed || room1.longest.has(border))) { + continue; + } + if ( + room2 && + room1 !== room2 && + (!room2.closed || room2.longest.has(border)) + ) { + continue; + } + + result = false; + if (this.checkOnly) { + break; + } + border.seterr(1); + } + if (!result) { + this.failcode.add("bdGivenUnused"); + borders.setnoerr(); + } + }, + + checkOutsideBorder: function() { + var borders = this.board.border; + for (var id = 0; id < borders.length; id++) { + var border = borders[id]; + if (border.inside || border.isBorder()) { + continue; + } + + this.failcode.add("bkNotClosed"); + if (this.checkOnly) { + break; + } + new this.klass.CellList(border.sidecell).seterr(1); + } + }, + + checkBorderDeadend: function() { + var result = true, + bd = this.board; + var crosses = bd.crossinside( + bd.minbx + 2, + bd.minby + 2, + bd.maxbx - 2, + bd.maxby - 2 + ); + for (var c = 0; c < crosses.length; c++) { + var cross = crosses[c]; + if (cross.lcnt !== 1) { + continue; + } + + result = false; + if (this.checkOnly) { + break; + } + cross.setCrossBorderError(); + } + if (!result) { + this.failcode.add("bdDeadEnd"); + bd.border.setnoerr(); + } + } + } +}); diff --git a/test/script/longest.js b/test/script/longest.js new file mode 100644 index 000000000..42beb045c --- /dev/null +++ b/test/script/longest.js @@ -0,0 +1,28 @@ +/* longest.js */ + +ui.debug.addDebugData("longest", { + url: "4/4/94001hrn", + failcheck: [ + [ + "bdNotGiven", + "pzprv3/longest/4/4/2 0 1 2 2 /1 2 1 2 1 /1 0 1 2 1 /1 2 0 0 1 /1 1 2 2 /2 0 0 2 /0 2 0 0 /0 2 2 0 /2 1 1 1 /" + ], + [ + "bkNotClosed", + "pzprv3/longest/4/4/0 0 1 0 0 /1 2 1 0 1 /1 0 1 0 1 /1 2 0 0 1 /1 1 0 0 /2 0 0 0 /0 2 0 0 /0 2 0 0 /2 1 1 1 /" + ], + [ + "bdGivenUnused", + "pzprv3/longest/4/4/2 0 1 0 2 /1 2 1 0 1 /1 0 1 2 1 /1 2 0 0 1 /1 1 2 2 /2 0 0 0 /0 2 0 2 /0 2 2 0 /2 1 1 1 /" + ], + [ + "bdDeadEnd", + "pzprv3/longest/4/4/2 0 1 0 2 /1 0 1 0 1 /1 0 1 0 1 /1 0 0 0 1 /1 1 2 2 /0 0 0 0 /0 0 0 0 /0 0 0 0 /2 1 1 1 /" + ], + [ + null, + "pzprv3/longest/4/4/2 0 1 0 2 /1 2 1 2 1 /1 0 1 2 1 /1 2 0 0 1 /1 1 2 2 /2 0 0 2 /0 2 0 0 /0 2 2 0 /2 1 1 1 /" + ] + ], + inputs: [] +}); From a4de285a031e468b3ce5233a4806cbabfd1a313a Mon Sep 17 00:00:00 2001 From: Lennard Sprong Date: Mon, 11 May 2026 09:26:45 +0200 Subject: [PATCH 2/2] Factor out decodeOuterBorder --- src/variety-common/Encode.js | 53 ++++++++++++++++++++++++++++++++ src/variety/cbblock.js | 46 ++-------------------------- src/variety/longest.js | 59 ++---------------------------------- src/variety/mejilink.js | 57 ++-------------------------------- 4 files changed, 60 insertions(+), 155 deletions(-) diff --git a/src/variety-common/Encode.js b/src/variety-common/Encode.js index 0f725e333..b15a51fcb 100644 --- a/src/variety-common/Encode.js +++ b/src/variety-common/Encode.js @@ -671,6 +671,59 @@ pzpr.classmgr.makeCommon({ this.outbstr += cm; }, + decodeOuterBorder: function() { + var bstr = this.outbstr, + bd = this.board, + twi = [16, 8, 4, 2, 1]; + var pos = bstr + ? Math.min(((bd.border.length + 4) / 5) | 0, bstr.length) + : 0, + id = 0; + for (var i = 0; i < pos; i++) { + var ca = parseInt(bstr.charAt(i), 32); + for (var w = 0; w < 5; w++) { + if (!!bd.border[id]) { + bd.border[id].ques = ca & twi[w] ? 1 : 0; + id++; + } + } + } + this.outbstr = bstr.substr(pos); + }, + encodeOuterBorder: function() { + var hasoutside = 0, + bd = this.board; + var bdinside = 2 * bd.cols * bd.rows - bd.cols - bd.rows; + for (var id = bdinside; id < bd.border.length && !hasoutside; id++) { + if (bd.border[id].isLineNG()) { + hasoutside = true; + } + } + var num = 0, + pass = 0, + cm = "", + twi = [16, 8, 4, 2, 1]; + for ( + var id = 0, max = !hasoutside ? bdinside : bd.border.length; + id < max; + id++ + ) { + if (bd.border[id].isLineNG()) { + pass += twi[num]; + } + num++; + if (num === 5) { + cm += pass.toString(32); + num = 0; + pass = 0; + } + } + if (num > 0) { + cm += pass.toString(32); + } + this.outbstr += cm; + }, + //--------------------------------------------------------------------------- // enc.decodeCrossMark() 黒点をデコードする // enc.encodeCrossMark() 黒点をエンコードする diff --git a/src/variety/cbblock.js b/src/variety/cbblock.js index e61887e75..86d7d86ea 100644 --- a/src/variety/cbblock.js +++ b/src/variety/cbblock.js @@ -463,52 +463,10 @@ // URLエンコード/デコード処理 "Encode@cbblock": { decodePzpr: function(type) { - this.decodeCBBlock(); + this.decodeOuterBorder(); }, encodePzpr: function(type) { - this.encodeCBBlock(); - }, - - decodeCBBlock: function() { - var bstr = this.outbstr, - bd = this.board, - twi = [16, 8, 4, 2, 1]; - var pos = bstr - ? Math.min(((bd.border.length + 4) / 5) | 0, bstr.length) - : 0, - id = 0; - for (var i = 0; i < pos; i++) { - var ca = parseInt(bstr.charAt(i), 32); - for (var w = 0; w < 5; w++) { - if (!!bd.border[id]) { - bd.border[id].ques = ca & twi[w] ? 1 : 0; - id++; - } - } - } - this.outbstr = bstr.substr(pos); - }, - encodeCBBlock: function() { - var num = 0, - pass = 0, - cm = "", - bd = this.board, - twi = [16, 8, 4, 2, 1]; - for (var id = 0, max = bd.border.length; id < max; id++) { - if (bd.border[id].isGround()) { - pass += twi[num]; - } - num++; - if (num === 5) { - cm += pass.toString(32); - num = 0; - pass = 0; - } - } - if (num > 0) { - cm += pass.toString(32); - } - this.outbstr += cm; + this.encodeOuterBorder(); } }, diff --git a/src/variety/longest.js b/src/variety/longest.js index 26d5b8720..5bf6f6e1e 100644 --- a/src/variety/longest.js +++ b/src/variety/longest.js @@ -9,7 +9,7 @@ // マウス入力系 MouseEvent: { inputModes: { - edit: ["info-line"], + edit: [], play: ["border", "subline"] }, mouseinput_auto: function() { @@ -161,63 +161,10 @@ Encode: { decodePzpr: function(type) { - this.decodeMejilink(); + this.decodeOuterBorder(); }, encodePzpr: function(type) { - this.encodeMejilink(); - }, - - decodeMejilink: function() { - var bstr = this.outbstr, - bd = this.board, - twi = [16, 8, 4, 2, 1]; - var pos = bstr - ? Math.min(((bd.border.length + 4) / 5) | 0, bstr.length) - : 0, - id = 0; - for (var i = 0; i < pos; i++) { - var ca = parseInt(bstr.charAt(i), 32); - for (var w = 0; w < 5; w++) { - if (!!bd.border[id]) { - bd.border[id].ques = ca & twi[w] ? 1 : 0; - id++; - } - } - } - this.outbstr = bstr.substr(pos); - }, - encodeMejilink: function() { - var count = 0, - bd = this.board; - var bdinside = 2 * bd.cols * bd.rows - bd.cols - bd.rows; - for (var id = bdinside; id < bd.border.length; id++) { - if (bd.border[id].isLineNG()) { - count++; - } - } - var num = 0, - pass = 0, - cm = "", - twi = [16, 8, 4, 2, 1]; - for ( - var id = 0, max = count === 0 ? bdinside : bd.border.length; - id < max; - id++ - ) { - if (bd.border[id].isLineNG()) { - pass += twi[num]; - } - num++; - if (num === 5) { - cm += pass.toString(32); - num = 0; - pass = 0; - } - } - if (num > 0) { - cm += pass.toString(32); - } - this.outbstr += cm; + this.encodeOuterBorder(); } }, //--------------------------------------------------------- diff --git a/src/variety/mejilink.js b/src/variety/mejilink.js index 77c7c177c..c59d3d57d 100644 --- a/src/variety/mejilink.js +++ b/src/variety/mejilink.js @@ -151,63 +151,10 @@ // URLエンコード/デコード処理 Encode: { decodePzpr: function(type) { - this.decodeMejilink(); + this.decodeOuterBorder(); }, encodePzpr: function(type) { - this.encodeMejilink(); - }, - - decodeMejilink: function() { - var bstr = this.outbstr, - bd = this.board, - twi = [16, 8, 4, 2, 1]; - var pos = bstr - ? Math.min(((bd.border.length + 4) / 5) | 0, bstr.length) - : 0, - id = 0; - for (var i = 0; i < pos; i++) { - var ca = parseInt(bstr.charAt(i), 32); - for (var w = 0; w < 5; w++) { - if (!!bd.border[id]) { - bd.border[id].ques = ca & twi[w] ? 1 : 0; - id++; - } - } - } - this.outbstr = bstr.substr(pos); - }, - encodeMejilink: function() { - var count = 0, - bd = this.board; - var bdinside = 2 * bd.cols * bd.rows - bd.cols - bd.rows; - for (var id = bdinside; id < bd.border.length; id++) { - if (bd.border[id].isGround()) { - count++; - } - } - var num = 0, - pass = 0, - cm = "", - twi = [16, 8, 4, 2, 1]; - for ( - var id = 0, max = count === 0 ? bdinside : bd.border.length; - id < max; - id++ - ) { - if (bd.border[id].isGround()) { - pass += twi[num]; - } - num++; - if (num === 5) { - cm += pass.toString(32); - num = 0; - pass = 0; - } - } - if (num > 0) { - cm += pass.toString(32); - } - this.outbstr += cm; + this.encodeOuterBorder(); } }, //---------------------------------------------------------