diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/cyberspace/deck0003Sno/SnoAvatarWorkGoldenVectorsTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/cyberspace/deck0003Sno/SnoAvatarWorkGoldenVectorsTest.kt new file mode 100644 index 0000000000..3fbd5f7678 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/cyberspace/deck0003Sno/SnoAvatarWorkGoldenVectorsTest.kt @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.cyberspace.deck0003Sno + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +/** + * The avatar-work golden vectors from the reference implementation. + * + * `CYBERSPACE_V2.md` §8.10 says its two reference implementations, `avatar.ts` + * in cyberspace-core and `cyberspace_core/avatar.py` in cyberspace-cli, are + * "pinned to one set of golden vectors". This is that set, read out of + * `tests/fixtures/avatar_work.json` in cyberspace-cli (MIT), so our ladder is + * checked against the implementation the spec points at and not only against + * the pseudocode we both read. + * + * Two of the fixtures need a word: + * - **"half gibson, ticks"** is the case a careless reader gets wrong. Its + * vertex is `[-1, -1, -1]` with ticks `[60, 60, 60]`, which is -0.5 of a unit, + * not -1.5: the whole part is the floor and the ticks count up from it, so the + * ticks must be added to the signed whole before the magnitude is taken. + * - **"detailed but small"** indexes faces past the end of its vertex list, + * because the work formula only counts faces and never looks at them. §1.9 + * would refuse that payload, so this keeps the face count and substitutes + * indices the parser accepts, which is the same input as far as the formula + * is concerned. + */ +class SnoAvatarWorkGoldenVectorsTest { + private data class Vector( + val name: String, + val reach: Double, + val required: Int, + val substitutedFaces: Boolean, + val json: String, + ) + + private val vectors = + listOf( + Vector( + name = "one gibson, plain", + reach = 1.0, + required = 16, + substitutedFaces = false, + json = + "{\"v\":2,\"name\":\"one gibson, plain\",\"unit\":0,\"mode\":\"solid\",\"vertices\":[[0,0,0],[1,0,0],[0,1,0],[0,0,1]],\"colors\":[225,225,225" + + ",225],\"faces\":[[0,1,2],[0,2,3]]}", + ), + Vector( + name = "half gibson, ticks", + reach = 0.5, + required = 16, + substitutedFaces = false, + json = + "{\"v\":2,\"name\":\"half gibson, ticks\",\"unit\":0,\"mode\":\"points\",\"vertices\":[[0,0,0],[-1,-1,-1]],\"colors\":[225,225],\"faces\":[]," + + "\"ticks\":[[60,60,60],[60,60,60]]}", + ), + Vector( + name = "two gibsons", + reach = 2.0, + required = 18, + substitutedFaces = false, + json = + "{\"v\":2,\"name\":\"two gibsons\",\"unit\":0,\"mode\":\"solid\",\"vertices\":[[-2,-2,-2],[-2,-2,2],[-2,2,-2],[-2,2,2],[2,-2,-2],[2,-2,2],[2," + + "2,-2],[2,2,2]],\"colors\":[225,225,225,225,225,225,225,225],\"faces\":[[0,1,2],[1,3,2],[4,6,5],[5,6,7],[0,4,1],[1,4,5],[2,3,6],[3,7,6],[0,2," + + "4],[2,6,4],[1,5,3],[3,5,7]]}", + ), + Vector( + name = "four gibsons", + reach = 4.0, + required = 20, + substitutedFaces = false, + json = + "{\"v\":2,\"name\":\"four gibsons\",\"unit\":0,\"mode\":\"solid\",\"vertices\":[[-4,-4,-4],[-4,-4,4],[-4,4,-4],[-4,4,4],[4,-4,-4],[4,-4,4],[4" + + ",4,-4],[4,4,4]],\"colors\":[225,225,225,225,225,225,225,225],\"faces\":[[0,1,2],[1,3,2],[4,6,5],[5,6,7],[0,4,1],[1,4,5],[2,3,6],[3,7,6],[0,2" + + ",4],[2,6,4],[1,5,3],[3,5,7]]}", + ), + Vector( + name = "sixteen gibsons via unit", + reach = 16.0, + required = 24, + substitutedFaces = false, + json = + "{\"v\":2,\"name\":\"sixteen gibsons via unit\",\"unit\":2,\"mode\":\"solid\",\"vertices\":[[-4,-4,-4],[-4,-4,4],[-4,4,-4],[-4,4,4],[4,-4,-4]" + + ",[4,-4,4],[4,4,-4],[4,4,4]],\"colors\":[225,225,225,225,225,225,225,225],\"faces\":[[0,1,2],[1,3,2],[4,6,5],[5,6,7],[0,4,1],[1,4,5],[2,3,6]," + + "[3,7,6],[0,2,4],[2,6,4],[1,5,3],[3,5,7]]}", + ), + Vector( + name = "a thousand and twenty-four gibsons", + reach = 1024.0, + required = 36, + substitutedFaces = false, + json = + "{\"v\":2,\"name\":\"a thousand and twenty-four gibsons\",\"unit\":10,\"mode\":\"solid\",\"vertices\":[[-1,-1,-1],[-1,-1,1],[-1,1,-1],[-1,1,1" + + "],[1,-1,-1],[1,-1,1],[1,1,-1],[1,1,1]],\"colors\":[225,225,225,225,225,225,225,225],\"faces\":[[0,1,2],[1,3,2],[4,6,5],[5,6,7],[0,4,1],[1,4," + + "5],[2,3,6],[3,7,6],[0,2,4],[2,6,4],[1,5,3],[3,5,7]]}", + ), + Vector( + name = "detailed but small", + reach = 1.0, + required = 28, + substitutedFaces = true, + json = + "{\"v\":2,\"name\":\"detailed but small\",\"unit\":0,\"mode\":\"solid\",\"vertices\":[[-1,-1,0],[0,-1,0],[1,-1,0],[-1,0,0],[0,0,0],[1,0,0],[-" + + "1,1,0],[0,1,0],[1,1,0],[-1,-1,1],[0,-1,1],[1,-1,1],[-1,0,1],[0,0,1],[1,0,1],[-1,1,1],[0,1,1],[1,1,1],[-1,-1,0],[0,-1,0],[1,-1,0],[-1,0,0],[0" + + ",0,0],[1,0,0],[-1,1,0],[0,1,0],[1,1,0],[-1,-1,1],[0,-1,1],[1,-1,1],[-1,0,1],[0,0,1],[1,0,1],[-1,1,1],[0,1,1],[1,1,1],[-1,-1,0],[0,-1,0],[1,-" + + "1,0],[-1,0,0],[0,0,0],[1,0,0],[-1,1,0],[0,1,0],[1,1,0],[-1,-1,1],[0,-1,1],[1,-1,1],[-1,0,1],[0,0,1],[1,0,1],[-1,1,1],[0,1,1],[1,1,1],[-1,-1," + + "0],[0,-1,0],[1,-1,0],[-1,0,0],[0,0,0],[1,0,0],[-1,1,0],[0,1,0],[1,1,0],[-1,-1,1],[0,-1,1],[1,-1,1],[-1,0,1],[0,0,1],[1,0,1],[-1,1,1],[0,1,1]" + + ",[1,1,1],[-1,-1,0],[0,-1,0],[1,-1,0],[-1,0,0],[0,0,0],[1,0,0],[-1,1,0],[0,1,0],[1,1,0],[-1,-1,1],[0,-1,1],[1,-1,1],[-1,0,1],[0,0,1],[1,0,1]," + + "[-1,1,1],[0,1,1],[1,1,1],[-1,-1,0],[0,-1,0],[1,-1,0],[-1,0,0],[0,0,0],[1,0,0],[-1,1,0],[0,1,0],[1,1,0],[-1,-1,1],[0,-1,1],[1,-1,1],[-1,0,1]," + + "[0,0,1],[1,0,1],[-1,1,1],[0,1,1],[1,1,1],[-1,-1,0],[0,-1,0],[1,-1,0],[-1,0,0],[0,0,0],[1,0,0],[-1,1,0],[0,1,0],[1,1,0],[-1,-1,1],[0,-1,1],[1" + + ",-1,1],[-1,0,1],[0,0,1],[1,0,1],[-1,1,1],[0,1,1],[1,1,1],[-1,-1,0],[0,-1,0],[1,-1,0],[-1,0,0],[0,0,0],[1,0,0],[-1,1,0],[0,1,0],[1,1,0],[-1,-" + + "1,1],[0,-1,1],[1,-1,1],[-1,0,1],[0,0,1],[1,0,1],[-1,1,1],[0,1,1],[1,1,1],[-1,-1,0],[0,-1,0],[1,-1,0],[-1,0,0],[0,0,0],[1,0,0],[-1,1,0],[0,1," + + "0],[1,1,0],[-1,-1,1],[0,-1,1],[1,-1,1],[-1,0,1],[0,0,1],[1,0,1],[-1,1,1],[0,1,1],[1,1,1],[-1,-1,0],[0,-1,0],[1,-1,0],[-1,0,0],[0,0,0],[1,0,0" + + "],[-1,1,0],[0,1,0],[1,1,0],[-1,-1,1],[0,-1,1],[1,-1,1],[-1,0,1],[0,0,1],[1,0,1],[-1,1,1],[0,1,1],[1,1,1],[-1,-1,0],[0,-1,0],[1,-1,0],[-1,0,0" + + "],[0,0,0],[1,0,0],[-1,1,0],[0,1,0],[1,1,0],[-1,-1,1],[0,-1,1],[1,-1,1],[-1,0,1],[0,0,1],[1,0,1],[-1,1,1],[0,1,1],[1,1,1],[-1,-1,0],[0,-1,0]]" + + ",\"colors\":[225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225" + + ",225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225" + + ",225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225" + + ",225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225" + + ",225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225" + + ",225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225,225],\"faces\":[[0,1,2],[1,2,3]" + + ",[2,3,4],[3,4,5],[4,5,6],[5,6,7],[6,7,8],[7,8,9],[8,9,10],[9,10,11],[10,11,12],[11,12,13],[12,13,14],[13,14,15],[14,15,16],[15,16,17],[16,17" + + ",18],[17,18,19],[18,19,20],[19,20,21],[20,21,22],[21,22,23],[22,23,24],[23,24,25],[24,25,26],[25,26,27],[26,27,28],[27,28,29],[28,29,30],[29" + + ",30,31],[30,31,32],[31,32,33],[32,33,34],[33,34,35],[34,35,36],[35,36,37],[36,37,38],[37,38,39],[38,39,40],[39,40,41],[40,41,42],[41,42,43]," + + "[42,43,44],[43,44,45],[44,45,46],[45,46,47],[46,47,48],[47,48,49],[48,49,50],[49,50,51],[50,51,52],[51,52,53],[52,53,54],[53,54,55],[54,55,5" + + "6],[55,56,57],[56,57,58],[57,58,59],[58,59,60],[59,60,61],[60,61,62],[61,62,63],[62,63,64],[63,64,65],[64,65,66],[65,66,67],[66,67,68],[67,6" + + "8,69],[68,69,70],[69,70,71],[70,71,72],[71,72,73],[72,73,74],[73,74,75],[74,75,76],[75,76,77],[76,77,78],[77,78,79],[78,79,80],[79,80,81],[8" + + "0,81,82],[81,82,83],[82,83,84],[83,84,85],[84,85,86],[85,86,87],[86,87,88],[87,88,89],[88,89,90],[89,90,91],[90,91,92],[91,92,93],[92,93,94]" + + ",[93,94,95],[94,95,96],[95,96,97],[96,97,98],[97,98,99],[98,99,100],[99,100,101],[100,101,102],[101,102,103],[102,103,104],[103,104,105],[10" + + "4,105,106],[105,106,107],[106,107,108],[107,108,109],[108,109,110],[109,110,111],[110,111,112],[111,112,113],[112,113,114],[113,114,115],[11" + + "4,115,116],[115,116,117],[116,117,118],[117,118,119],[118,119,120],[119,120,121],[120,121,122],[121,122,123],[122,123,124],[123,124,125],[12" + + "4,125,126],[125,126,127],[126,127,128],[127,128,129],[128,129,130],[129,130,131],[130,131,132],[131,132,133],[132,133,134],[133,134,135],[13" + + "4,135,136],[135,136,137],[136,137,138],[137,138,139],[138,139,140],[139,140,141],[140,141,142],[141,142,143],[142,143,144],[143,144,145],[14" + + "4,145,146],[145,146,147],[146,147,148],[147,148,149],[148,149,150],[149,150,151],[150,151,152],[151,152,153],[152,153,154],[153,154,155],[15" + + "4,155,156],[155,156,157],[156,157,158],[157,158,159],[158,159,160],[159,160,161],[160,161,162],[161,162,163],[162,163,164],[163,164,165],[16" + + "4,165,166],[165,166,167],[166,167,168],[167,168,169],[168,169,170],[169,170,171],[170,171,172],[171,172,173],[172,173,174],[173,174,175],[17" + + "4,175,176],[175,176,177],[176,177,178],[177,178,179],[178,179,180],[179,180,181],[180,181,182],[181,182,183],[182,183,184],[183,184,185],[18" + + "4,185,186],[185,186,187],[186,187,188],[187,188,189],[188,189,190],[189,190,191],[190,191,192],[191,192,193],[192,193,194],[193,194,195],[19" + + "4,195,196],[195,196,197],[196,197,198],[197,198,199],[198,199,0],[199,0,1],[0,1,2],[1,2,3],[2,3,4],[3,4,5],[4,5,6],[5,6,7],[6,7,8],[7,8,9],[" + + "8,9,10],[9,10,11],[10,11,12],[11,12,13],[12,13,14],[13,14,15],[14,15,16],[15,16,17],[16,17,18],[17,18,19],[18,19,20],[19,20,21],[20,21,22],[" + + "21,22,23],[22,23,24],[23,24,25],[24,25,26],[25,26,27],[26,27,28],[27,28,29],[28,29,30],[29,30,31],[30,31,32],[31,32,33],[32,33,34],[33,34,35" + + "],[34,35,36],[35,36,37],[36,37,38],[37,38,39],[38,39,40],[39,40,41],[40,41,42],[41,42,43],[42,43,44],[43,44,45],[44,45,46],[45,46,47],[46,47" + + ",48],[47,48,49],[48,49,50],[49,50,51],[50,51,52],[51,52,53],[52,53,54],[53,54,55],[54,55,56],[55,56,57],[56,57,58],[57,58,59],[58,59,60],[59" + + ",60,61],[60,61,62],[61,62,63],[62,63,64],[63,64,65],[64,65,66],[65,66,67],[66,67,68],[67,68,69],[68,69,70],[69,70,71],[70,71,72],[71,72,73]," + + "[72,73,74],[73,74,75],[74,75,76],[75,76,77],[76,77,78],[77,78,79],[78,79,80],[79,80,81],[80,81,82],[81,82,83],[82,83,84],[83,84,85],[84,85,8" + + "6],[85,86,87],[86,87,88],[87,88,89],[88,89,90],[89,90,91],[90,91,92],[91,92,93],[92,93,94],[93,94,95],[94,95,96],[95,96,97],[96,97,98],[97,9" + + "8,99],[98,99,100],[99,100,101],[100,101,102],[101,102,103],[102,103,104],[103,104,105],[104,105,106],[105,106,107],[106,107,108],[107,108,10" + + "9],[108,109,110],[109,110,111],[110,111,112],[111,112,113]]}", + ), + Vector( + name = "packed zero ticks", + reach = 3.0, + required = 20, + substitutedFaces = false, + json = + "{\"v\":2,\"name\":\"packed zero ticks\",\"unit\":0,\"mode\":\"points\",\"vertices\":[[3,0,0],[0,0,0]],\"colors\":[225,225],\"faces\":[],\"ti" + + "cks\":[-2]}", + ), + ) + + @Test + fun ourLadderMatchesTheReferenceImplementation() { + vectors.forEach { vector -> + val payload = SnoParser.parse(vector.json).payloadOrNull() + assertNotNull(payload, "${vector.name} did not parse") + assertEquals(vector.required, SnoAvatarWork.required(payload), "${vector.name} (reach ${vector.reach})") + } + } + + @Test + fun theWholeSetIsCovered() { + assertEquals(8, vectors.size) + } +}