This commit is contained in:
mingyang 2022-06-30 22:19:59 +08:00
parent f424be4a1d
commit a579b5334e
9 changed files with 773 additions and 0 deletions

3
.gitignore vendored
View File

@ -3,6 +3,9 @@ __pycache__/
*.py[cod]
*$py.class
recipes.json
backup/
# C extensions
*.so

View File

@ -1,2 +1,21 @@
# LuckyColorRecipeCalculator
A luckycolor recipe calculator for islet to get maximum protection.
# What is this?
Islet is a minecraft server that have been playing lately and it has a special armor system. In short, only leather armor could reduce the damage and the degree of the reduction depends on the manhattan distance between the color of the leather armor and a randomly generated color named "luckycolor".
To this end, inspired by [ANRAR4](https://github.com/ANRAR4)'s repo [ANRAR4/DyeLeatherArmor](https://github.com/ANRAR4/DyeLeatherArmor), I re-write the code that calculate the craftable colors in c++ and wrote a python script that helps to find the closest color to "luckycolor".
# How to use?
## I just want to calculat my luckycolor:
1. decompress the file ```./data/recipes.7z```, you will get ```recipes.json```. Put it in the current floder.
2. run ```python find_recipe.py```
## How can I get recipe.json from scratch:
1. compile ```leatherColorCombinationsCalculator.cpp``` with ```g++ -O3 -Wall leatherColorCombinationsCalculator.cpp -lpthread -o leatherColorCombinationsCalculator```
2. run ```./leatherColorCombinationsCalculator <numThreads>```, it will produce ```layer1-14.bin``` (the generated file is also provided in ./data/layers.7z)
3. run ```python generate_recipes.py``` to generate ```recipes.json```.

106
colors.py Normal file
View File

@ -0,0 +1,106 @@
from base64 import decode, encode
from torch import full
mc_colors = [
['红色染料', '#B02E26', 'red'],
['绿色染料', '#5E7C16', 'green'],
['紫色染料', '#8932B8', 'purple'],
['青色染料', '#169C9C', 'cyan'],
['淡灰色染料', '#9D9D97', 'silver'],
['灰色染料', '#474F52', 'gray'],
['粉红色染料', '#F38BAA', 'pink'],
['黄绿色染料', '#80C71F', 'lime'],
['黄色染料', '#FED83D', 'yellow'],
['淡蓝色染料', '#3AB3DA', 'lightBlue'],
['品红色染料', '#C74EBD', 'magenta'],
['橙色染料', '#F9801D', 'orange'],
['黑色染料', '#1D1D21', 'black'],
['棕色染料', '#835432', 'brown'],
['蓝色染料', '#3C44AA', 'blue'],
['白色染料', '#F9FFFE', 'white']
]
colornames = [
"red", "green", "purple", "cyan",
"silver", "gray", "pink", "lime",
"yellow", "lightBlue", "magenta", "orange",
"black", "brown", "blue", "white"
]
def encode_color(r, g, b):
color = '#{r}{g}{b}'.format(
r = hex(r)[2:].rjust(2, '0'),
g = hex(g)[2:].rjust(2, '0'),
b = hex(b)[2:].rjust(2, '0'),
)
return color
def decode_color(color:str):
color = color.strip('#')
color = int(color, base=16)
r = (color>>16) & 0xff
g = (color>>8) & 0xff
b = color & 0xff
return [r, g, b]
def parse_mc_color(colors):
parsed_color = {}
for color in colors:
name, value, key = color
assert len(value) == 7
r, g, b = decode_color(value)
parsed_color[key] = {
"name": name,
"key": key,
"value": [r, g, b]
}
return parsed_color
colors = parse_mc_color(mc_colors)
def calculate_fused_color(colors):
if len(colors) == 0:
return None
# calculate average color
avg_color = []
num_colors = len(colors)
for channel in zip(*colors):
channel_avg = sum(channel) // num_colors
avg_color.append(channel_avg)
# calculate gain factor.
avg_max = sum([max(x) for x in colors])/num_colors
max_avg = max(avg_color)
# print('avgcolor:', avg_color)
# print('avgmax:', avg_max)
# print('maxavg:', max_avg)
gain_factor = avg_max / max_avg
fused_color = [int(x*gain_factor) for x in avg_color]
return fused_color
def get_color_by_recipe(recipe, current_color=None):
color_list = []
for key, count in recipe.items():
if count > 0:
color_list += [colors[key]['value']]*count
if current_color is not None:
color_list.append(current_color)
return color_list
if __name__ == '__main__':
# color = get_color_by_recipe({
# "purple": 1,
# "blue": 1,
# "white": 6
# })
# print('color:', color)
# print(calculate_fused_color(color))
color = [123, 59, 71]
encoded = encode_color(*color)
decoded = decode_color(encoded)
print('color:', color)
print('encoded:', encoded)
print('decoded:', decoded)

BIN
data/layers.7z Normal file

Binary file not shown.

BIN
data/recipes.7z Normal file

Binary file not shown.

156
find_recipe.py Normal file
View File

@ -0,0 +1,156 @@
from base64 import decode
from utils import load_dict
from colors import colors, colornames, decode_color
import math
def calculate_leather_color(recipes):
cache = {}
for layer in recipes:
for color in recipes[layer]:
cache[color] = {
"color": decode_color(color),
"step": layer
}
return cache
def color_distance(c1, c2):
return sum([abs(x - y) for x, y in zip(c1, c2)])
def find_closest_color(cache, color, max_step=-1):
min_dist = 10000
closest_color = None
for name, this_color in cache.items():
if max_step > 0 and this_color['step'] > max_step:
continue
this_dist = color_distance(color, this_color['color'])
if this_dist < min_dist:
min_dist = this_dist
closest_color = name
return closest_color, min_dist
def decode_recipe(encoded):
encoded = int(encoded, base=16)
counts = []
while len(counts) < 16:
counts.append(encoded%9)
encoded //=9
return {name:count for name, count in zip(colornames, counts) if count > 0}
def query_recipe(recipes, name):
# search color layer.
recipe = None
for layer in recipes:
recipe = recipes[layer].get(name, None)
if recipe is not None:
break
if recipe is None:
raise ValueError('Color not craftable.')
base_color, recipe = recipe
recipe = decode_recipe(recipe)
if base_color == '#000000':
return [recipe]
else:
steps = query_recipe(recipes, base_color)
steps.append(recipe)
return steps
def explain_recipe(steps):
infos = []
for i, step in enumerate(steps):
info = ','.join(['{0}*{1}'.format(colors[name]['name'], count) for name, count in step.items()])
infos.append(info)
print('->'.join(infos))
def calculate_multipler(diff, enchant=False):
diff = 256 if diff > 256 else diff
multiplier = None
if enchant:
multiplier = 1 - (0.50) * (256.0 - diff)*0.5 / 256.0
else:
multiplier = 1 - (0.50) * (256.0 - diff) / 256.0
multiplier = multiplier * math.log(80) / math.log(125) # single cloth.
return multiplier
def show_protection(diff):
print('============= 减伤系数 =============')
multiplier = calculate_multipler(diff, enchant=False)
multiplier_full = multiplier**4
multiplier_elytra = multiplier**3 * math.log(432) / math.log(125)
info = '\n'.join([
"无附魔情况下,单件衣服减伤系数为:{0}".format(multiplier),
"无附魔情况下,全套衣服减伤系数为:{0}".format(multiplier_full),
"无附魔情况下,三件衣服+鞘翅减伤系数为:{0}".format(multiplier_elytra),
])
print(info)
multiplier = calculate_multipler(diff, enchant=True)
multiplier_full = multiplier**4
multiplier_elytra = multiplier**3 * math.log(432) / math.log(125)
info = '\n'.join([
"有附魔情况下,单件衣服减伤系数为:{0}".format(multiplier),
"有附魔情况下,全套衣服减伤系数为:{0}".format(multiplier_full),
"有附魔情况下,三件衣服+鞘翅减伤系数为:{0}".format(multiplier_elytra),
])
print(info)
print('在有保护类附魔存在时实际减伤表格如下所示。其中横排分别表示单件、全套、有鞘翅三种情况纵列分别表示保护系数为0/1/2/3/4/5')
print('(保护系数的计算上限为5其中保护四记为1火焰、爆炸、弹射物保护对其对应的伤害时记为2摔落保护对摔落保护记为3每件衣服的保护系数可以直接相加且上限为5超过5记为5)')
multiplier_protection = [1 - x*4/25 for x in range(6)]
multiplier_protect_enchant = [multiplier*this_protection for this_protection in multiplier_protection]
multiplier_full_protect_enchant = [multiplier_full*this_protection for this_protection in multiplier_protection]
multiplier_elytra_protect_enchant = [multiplier_elytra*this_protection for this_protection in multiplier_protection]
info = '\n'.join([
' '.join(['%.4f']*6)%tuple(multiplier_protect_enchant),
' '.join(['%.4f']*6)%tuple(multiplier_full_protect_enchant),
' '.join(['%.4f']*6)%tuple(multiplier_elytra_protect_enchant)
])
print(info)
if __name__ == '__main__':
print('正在加载配方数据...')
recipes = load_dict('./recipes.json')
print('配方数据库加载完成!正在准备数据...')
leather_color_cache = calculate_leather_color(recipes)
print('数据准备完毕,共加载{0}种可合成颜色。'.format(len(leather_color_cache)))
note = '\n'.join(
[
'请输入你的幸运色,用空格分隔。',
'如你的幸运色为100 200 100, 请输入 100 200 100。',
'默认所有颜色可用,若你希望限制合成的步骤数量,可以在幸运色后追加一个数字表示每种颜色的最大合成步骤',
'如 100 200 100 2 表示给出的配方中最多进行两次合成。'
])
print(note)
while True:
luckycolor = None
max_step = -1
while luckycolor is None:
try:
luckycolor_str = input('>>> ')
luckycolor = luckycolor_str.split()
luckycolor = [int(x) for x in luckycolor]
if len(luckycolor) < 3 or len(luckycolor) > 4:
raise ValueError
if len(luckycolor) == 4:
max_step = luckycolor[3]
luckycolor = luckycolor[:3]
except Exception:
print('幸运色颜色格式错误qwq')
luckycolor = None
print('正在寻找最接近的可合成颜色...')
name, dist = find_closest_color(leather_color_cache, luckycolor, max_step=max_step)
color = decode_color(name)
print('最接近的可合成颜色为: {0}{1},曼哈顿距离误差为:{2}'.format(
name, tuple(color), dist
))
show_protection(dist)
print('查找配方中...')
crafting_steps = query_recipe(recipes, name)
print('合成配方如下:')
explain_recipe(crafting_steps)

65
generate_recipes.py Normal file
View File

@ -0,0 +1,65 @@
import os
import struct
from utils import save_dict
from colors import encode_color, colornames, colors
def decode_color_from_bytes(buffer):
color = struct.unpack('L', buffer)[0]
r = (color>>16) & 0xff
g = (color>>8) & 0xff
b = color & 0xff
return r, g, b
def decode_recipe_from_bytes(buffer):
encoded = struct.unpack('Q', buffer)[0]
# recipe = []
# while len(recipe) < 16:
# recipe.append(encoded%9)
# encoded //=9
# return {name:count for name, count in zip(colornames, recipe) if count > 0}
return hex(encoded)[2:].rjust(16, '0')
def load_recipe(filepath):
file = open(filepath, 'rb')
while True:
base_color = file.read(4)
if base_color == b'':
break
recipe = file.read(8)
if recipe == b'':
raise ValueError
crafted_color = file.read(4)
base_color = decode_color_from_bytes(base_color)
recipe = decode_recipe_from_bytes(recipe)
crafted_color = decode_color_from_bytes(crafted_color)
yield base_color, recipe, crafted_color
def convert_recipes(fmt='./layer{0}.bin', save_path='./recipes.json'):
i = 1
print('checking', fmt.format(i))
recipe_db = {}
counter = 0
while os.path.exists(fmt.format(i)):
path = fmt.format(i)
print('loading ', path)
recipes = load_recipe(path)
this_converted_recipe = {}
this_counter = 0
for base_color, recipe, crafted_color in recipes:
crafted_color = encode_color(*crafted_color)
base_color = encode_color(*base_color)
this_converted_recipe[crafted_color] = [base_color, recipe]
this_counter += 1
recipe_db[str(i)] = this_converted_recipe
print('layer {0} has {1} colors.'.format(i, this_counter))
counter += this_counter
i += 1
print('{0} colors loaded in total.'.format(counter))
print('writing result to', save_path)
save_dict(save_path, recipe_db, indent=None)
if __name__ == '__main__':
convert_recipes()

View File

@ -0,0 +1,404 @@
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <bitset>
#include <map>
#include <thread>
#include <mutex>
template<typename T>
void printVector(std::vector<T> vec){
for (size_t i = 0; i < vec.size(); i++)
{
std::cout<<vec[i];
if (i < vec.size() - 1) {
std::cout<<", ";
}
}
std::cout<<std::endl;
}
template<typename T>
void printMatrix(std::vector<std::vector<T>> mat){
for (size_t i = 0; i < mat.size(); i++)
{
printVector(mat[i]);
}
}
class Color
{
public:
Color():r(0), g(0), b(0){}
Color(const Color& color) {
r = color.r;
g = color.g;
b = color.b;
}
Color(uint32_t color){
Color c = Color::decode(color);
r = c.r;
g = c.g;
b = c.b;
}
Color(int _r, int _g, int _b){
r = uint8_t(_r);
g = uint8_t(_g);
b = uint8_t(_b);
}
uint32_t encode(void) const{
return (uint32_t(r)<<16) + (uint32_t(g)<<8) + uint32_t(b);
}
static Color decode(uint32_t val) {
uint8_t r = (val>>16) & 0xff;
uint8_t g = (val>>8) & 0xff;
uint8_t b = val & 0xff;
return Color(r, g, b);
}
void show(void) {
std::cout<<"(r, g, b) = ("<<uint32_t(r)<<", "<<uint32_t(g)<<", "<<uint32_t(b)<<")"<<std::endl;
}
bool operator==(const Color& color) const {
return (r == color.r) && (g == color.g) && (b ==color.b);
}
public:
uint8_t r;
uint8_t g;
uint8_t b;
};
class ColorCraftingTable {
public:
ColorCraftingTable(void){
totalRed = 0;
totalGreen = 0;
totalBlue = 0;
maxSum = 0;
numColors = 0;
}
public:
void addColor(const Color& color) {
totalRed += color.r;
totalGreen += color.g;
totalBlue += color.b;
maxSum += std::max(color.r, std::max(color.g, color.b));
numColors += 1;
}
Color craft() {
uint32_t avgRed = totalRed / numColors;
uint32_t avgGreen = totalGreen / numColors;
uint32_t avgBlue = totalBlue / numColors;
float avgOfMax = maxSum*1.0 / numColors;
uint32_t maxAvg = std::max(avgRed, std::max(avgGreen, avgBlue));
// std::cout<<numColors<<","<<avgRed<<","<<avgGreen<<","<<avgBlue<<","<<avgOfMax<<","<<maxAvg<<","<<std::endl;
return Color(
uint8_t(float(avgRed)*avgOfMax/maxAvg),
uint8_t(float(avgGreen)*avgOfMax/maxAvg),
uint8_t(float(avgBlue)*avgOfMax/maxAvg)
);
}
private:
uint32_t totalRed;
uint32_t totalGreen;
uint32_t totalBlue;
uint32_t maxSum;
uint32_t numColors;
};
void generateCompose(int total, int choice, std::vector<int>& current, std::vector<std::vector<int>>& ans){
if (choice <= 0) {
std::vector<int> compose;
for (size_t i = 0; i < current.size(); i++)
{
compose.push_back(current[current.size() - 1 - i]);
}
ans.push_back(compose);
return;
}
if (total<=0 || choice <=0 || choice > total) return;
// do not choose current number
generateCompose(total-1, choice, current, ans);
// choose current number.
current.push_back(total);
generateCompose(total-1, choice-1, current, ans);
current.pop_back();
}
std::vector<int> composeToRecipe(const std::vector<int>& compose, int num) {
std::vector<int> pre = compose;
std::vector<int> post = compose;
pre.insert(pre.begin(), 0);
post.push_back(num);
std::vector<int> recipe;
for (size_t i = 1; i < pre.size(); i++)
{
recipe.push_back(post[i] - pre[i] - 1);
}
return recipe;
}
class Recipe {
public:
Recipe(const std::vector<int>& _counts){
counts = _counts;
}
public:
uint64_t encode(void) {
uint64_t val = 0;
uint64_t bits = 1;
for (size_t i = 0; i < counts.size(); i++)
{
val += bits * counts[i];
bits *= 9;
}
return val;
}
static Recipe decode(uint64_t encoded) {
std::vector<int> counts;
for (size_t i = 0; i < 16; i++)
{
counts.push_back(int(encoded%9));
encoded /= 9;
}
return Recipe(counts);
}
void preCraft(const std::vector<Color>& colorlist) {
for (size_t i = 0; i < counts.size(); i++)
{
for (int j = 0; j < counts[i]; j++)
{
table.addColor(colorlist[i]);
}
}
}
Color getTableColor(void) {
return table.craft();
}
Color getCraftedArmorColor(const Color armorColor) {
ColorCraftingTable temp = table;
if (!(armorColor == Color(0, 0, 0))) {
temp.addColor(armorColor);
}
return temp.craft();
}
std::vector<int> getCounts(void){
return counts;
}
private:
std::vector<int> counts;
ColorCraftingTable table;
};
void dumpPossibleRecipe(const std::string& path) {
std::vector<std::vector<int>> ans;
std::vector<int> current;
std::fstream recipes;
recipes.open(path, std::fstream::out|std::fstream::binary);
generateCompose(24, 16, current, ans);
size_t counts = 0;
for (size_t i = 0; i < ans.size(); i++)
{
Recipe recipe(composeToRecipe(ans[i], 25));
uint64_t encoded = recipe.encode();
if (encoded == 0) continue; // this recipe is empty.
recipes.write((char*) &encoded, sizeof(encoded));
counts += 1;
}
recipes.close();
std::cout<<"we have write "<<counts<<" possible recipes."<<std::endl;
}
template<int N>
class SyncholizedHeapBitset {
public:
SyncholizedHeapBitset(void) {
bitmap = new std::bitset<N>;
}
~SyncholizedHeapBitset(void) {
delete bitmap;
}
bool test(size_t pos) {return bitmap->test(pos);}
void set(size_t pos, bool val) {bitmap->set(pos, val);}
size_t count(void) {return bitmap->count();}
private:
std::bitset<N>* bitmap;
};
void searchBlock(
std::mutex& lock,
std::vector<Recipe>& recipes,// readonly
SyncholizedHeapBitset<(2<<24)>& colormap, // thread-safe
std::vector<Color>& oldColors, // read-only
std::vector<Color>& newColors, //write-only
std::fstream& searched, // write-only.
size_t start, size_t end
) {
for (size_t i = start; i < end; i++)
{
for(size_t j = 0; j < recipes.size(); j++) {
Color crafted = recipes[j].getCraftedArmorColor(oldColors[i]);
uint32_t encodedColor = crafted.encode();
if (!colormap.test(encodedColor)) {
lock.lock(); // for thread safety.
if (!colormap.test(encodedColor)) { // double check after locked to avoid duplication.
colormap.set(encodedColor, true);
newColors.push_back(crafted);
uint32_t encodedOldColor = oldColors[i].encode();
searched.write((const char*) &encodedOldColor, sizeof(encodedOldColor));
uint64_t encodedRecipe = recipes[j].encode();
searched.write((const char*) &encodedRecipe, sizeof(encodedRecipe));
searched.write((const char*) &encodedColor, sizeof(encodedColor));
}
lock.unlock();
}
}
}
}
void searchAllColors(const std::vector<Color>& colorlist, int numThreads=8, int blockSize=32) {
std::cout<<"preparing..."<<std::endl;
// generate all possible recipes for a leather.
std::vector<Recipe> recipes;
std::vector<std::vector<int>> ans;
std::vector<int> current;
std::cout<<"Generating compose..."<<std::endl;
generateCompose(24, 16, current, ans);
size_t counts = 0;
std::cout<<"Converting compose to recipe..."<<std::endl;
for (size_t i = 0; i < ans.size(); i++)
{
Recipe recipe(composeToRecipe(ans[i], 25));
if (recipe.encode() == 0) continue; // this recipe is empty.
recipes.push_back(recipe);
counts += 1;
}
std::cout<<"we have find "<<counts<<" possible recipes."<<std::endl;
// pre-craft all possible recipes to save time.
std::cout<<"Pre-crafting recipe..."<<std::endl;
for (size_t i = 0; i < recipes.size(); i++)
{
recipes[i].preCraft(colorlist);
}
// lets start to search.
SyncholizedHeapBitset<(2<<24)> colormap;
std::vector<Color> oldColors;
// color #0x000000 do not exist in minecraft, so it is okay to be used as empty color.
oldColors.push_back(Color(uint32_t(0)));
std::mutex lock;
size_t layer = 1;
while (oldColors.size() > 0) {
std::cout<<"processing layer "<<layer<<std::endl;
std::fstream output;
std::string savepath = std::string("layer") + std::to_string(layer) + std::string(".bin");
std::cout<<"writing generated recipe in this layer to file "<<savepath<<"..."<<std::endl;
output.open(
savepath,
std::fstream::out|std::fstream::binary
);
std::vector<Color> newColors;
auto start = std::chrono::high_resolution_clock::now();
auto last = start;
for (size_t i = 0; i < oldColors.size(); i+=numThreads*blockSize)
{
// create threads and dispatch jobs.
std::vector<std::thread*> threads;
for(int j = 0; j < numThreads; j++) {
int start = i + j*blockSize;
int end = i + (j+1)*blockSize;
if (end > (int) oldColors.size()) {
end = (int) oldColors.size();
}
if (start > end) {start = end;}
std::thread* t = new std::thread(
searchBlock,
std::ref(lock),
std::ref(recipes),
std::ref(colormap),
std::ref(oldColors),
std::ref(newColors),
std::ref(output),
start, end
);
threads.push_back(t);
}
// wait for all threads to finish.
for(size_t j = 0; j < threads.size(); j++) {
threads[j]->join();
delete threads[j];
threads[j] = nullptr;
}
auto now = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = now - start;
std::chrono::duration<double> interval = now - last;
if (interval.count() >= 5) {
last = now;
std::cout<<"progress: "<<double((i+1)*100)/oldColors.size()<<"%, ";
std::cout<<colormap.count()<<" colors found.";
double totalTime = diff.count()*oldColors.size()/(i+1);
double remeaningTime = totalTime * (1 - (i+1)*1.0/oldColors.size());
std::cout<<"estimating "<<totalTime<<" s in total and "<<remeaningTime<<" s left."<<std::endl;
}
}
output.close();
oldColors = newColors;
std::cout<<"layer "<<layer<<" finished, found "<<oldColors.size()<<" new colors."<<std::endl;
layer++;
}
}
int main(int argc, char const *argv[])
{
std::vector<Color> colors = {
Color(176, 46, 38),
Color(94, 124, 22),
Color(137, 50, 184),
Color(22, 156, 156),
Color(157, 157, 151),
Color(71, 79, 82),
Color(243, 139, 170),
Color(128, 199, 31),
Color(254, 216, 61),
Color(58, 179, 218),
Color(199, 78, 189),
Color(249, 128, 29),
Color(29, 29, 33),
Color(131, 84, 50),
Color(60, 68, 170),
Color(249, 255, 254),
};
// useless in this script, but useful for interpreting encoded recipe.
std::vector<std::string> keys = {
"red", "green", "purple", "cyan",
"silver", "gray", "pink", "lime",
"yellow", "lightBlue", "magenta", "orange",
"black", "brown", "blue", "white"
};
// parse numThreads.
if (argc == 2) {
int numThreads = std::stoi(std::string(argv[1]));
searchAllColors(colors, numThreads);
} else {
std::cout<<"Usage: ./leatherColorCombinationsCalculator <numThreads>"<<std::endl;
}
return 0;
}

20
utils.py Normal file
View File

@ -0,0 +1,20 @@
import json
def save_dict(path, json_dict, indent=4, mute=True):
if not mute:
print('dumping json string...')
text = json.dumps(json_dict, indent=indent, ensure_ascii=False)
if not mute:
print('saving to {0}...'.format(path))
with open(path, 'w+', encoding='utf-8') as f:
f.write(text)
def load_dict(path, mute=True):
if not mute:
print('reading file {0}...'.format(path))
with open(path, 'r', encoding='utf-8') as f:
text = f.read()
if not mute:
print('parsing json...')
json_dict = json.loads(text)
return json_dict