Resolve block names to the default state, not the first one
blocks.json lists a block's states in getPossibleStates() order -- the property cartesian product -- and separately marks which one is the default. The parser declared only id and properties, so the default flag was dropped on the floor and nameToStateID returned states[0]. Those differ for 642 of the 1168 blocks. What that produced, measured rather than guessed: every redstone vein in the world was lit=true and glowing, every sunflower was placed as its own top half with nothing under it, and oak stairs came out upside down and waterlogged. It also quietly disagreed with the StateGrass, StateOakLog and StateOakLeaf constants next door in encode.go, which are the real defaults. Now it starts from the default state and applies the properties it recognises, keeping the default's value for an unknown key or an illegal value -- which is what vanilla does when it reads a palette entry. That matters on the disk path: a chunk written with a property we no longer know used to decode to a random corner state instead of something sane. The signature grows an ok result, because air was doing double duty as both a real block and "no such name". Separately, blockPaletteEntry filled the Properties compound by ranging a Go map. nbt.Compound preserves insertion order precisely so encoding is deterministic, so saving one chunk twice produced different region-file bytes for every multi-property block. Keys are sorted now.
This commit is contained in:
parent
9e91425c5d
commit
113a59e365
14 changed files with 215 additions and 62 deletions
|
|
@ -38,8 +38,8 @@ var oreSpecs = []oreSpec{
|
|||
// are untouched.
|
||||
func placeOres(c *Chunk, r *chunkRand) {
|
||||
for _, spec := range oreSpecs {
|
||||
ore := nameToStateID(spec.name, nil)
|
||||
if ore == StateAir {
|
||||
ore, ok := nameToStateID(spec.name, nil)
|
||||
if !ok {
|
||||
continue // unknown block name; skip defensively
|
||||
}
|
||||
for a := 0; a < spec.attempts; a++ {
|
||||
|
|
@ -117,8 +117,7 @@ func placeFlora(c *Chunk, r *chunkRand, surfTop *[16][16]int, grass *[16][16]boo
|
|||
if c.GetBlock(lx, y, lz) != StateAir {
|
||||
continue
|
||||
}
|
||||
flower := nameToStateID(flowers[int(r.next())%len(flowers)], nil)
|
||||
if flower != StateAir {
|
||||
if flower, ok := nameToStateID(flowers[int(r.next())%len(flowers)], nil); ok {
|
||||
c.SetBlock(lx, y, lz, flower)
|
||||
}
|
||||
}
|
||||
|
|
@ -155,8 +154,8 @@ func placeDesertFeatures(c *Chunk, r *chunkRand, surfTop *[16][16]int, biomeName
|
|||
|
||||
// placeCactus writes a 1-3 tall cactus column on top of the surface.
|
||||
func placeCactus(c *Chunk, lx, baseY, lz int, r *chunkRand) {
|
||||
cactus := nameToStateID("minecraft:cactus", nil)
|
||||
if cactus == StateAir {
|
||||
cactus, ok := nameToStateID("minecraft:cactus", nil)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h := 1 + int(r.next()%3)
|
||||
|
|
@ -167,8 +166,8 @@ func placeCactus(c *Chunk, lx, baseY, lz int, r *chunkRand) {
|
|||
|
||||
// placeDeadBush writes a single dead_bush on the surface.
|
||||
func placeDeadBush(c *Chunk, lx, baseY, lz int) {
|
||||
db := nameToStateID("minecraft:dead_bush", nil)
|
||||
if db == StateAir {
|
||||
db, ok := nameToStateID("minecraft:dead_bush", nil)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.SetBlock(lx, baseY, lz, db)
|
||||
|
|
@ -200,8 +199,8 @@ func placeRocks(c *Chunk, r *chunkRand, surfTop *[16][16]int, grass *[16][16]boo
|
|||
// placeBoulder writes a small 2-3 block cluster of stone-family blocks.
|
||||
func placeBoulder(c *Chunk, lx, baseY, lz int, r *chunkRand) {
|
||||
rocks := []string{"minecraft:cobblestone", "minecraft:granite", "minecraft:diorite", "minecraft:andesite"}
|
||||
block := nameToStateID(rocks[int(r.next())%len(rocks)], nil)
|
||||
if block == StateAir {
|
||||
block, ok := nameToStateID(rocks[int(r.next())%len(rocks)], nil)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
n := 2 + int(r.next()%2)
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ func TestHeightmapsDiffer(t *testing.T) {
|
|||
c := NewChunk(0, 0, BiomePlains)
|
||||
const floor = 64
|
||||
|
||||
dandelion := nameToStateID("minecraft:dandelion", nil)
|
||||
if dandelion == StateAir {
|
||||
dandelion, ok := nameToStateID("minecraft:dandelion", nil)
|
||||
if !ok {
|
||||
t.Fatal("dandelion is missing from the block table")
|
||||
}
|
||||
for lx := 0; lx < 16; lx++ {
|
||||
|
|
@ -96,8 +96,8 @@ func TestBlockStatePredicates(t *testing.T) {
|
|||
}
|
||||
}
|
||||
// A flower is the case that separates WORLD_SURFACE from MOTION_BLOCKING.
|
||||
dandelion := nameToStateID("minecraft:dandelion", nil)
|
||||
if dandelion == StateAir {
|
||||
dandelion, ok := nameToStateID("minecraft:dandelion", nil)
|
||||
if !ok {
|
||||
t.Fatal("dandelion is missing from the block table")
|
||||
}
|
||||
if blocksMotionOrFluid(dandelion) {
|
||||
|
|
@ -113,11 +113,9 @@ func TestSectionFluidCount(t *testing.T) {
|
|||
const y = 20
|
||||
// One section: stone floor, water above it, and one waterlogged block —
|
||||
// which counts as fluid even though it is not a fluid block.
|
||||
stairs := nameToStateID("minecraft:oak_stairs", map[string]string{
|
||||
"facing": "north", "half": "bottom", "shape": "straight", "waterlogged": "true",
|
||||
})
|
||||
if stairs == StateAir {
|
||||
t.Fatal("waterlogged oak stairs are missing from the block table")
|
||||
stairs, ok := nameToStateID("minecraft:oak_stairs", map[string]string{"waterlogged": "true"})
|
||||
if !ok {
|
||||
t.Fatal("oak stairs are missing from the block table")
|
||||
}
|
||||
if stateFlags(stairs)&flagFluid == 0 {
|
||||
t.Fatal("waterlogged stairs do not carry the fluid flag; the dump is wrong")
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func TestIncrementalBlockLightAgainstVanillaFixture(t *testing.T) {
|
|||
cache.chunkAt(cx, cz)
|
||||
}
|
||||
}
|
||||
glowstone := nameToStateID("minecraft:glowstone", nil)
|
||||
glowstone, _ := nameToStateID("minecraft:glowstone", nil)
|
||||
if valid, _ := cache.SetBlockWithLight(15, 100, 8, glowstone); !valid {
|
||||
t.Fatal("glowstone edit rejected")
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ func TestIncrementalBlockLightCrossesChunkBoundaryAndClears(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
glowstone := nameToStateID("minecraft:glowstone", nil)
|
||||
glowstone, _ := nameToStateID("minecraft:glowstone", nil)
|
||||
if emission := lightEmission(glowstone); emission != 15 {
|
||||
t.Fatalf("glowstone emission = %d, want 15", emission)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func TestSafeSpawnYRejectsUnderwaterColumn(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSafeSpawnYAcceptsNonOpaqueSolidFloor(t *testing.T) {
|
||||
stairs := nameToStateID("minecraft:oak_stairs", nil)
|
||||
stairs, _ := nameToStateID("minecraft:oak_stairs", nil)
|
||||
if stairs == StateAir {
|
||||
t.Fatal("oak stairs state is unavailable")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package world
|
|||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ var (
|
|||
stateByIDOnce sync.Once
|
||||
stateByIDImpl map[uint16]stateName
|
||||
idsByName map[string][]uint16
|
||||
defaultByName map[string]uint16
|
||||
)
|
||||
|
||||
// stateByID returns the named form of a block-state ID, building the lookup
|
||||
|
|
@ -79,6 +81,7 @@ func buildStateTable() {
|
|||
var blocks map[string]struct {
|
||||
States []struct {
|
||||
ID int `json:"id"`
|
||||
Default bool `json:"default"`
|
||||
Properties map[string]string `json:"properties"`
|
||||
} `json:"states"`
|
||||
}
|
||||
|
|
@ -87,6 +90,7 @@ func buildStateTable() {
|
|||
}
|
||||
stateByIDImpl = make(map[uint16]stateName, 30000)
|
||||
idsByName = make(map[string][]uint16, len(blocks))
|
||||
defaultByName = make(map[string]uint16, len(blocks))
|
||||
for name, b := range blocks {
|
||||
for _, s := range b.States {
|
||||
if s.ID < 0 || s.ID > 65535 {
|
||||
|
|
@ -95,12 +99,20 @@ func buildStateTable() {
|
|||
id := uint16(s.ID)
|
||||
stateByIDImpl[id] = stateName{Name: name, Properties: s.Properties}
|
||||
idsByName[name] = append(idsByName[name], id)
|
||||
if s.Default {
|
||||
defaultByName[name] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// blockPaletteEntry builds the NBT compound for a block-state ID: {Name,
|
||||
// Properties} (Properties omitted when empty). Unknown IDs map to air.
|
||||
//
|
||||
// Property keys are sorted. nbt.Compound preserves insertion order so that
|
||||
// encoding is deterministic, but ranging a Go map is not: the same chunk saved
|
||||
// twice produced different region-file bytes for every block with more than one
|
||||
// property, which makes a byte-level diff of two saves useless.
|
||||
func blockPaletteEntry(id uint16) *nbt.Compound {
|
||||
s, ok := stateByID(id)
|
||||
if !ok {
|
||||
|
|
@ -108,9 +120,14 @@ func blockPaletteEntry(id uint16) *nbt.Compound {
|
|||
}
|
||||
c := nbt.NewCompound().Set("Name", nbt.String(s.Name))
|
||||
if len(s.Properties) > 0 {
|
||||
keys := make([]string, 0, len(s.Properties))
|
||||
for k := range s.Properties {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
props := nbt.NewCompound()
|
||||
for k, v := range s.Properties {
|
||||
props.Set(k, nbt.String(v))
|
||||
for _, k := range keys {
|
||||
props.Set(k, nbt.String(s.Properties[k]))
|
||||
}
|
||||
c.Set("Properties", props)
|
||||
}
|
||||
|
|
@ -124,21 +141,47 @@ type paletteEntryKey struct {
|
|||
sig string
|
||||
}
|
||||
|
||||
// nameToStateID returns the block-state ID for a (name, properties) pair from
|
||||
// the loaded table. It is used when decoding on-disk chunk NBT back into a
|
||||
// Chunk. Unknown names/properties map to air (0).
|
||||
func nameToStateID(name string, props map[string]string) uint16 {
|
||||
// nameToStateID resolves a block name plus any properties to a state ID,
|
||||
// mirroring how vanilla reads a palette entry: start from the block's default
|
||||
// state and apply the properties it recognises, keeping the default's value for
|
||||
// anything it does not.
|
||||
//
|
||||
// It used to return the block's *first* state — blocks.json lists states in
|
||||
// StateDefinition.getPossibleStates() order, the property cartesian product,
|
||||
// which has nothing to do with the default. For 642 of 1168 blocks those
|
||||
// differ, so every caller passing nil got a corner state: redstone ore came out
|
||||
// permanently lit, a sunflower came out as its own top half, oak stairs came out
|
||||
// upside down and waterlogged. blocks.json marks the default state and the
|
||||
// parser was dropping the flag.
|
||||
//
|
||||
// ok is false only for a name that is not a block at all.
|
||||
func nameToStateID(name string, props map[string]string) (uint16, bool) {
|
||||
stateByIDOnce.Do(buildStateTable)
|
||||
ids := idsByName[name]
|
||||
for _, id := range ids {
|
||||
if propsMatch(stateByIDImpl[id].Properties, props) {
|
||||
return id
|
||||
defaultID, ok := defaultByName[name]
|
||||
if !ok {
|
||||
return StateAir, false
|
||||
}
|
||||
if len(props) == 0 {
|
||||
return defaultID, true
|
||||
}
|
||||
// Overlay only keys the block actually has; an unknown key or an illegal
|
||||
// value leaves the default's value in place, which is what
|
||||
// StateHolder.setValue's helper does after logging.
|
||||
base := stateByIDImpl[defaultID].Properties
|
||||
merged := make(map[string]string, len(base))
|
||||
for k, v := range base {
|
||||
if override, present := props[k]; present {
|
||||
merged[k] = override
|
||||
continue
|
||||
}
|
||||
merged[k] = v
|
||||
}
|
||||
for _, id := range idsByName[name] {
|
||||
if propsMatch(stateByIDImpl[id].Properties, merged) {
|
||||
return id, true
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
return ids[0]
|
||||
}
|
||||
return StateAir
|
||||
return defaultID, true
|
||||
}
|
||||
|
||||
func propsMatch(a, b map[string]string) bool {
|
||||
|
|
|
|||
91
internal/world/state_names_test.go
Normal file
91
internal/world/state_names_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"regionio/internal/nbt"
|
||||
)
|
||||
|
||||
// TestNameToStateIDDefaults pins the one thing this function has to get right:
|
||||
// a name with no properties resolves to the block's *default* state. It used to
|
||||
// return blocks.json's first state, which is the property cartesian product's
|
||||
// first entry and differs from the default for 642 of 1168 blocks.
|
||||
func TestNameToStateIDDefaults(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
want uint16
|
||||
why string
|
||||
}{
|
||||
{"minecraft:redstone_ore", 6882, "lit=false; the first state is lit=true, so every vein glowed"},
|
||||
{"minecraft:sunflower", 12916, "half=lower; the first state is the top half of the plant"},
|
||||
{"minecraft:oak_stairs", 3918, "north/bottom/straight/dry; the first state is top-half and waterlogged"},
|
||||
{"minecraft:grass_block", StateGrass, "snowy=false, and it must agree with the StateGrass constant"},
|
||||
{"minecraft:oak_log", StateOakLog, "axis=y, and it must agree with the StateOakLog constant"},
|
||||
{"minecraft:oak_leaves", StateOakLeaf, "distance=7/persistent=false/dry, agreeing with StateOakLeaf"},
|
||||
{"minecraft:stone", StateStone, "single state"},
|
||||
{"minecraft:water", StateWater, "level=0"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, ok := nameToStateID(c.name, nil)
|
||||
if !ok {
|
||||
t.Errorf("%s: not found", c.name)
|
||||
continue
|
||||
}
|
||||
if got != c.want {
|
||||
t.Errorf("%s = %d, want %d (%s)", c.name, got, c.want, c.why)
|
||||
}
|
||||
}
|
||||
if _, ok := nameToStateID("minecraft:not_a_block", nil); ok {
|
||||
t.Error("an unknown block name resolved to a state")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNameToStateIDOverrides checks the "default plus recognised overrides"
|
||||
// behaviour on the path that matters — decoding a palette entry off disk.
|
||||
func TestNameToStateIDOverrides(t *testing.T) {
|
||||
full, ok := nameToStateID("minecraft:oak_stairs", map[string]string{
|
||||
"facing": "east", "half": "top", "shape": "straight", "waterlogged": "true",
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("oak stairs not found")
|
||||
}
|
||||
partial, ok := nameToStateID("minecraft:oak_stairs", map[string]string{
|
||||
"facing": "east", "half": "top", "waterlogged": "true",
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("oak stairs not found")
|
||||
}
|
||||
if full != partial {
|
||||
t.Errorf("a partial property set gave %d, a complete one %d; the unnamed property should keep its default", partial, full)
|
||||
}
|
||||
|
||||
// An unknown key and an illegal value both fall back to the default rather
|
||||
// than to some unrelated corner state.
|
||||
def, _ := nameToStateID("minecraft:oak_stairs", nil)
|
||||
if got, _ := nameToStateID("minecraft:oak_stairs", map[string]string{"nonsense": "1"}); got != def {
|
||||
t.Errorf("unknown property gave %d, want the default %d", got, def)
|
||||
}
|
||||
if got, _ := nameToStateID("minecraft:oak_stairs", map[string]string{"facing": "sideways"}); got != def {
|
||||
t.Errorf("illegal property value gave %d, want the default %d", got, def)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBlockPaletteEntryDeterministic guards the one genuinely non-deterministic
|
||||
// thing in this file: the NBT Properties compound was filled by ranging a Go
|
||||
// map, so saving the same chunk twice produced different region-file bytes.
|
||||
func TestBlockPaletteEntryDeterministic(t *testing.T) {
|
||||
stairs, ok := nameToStateID("minecraft:oak_stairs", nil)
|
||||
if !ok {
|
||||
t.Fatal("oak stairs not found")
|
||||
}
|
||||
encode := func() []byte {
|
||||
return nbt.Marshal(nbt.NewCompound().Set("e", blockPaletteEntry(stairs)))
|
||||
}
|
||||
first := encode()
|
||||
for i := 0; i < 64; i++ {
|
||||
if got := encode(); !bytes.Equal(got, first) {
|
||||
t.Fatalf("palette entry bytes differ between encodes on attempt %d:\n%x\n%x", i, first, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ const dataVersion26 = 4790
|
|||
// first time it ran: chunkAt prefers the store over the generator, so the
|
||||
// already-explored area around spawn keeps its old terrain and every later fix
|
||||
// looks like it did nothing in exactly the place you are standing.
|
||||
const generatorVersion = 7
|
||||
const generatorVersion = 8
|
||||
|
||||
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
||||
// because it is ours, not part of the vanilla chunk format.
|
||||
|
|
@ -565,7 +565,9 @@ func readBlockStates(c *Chunk, si int, sc *nbt.Compound) {
|
|||
}
|
||||
name := string(nbtAsString(ec, "Name"))
|
||||
props := readProps(ec)
|
||||
ids[i] = nameToStateID(name, props)
|
||||
// An unknown block name decodes to air rather than to a neighbour's
|
||||
// state; that loses the block but does not corrupt the column.
|
||||
ids[i], _ = nameToStateID(name, props)
|
||||
}
|
||||
c.section(si) // ensure allocated
|
||||
s := c.sections[si]
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ func TestStoreLightRoundTrip(t *testing.T) {
|
|||
if _, err := cache.FrameErr(1, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
glowstone := nameToStateID("minecraft:glowstone", nil)
|
||||
glowstone, _ := nameToStateID("minecraft:glowstone", nil)
|
||||
if valid, _ := cache.SetBlockWithLight(15, 0, 8, glowstone); !valid {
|
||||
t.Fatal("glowstone edit rejected")
|
||||
}
|
||||
|
|
@ -191,7 +191,7 @@ func TestCacheReconcilesPersistedLightWithLoadedNeighbor(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
glowstone := nameToStateID("minecraft:glowstone", nil)
|
||||
glowstone, _ := nameToStateID("minecraft:glowstone", nil)
|
||||
left := NewChunk(0, 0, BiomePlains)
|
||||
left.SetBlock(15, 100, 8, glowstone)
|
||||
if err := store.SaveChunk(left); err != nil {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ func centreSurfaceBlock(ch *Chunk) (uint16, bool) {
|
|||
st := s[blockIndex(8, MinY+i*16+ly, 8)]
|
||||
if st != StateAir && st != StateWater {
|
||||
// Dry only if this top block is at/above sea level.
|
||||
return st, (MinY+i*16+ly) >= SeaLevel
|
||||
return st, (MinY + i*16 + ly) >= SeaLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import "testing"
|
|||
|
||||
func BenchmarkGenerateTerrain(b *testing.B) {
|
||||
gen := NewTerrainGenerator(0)
|
||||
b.ResetTimer(); b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ { _ = gen(int32(i), 0) }
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = gen(int32(i), 0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package world
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"regionio/internal/worldgen"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTerrainHeightProfile(t *testing.T) {
|
||||
|
|
@ -19,8 +19,12 @@ func TestTerrainHeightProfile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
line += fmt.Sprintf("%d ", top)
|
||||
if top < minH { minH = top }
|
||||
if top > maxH { maxH = top }
|
||||
if top < minH {
|
||||
minH = top
|
||||
}
|
||||
if top > maxH {
|
||||
maxH = top
|
||||
}
|
||||
}
|
||||
t.Logf("surface heights (z=8): %s", line)
|
||||
t.Logf("min=%d max=%d range=%d", minH, maxH, maxH-minH)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
package world
|
||||
|
||||
import "testing"
|
||||
func BenchmarkGenerateVanilla(b *testing.B){
|
||||
g:=NewVanillaGenerator(12345)
|
||||
b.ResetTimer(); b.ReportAllocs()
|
||||
for i:=0;i<b.N;i++{ _=g(int32(i),0) }
|
||||
|
||||
func BenchmarkGenerateVanilla(b *testing.B) {
|
||||
g := NewVanillaGenerator(12345)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = g(int32(i), 0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import (
|
|||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -33,10 +33,18 @@ func TestVanillaParity(t *testing.T) {
|
|||
ourY := int(oh[idx]) - 65
|
||||
d := int(math.Abs(float64(ourY - vh[idx])))
|
||||
total++
|
||||
if d == 0 { exact++ }
|
||||
if d <= 1 { within1++ }
|
||||
if d <= 3 { within3++ }
|
||||
if d > maxDiff { maxDiff = d }
|
||||
if d == 0 {
|
||||
exact++
|
||||
}
|
||||
if d <= 1 {
|
||||
within1++
|
||||
}
|
||||
if d <= 3 {
|
||||
within3++
|
||||
}
|
||||
if d > maxDiff {
|
||||
maxDiff = d
|
||||
}
|
||||
}
|
||||
}
|
||||
pct := func(n int) float64 { return 100 * float64(n) / float64(total) }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue