标签 Java 下的文章

本文发布于 2026 年 3 月。

想要搭建一个博客的想法从高中就有了,不过一直没有足够的动力完成这件事。直到后来在大学接触到 CTF 才重拾搭建个人网站的兴趣。在高中那段时间的一些简陋的小玩意就全都放在这里啦。(留作回忆)

Minecraft Bukkit 声明式命令框架

高中时(2021)和一个同班同学琢磨着开一个 Minecraft 插件服务器骗钱,在那段时间写了很多服务器插件。(记得当时经常需要逆向分析一些付费的闭源插件。)最终整个项目(PolarLand)因长期没有进展一直在折腾一些底层的架构而永久搁置。如果那时候有现在这样的 Agentic Coding 的话大概不会放弃吧。其中一个插件是将原本重复繁琐的处理玩家命令的功能写成一个声明式框架。说实话,里面有一些想法直到今天的我看来也是十分 hack (nerd) 的。比如为了将玩家传入的 x y z 坐标自动实例化为 Location 于是在运行时编译一个 Java 类来实现;还有一个 Array 转数组的魔法操作 parsedArgs.add(parsedArray.toArray((Object[]) Array.newInstance(type.getComponentType(), 0)));以及后面的 tab 键补全嵌套的命令参数(参数的类型是命令)功能。

核心源码:

/*
 * Copyright 2022 pwnerik.cn
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.polarmc.bukkitplugin.poleax.kit.command.parser;

import com.itranswarp.compiler.JavaStringCompiler;
import net.polarmc.bukkitplugin.poleax.kit.command.CommandModel;
import net.polarmc.bukkitplugin.poleax.kit.command.NoSuchCommandException;
import net.polarmc.bukkitplugin.poleax.kit.command.annotation.Optional;
import net.polarmc.bukkitplugin.poleax.kit.command.annotation.*;
import net.polarmc.bukkitplugin.poleax.kit.command.util.Util;
import net.polarmc.bukkitplugin.poleax.utils.MessageUtil;
import org.apache.commons.lang.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.help.HelpTopic;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

public class CommandBinder implements TabExecutor { // TODO text

    private final List<ParsedCommand> commands = new ArrayList<>();
    private final String messagePrefix;
    private final StringBuilder helpText = new StringBuilder();
    private final List<List<Object>> routeRaw = new ArrayList<>();
    private final Map<String, Completer> customCompleters = new HashMap<>();
    private static final List<Method> toBeWrappedMethods = new ArrayList<>();

    public CommandBinder(String rootCommand, String messagePrefix) {
        this.messagePrefix = messagePrefix;
//        Bukkit.getPluginCommand(rootCommand).setExecutor(this);
//        Bukkit.getPluginCommand(rootCommand).setTabCompleter(this);
    }

    public CommandBinder bind(CommandModel model) {
        bind0(new BasicCommandModel(), new String[0]);
        bind0(model, new String[0]);
        parseTabComplete();
        return this;
    }

    @SuppressWarnings("unused")
    public CommandBinder bindCompleter(String parameterId, Completer completer) {
        customCompleters.put(parameterId, completer);
        return this;
    }

    public boolean parse(CommandSender sender, String label, String @NotNull [] args) {
        if (args.length == 0) {
            args = new String[]{"help"}; //重定向到 help
        }
        ParsedCommand entry;
        try {
            entry = findEntry(args);
        } catch (NoSuchCommandException e) {
            sender.sendMessage(messagePrefix + ChatColor.RED + e.getMessage());
            return false;
        }
        String[] realArgs = Arrays.copyOfRange(args, entry.getRoute().size(), args.length);
        if (sender instanceof Player) {
            if (Arrays.stream(entry.getPermissions()).anyMatch(permission -> !sender.hasPermission(permission))) {
                sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 权限不足.");
                return false;
            }
        } else if (entry.isPlayerSenderRequired()) {
            sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 该命令只能由玩家执行.");
            return false;
        }
        int optionalCount = 0;
        if (realArgs.length != entry.getParameters().size()) {  //TODO bind时计算
            for (Parameter arg : entry.getParameters()) {
                if (arg.isOptional()) {
                    optionalCount++;
                }
            }
            if (entry.getParameters().get(entry.getParameters().size() - 1).getType().isArray()) {
                if (realArgs.length < entry.getParameters().size() - optionalCount) {
                    sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数长度有误, 需要至少 %d / 提供 %d.",
                            entry.getParameters().size() - optionalCount,
                            realArgs.length));
                    return false;
                }
            } else {
                if (optionalCount == 0) {
                    sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数长度有误, 需要 %d / 提供 %d.",
                            entry.getParameters().size(),
                            realArgs.length));
                    return false;
                }
                int requiredLength = entry.getParameters().size() - optionalCount;
                if (realArgs.length < requiredLength) {
                    sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数长度有误, 需要 %d 至 %d / 提供 %d.",
                            requiredLength,
                            entry.getParameters().size(),
                            realArgs.length));
                    return false;
                }
            }
        }
        String[] fullCommandLine = new String[args.length + 1]; // 用于报错
        fullCommandLine[0] = "/" + label;
        System.arraycopy(args, 0, fullCommandLine, 1, args.length);
        int i = 0;
        boolean arrayParameterPresented = false;
        List<Object> parsedArgs = new ArrayList<>();
        Parameter parameter;
        Class<?> type;
        Object value;
        for (String arg : realArgs) {
            parameter = entry.getParameters().get(i);
            type = parameter.getType();
            if (type.isArray()) {
                arrayParameterPresented = true;
                ArrayList<Object> parsedArray = new ArrayList<>();
                for (; i < realArgs.length; i++) { //主动 index 由预处理 parameter 变为用户传入 argument
                    try {
                        if (String[].class.equals(type)) {
                            parsedArray.add(realArgs[i]);
                        } else {
                            Object arrayElementValue = type.getComponentType().getMethod("valueOf", String.class)
                                    .invoke(null, realArgs[i]); // 静态方法无需依赖对象, 故传null.
                            if (isNumberClass(type.getComponentType()) && (((Number) arrayElementValue).longValue() < parameter.getMin() ||
                                    ((Number) arrayElementValue).longValue() > parameter.getMax())) { //忽略此警告, 因为参数类型已经确定
                                sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s[%d] 的值超出范围 ([%d, %d]).",
                                        parameter.getName(),
                                        i - entry.getParameters().size() + 1,
                                        parameter.getMin(),
                                        parameter.getMax()));
                                sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine));
                                sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2));
                                return false;
                            }
                            parsedArray.add(arrayElementValue);
                        }
                    } catch (IllegalAccessException | NoSuchMethodException e) {
                        sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x01).");
                        e.printStackTrace();
                        return false;
                    } catch (InvocationTargetException e) {
                        if (type.getComponentType().isEnum()) {
                            sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s[%d] 的值只能为 %s 之一",
                                    parameter.getName(),
                                    i - entry.getParameters().size() + 1,
                                    Util.simplify(Arrays.toString(Util.getOptions(type.getComponentType())))));
                            sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine));
                            sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2));
                        } else {
                            if (isNumberClass(type.getComponentType()) && StringUtils.isNumeric(realArgs[i])) {
                                if (parameter.getMin() != Long.MIN_VALUE || parameter.getMax() != Long.MAX_VALUE) {
                                    sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s[%d] 的值超出范围 ([%d, %d]).",
                                            parameter.getName(),
                                            i - entry.getParameters().size() + 1,
                                            parameter.getMin(),
                                            parameter.getMax()));
                                } else {
                                    try {
                                        sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s[%d] 的值超出范围 ([%d, %d]).",
                                                parameter.getName(),
                                                i - entry.getParameters().size() + 1,
                                                type.getComponentType().getField("MIN_VALUE").get(null), //忽略此警告, 因为参数类型已经确定
                                                type.getComponentType().getField("MAX_VALUE").get(null))); //忽略此警告, 因为参数类型已经确定
                                    } catch (IllegalAccessException | NoSuchFieldException ex) {
                                        sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x03).");
                                        e.printStackTrace();
                                        return false;
                                    }
                                } // TODO 不必要的 if bind 时处理
                            } else {
                                sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 传入参数 %s[%d] 类型有误, 应为 %s.",
                                        parameter.getName(),
                                        i - entry.getParameters().size() + 1,
                                        humanReadableTypeName(type.getComponentType())));
                            }
                            sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine));
                            sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2));
                        }
                        return false;
                    }
                }
                parsedArgs.add(parsedArray.toArray((Object[]) Array.newInstance(type.getComponentType(), 0))); // 转换为对应类型数组并添加至参数列表
                break;
            }
            try {
                if (String.class.equals(type)) {
                    value = arg;
                } else {
                    value = type.getMethod("valueOf", String.class).invoke(null, arg); // 静态方法无需依赖对象, 故传null.
                }
            } catch (IllegalAccessException | NoSuchMethodException e) {
                sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x01).");
                e.printStackTrace();
                return false;
            } catch (InvocationTargetException e) {
                if (type.isEnum()) {
                    sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s 的值只能为 %s 之一",
                            parameter.getName(),
                            Util.simplify(Arrays.toString(Util.getOptions(type)))));
                    sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine));
                    sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2));
                } else {
                    if (isNumberClass(type) && StringUtils.isNumeric(arg)) {
                        if (parameter.getMin() != Long.MIN_VALUE || parameter.getMax() != Long.MAX_VALUE) {
                            sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s 的值超出范围 ([%d, %d]).",
                                    parameter.getName(),
                                    parameter.getMin(),
                                    parameter.getMax()));
                        } else {
                            try {
                                sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s 的值超出范围 ([%d, %d]).",
                                        parameter.getName(),
                                        type.getField("MIN_VALUE").get(null), //忽略此警告, 因为参数类型已经确定
                                        type.getField("MAX_VALUE").get(null))); //忽略此警告, 因为参数类型已经确定
                            } catch (IllegalAccessException | NoSuchFieldException ex) {
                                sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x03).");
                                e.printStackTrace();
                                return false;
                            }
                        } // TODO 不必要的 if bind 时处理
                    } else {
                        sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 传入参数 %s 类型有误, 应为 %s.",
                                parameter.getName(),
                                humanReadableTypeName(type)));
                    }
                    sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine));
                    sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2));
                }
                return false;
            }
            if (isNumberClass(type) && (((Number) value).longValue() < parameter.getMin() ||
                    ((Number) value).longValue() > parameter.getMax())) { //忽略此警告, 因为参数类型已经确定
                sender.sendMessage(messagePrefix + ChatColor.RED + String.format("错误: 参数 %s 的值超出范围 ([%d, %d]).",
                        parameter.getName(),
                        parameter.getMin(),
                        parameter.getMax()));
                sender.sendMessage(messagePrefix + ChatColor.RED + argsToString(fullCommandLine));
                sender.sendMessage(messagePrefix + ChatColor.RED + generateErrorWavyUnderline(fullCommandLine, i + 2));
                return false;
            }
            parsedArgs.add(value);
            i++;
        }
        //填充可选参数null
        if (!arrayParameterPresented) {
            for (; i < entry.getParameters().size(); i++) {
                parsedArgs.add(null);
            }
        }
        entry.getModelObject().sender = sender; // 无需考虑线程安全
        entry.getModelObject().label = label;
        try {
            return (boolean) entry.getHandler().invoke(entry.getModelObject(), parsedArgs.toArray());
        } catch (IllegalAccessException e) {
            sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x02).");
            e.printStackTrace();
            return false;
        } catch (InvocationTargetException e) {
            sender.sendMessage(
                    messagePrefix + ChatColor.RED + String.format("错误: 在执行命令时出现问题 (%s), 请报告给服务器管理员.",
                            e.getCause()));
            e.printStackTrace();
            return false;
        }
    }

    private void bind0(@NotNull CommandModel model, String @NotNull [] baseRoute) {
        Class<? extends CommandModel> modelClass = model.getClass();
        if (baseRoute.length != 0 && modelClass.isAnnotationPresent(Help.class)) {
            helpText.append("=== ")
                    .append(Util.simplify(Arrays.toString(baseRoute)))
                    .append(" - ")
                    .append(modelClass.getAnnotation(Help.class).value())
                    .append(" ===")
                    .append("\n");
        }
        boolean helped = false;
        command:
        for (Method method : modelClass.getMethods()) {
            if (!method.isAnnotationPresent(Command.class)) {
                continue;
            }
            if (!boolean.class.equals(method.getReturnType())) {
                MessageUtil.error(String.format("在类 %s 中的方法 %s 的返回值类型必须为 boolean.",
                        modelClass.getSimpleName(),
                        method.getName()));
                continue;
            }
            Command commandAnnotation = method.getAnnotation(Command.class);
            if (commandAnnotation.value().isEmpty()) {
                MessageUtil.error(String.format("在类 %s 中的方法 %s 的 @Command 中没有指定命令路径.",
                        modelClass.getSimpleName(),
                        method.getName()));
                continue;
            }

            for (java.lang.reflect.Parameter parameter : method.getParameters()) {
                if (Location.class.equals(parameter.getType())) {
                    toBeWrappedMethods.add(method);
                    JavaStringCompiler javaStringCompiler = new JavaStringCompiler();
                    StringBuilder code = new StringBuilder("public class A extends net.polarmc.bukkitplugin.poleax.kit.command.CommandModel{public boolean wrapped");
                    code.append(method.getName());
                    code.append("(");
                    for (java.lang.reflect.Parameter parameter1 : method.getParameters()) {
                        if (parameter1.getType().equals(Location.class)) {
                            code.append("@net.polarmc.bukkitplugin.poleax.kit.command.annotation.Para(\"x\")Double x,@net.polarmc.bukkitplugin.poleax.kit.command.annotation.Para(\"y\")Double y,@net.polarmc.bukkitplugin.poleax.kit.command.annotation.Para(\"z\")Double z,");
                            continue;
                        }
                        code.append(generateParameterDeclaration(parameter1)).append(",");
                    }
                    code.deleteCharAt(code.length() - 1);
                    code
                            .append(")throws java.lang.IllegalAccessException, java.lang.reflect.InvocationTargetException{")
                            .append("org.bukkit.Location $l;")
                            .append("if(sender instanceof org.bukkit.entity.Entity)$l=new org.bukkit.Location(((org.bukkit.entity.Entity)sender).getLocation().getWorld(),x,y,z);")
                            .append("else $l=new org.bukkit.Location(null,x,y,z);")
                            .append("return (boolean) net.polarmc.bukkitplugin.poleax.kit.command.parser.CommandBinder.getToBeWrappedMethod(")
                            .append(toBeWrappedMethods.indexOf(method))
                            .append(").invoke(this,");
                    for (java.lang.reflect.Parameter parameter1 : method.getParameters()) {
                        if (parameter1.getType().equals(Location.class)) {
                            code.append("$l,");
                            continue;
                        }
                        code.append(parameter1.getName()).append(",");
                    }
                    code.deleteCharAt(code.length() - 1);
                    code.append(");}}");
                    Map<String, byte[]> results;
                    Class<?> wrappedMethodClass;
                    try {
                        results = javaStringCompiler.compile("A.java", code.toString());
                    } catch (IOException e) {
                        e.printStackTrace(); //TODO 异常处理
                        return;
                    }
                    try {
                        wrappedMethodClass = javaStringCompiler.loadClass("A", results);
                    } catch (ClassNotFoundException | IOException e) {
                        e.printStackTrace(); //TODO 异常处理
                        return;
                    }
                    method = wrappedMethodClass.getDeclaredMethods()[0];
                }
            }

            ParsedCommand.Builder command = new ParsedCommand.Builder();
            command.appendRoute(baseRoute);
            command.appendRoute(commandAnnotation.value().split(" "));
            if (method.isAnnotationPresent(Permission.class)) {
                command.setPermissions(method.getAnnotation(Permission.class).value());
            } else if (method.isAnnotationPresent(PlayerSenderRequired.class) ||
                    modelClass.isAnnotationPresent(PlayerSenderRequired.class)) {
                command.setPlayerSenderRequired(true);
            }
            if (method.isAnnotationPresent(Help.class)) {
                helped = true;
                command.setDescription(method.getAnnotation(Help.class).value());
            }
            command.setHandler(method);
            command.setModelObject(model);
            boolean hasOptionalParameterPresented = false;
            String parameterName;
            Class<?> parameterType;
            for (java.lang.reflect.Parameter parameter : method.getParameters()) {
                parameterName = getParameterName(parameter);
                if (parameterName == null) {
                    MessageUtil.error(String.format("在类 %s 中的方法 %s 的参数 %s 没有指定命名. (使用 @Para 注解或在编译时使用 -parameters 选项)",
                            modelClass.getSimpleName(),
                            method.getName(),
                            parameter.getName()));
                    continue command;
                }
                parameterType = parameter.getType();
                if (parameterType.isArray()) { // TODO 考虑数组类型可能不是可变参数
                    if (hasOptionalParameterPresented) {
                        MessageUtil.error(
                                String.format("在类 %s 中的方法 %s 中的可选 (@Optional) 参数与可变参数不可同时存在.",
                                        modelClass.getSimpleName(),
                                        method.getName()));
                        continue command;
                    }
                    if (parameterType.getComponentType().isEnum() && Util.getOptions(parameterType.getComponentType()).length == 1) {
                        MessageUtil.error(String.format("在类 %s 中的方法 %s 中的枚举类型可变参数 %s 的枚举项数必须大于一",
                                modelClass.getSimpleName(),
                                method.getName(),
                                parameterName));
                        continue command;
                    }
                    try {
                        if (!String[].class.equals(parameterType)) {
                            parameterType.getComponentType().getMethod("valueOf", String.class);
                        }
                    } catch (NoSuchMethodException e) {
                        MessageUtil.error(String.format(
                                "在类 %s 中的方法 %s 的参数 %s 的数组的组成类型不包含静态方法 valueOf(String), 无法应用于命令参数 (不要使用基本类型数组).",
                                modelClass.getSimpleName(),
                                method.getName(),
                                parameterName));
                        continue command;
                    }
                } else {
                    try {
                        if (!String.class.equals(parameterType)) {
                            parameterType.getMethod("valueOf", String.class);
                        }
                    } catch (NoSuchMethodException e) {
                        MessageUtil.error(
                                String.format("在类 %s 中的方法 %s 的参数 %s 的类型不包含静态方法 valueOf(String), 无法应用于命令参数 (不要使用基本类型).",
                                        modelClass.getSimpleName(),
                                        method.getName(),
                                        parameterName));
                        continue command;
                    }
                }
                Parameter.Builder parameterBuilder = new Parameter.Builder();
                if (parameter.isAnnotationPresent(Optional.class)) {
                    hasOptionalParameterPresented = true;
                    parameterBuilder.setOptional(true);
                } else if (hasOptionalParameterPresented) {
                    MessageUtil.error(String.format("在类 %s 中的方法 %s 中的可选 (@Optional) 参数必须在所有参数中的末尾位置.",
                            modelClass.getSimpleName(),
                            method.getName()));
                    continue command;
                }
                if (parameter.isAnnotationPresent(Tab.class)) {
                    TabType tabType = parameter.getAnnotation(Tab.class).value();
                    parameterBuilder.setTabType(tabType);
                    if (tabType == TabType.COMMAND && !parameterType.isArray()) {
                        MessageUtil.error(String.format("在类 %s 中的方法 %s 的命令 (@Tab(TabType.COMMAND)) 参数 %s 必须是数组类型.",
                                modelClass.getSimpleName(),
                                method.getName(),
                                parameterName));
                        continue command;
                    }
                }
                if (parameter.isAnnotationPresent(CustomTab.class)) {
                    parameterBuilder.setCustomParameterId(parameter.getAnnotation(CustomTab.class).value());
                }
                if (parameter.isAnnotationPresent(Range.class)) {
                    if (isNumberClass(parameterType) || (parameterType.getComponentType() != null && isNumberClass(parameterType.getComponentType()))) {
                        parameterBuilder
                                .setMin(parameter.getAnnotation(Range.class).min())
                                .setMax(parameter.getAnnotation(Range.class).max());
                    } else {
                        MessageUtil.error(String.format("在类 %s 中的方法 %s 的参数 %s 的类型不是数字类型, 无法应用于命令参数 (不要使用基本类型).",
                                modelClass.getSimpleName(),
                                method.getName(),
                                parameterName));
                        continue command;
                    }
                }
                parameterBuilder
                        .setName(parameterName)
                        .setType(parameterType);
                command.addParameter(parameterBuilder.build());
            }
            ParsedCommand result = command.build();
            if (helped) {
                helpText.append("/")
                        .append("%label")
                        .append(" ")
                        .append(result.toString())
                        .append(" - ")
                        .append(result.getDescription())
                        .append("\n");
                helped = false;
            } else {
                helpText.append("/")
                        .append("%label")
                        .append(" ")
                        .append(result.toString())
                        .append("\n");
            }
            commands.add(result);
        }
        for (Class<?> subModelClass : modelClass.getClasses()) {
            if (CommandModel.class.isAssignableFrom(subModelClass)
                    && subModelClass.isAnnotationPresent(Command.class)) {
                try {
                    String[] subRoute = subModelClass.getAnnotation(Command.class).value().split(" ");
                    bind0((CommandModel) subModelClass.newInstance(), Util.mergeStringArray(baseRoute, subRoute));
                } catch (IllegalAccessException | InstantiationException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private @NotNull ParsedCommand findEntry(String[] args) throws NoSuchCommandException {
        String lastTrial = null; // 用于"找不到命令..."报错.
        outer:
        for (ParsedCommand commandHandler : commands) {
            if (args.length < commandHandler.getRoute().size()) {
                continue;
            }
            int i = 0;
            for (String label : commandHandler.getRoute()) {
                if (!label.equals(args[i])) {
                    if (i != 0) {
                        lastTrial = args[i];
                    }
                    continue outer;
                }
                i++;
            }
            return commandHandler;
        }
        if (lastTrial == null) {
            throw new NoSuchCommandException("找不到命令: " + args[0]);
        } else {
            throw new NoSuchCommandException("找不到命令: " + lastTrial);
        }
    }

    private void parseTabComplete() {
        for (ParsedCommand command : commands) {
            ArrayList<Object> fullRoute = new ArrayList<>();
            fullRoute.addAll(command.getRoute());
            fullRoute.addAll(command.getParameters());
            routeRaw.add(fullRoute);
        }
    }

    private boolean checkType(String input, Class<?> type) throws NoSuchMethodException, IllegalAccessException {
        if (String.class.equals(type) || String[].class.equals(type) || type.isEnum() || (type.getComponentType() != null && type.getComponentType().isEnum())) {
            return true;
        }
        if (type.isEnum() || (type.getComponentType() != null && type.getComponentType().isEnum())) {
            return true;
        }
        try {
            type.getMethod("valueOf", String.class).invoke(null, input);
        } catch (InvocationTargetException e) {
            return false;
        }
        return true;
    }

    private boolean isNumberClass(Class<?> type) {
        return Number.class.isAssignableFrom(type);
    }

    private @NotNull String argsToString(String @NotNull [] args) {
        StringBuilder builder = new StringBuilder();
        for (String arg : args) {
            builder.append(arg).append(" ");
        }
        return builder.toString().trim();
    }

    private @NotNull String generateErrorWavyUnderline(String @NotNull [] args, int errorIndex) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < args.length; i++) {
            for (int j = 0; j < args[i].length(); j++) {
                if (i == errorIndex) {
                    builder.append("~");
                } else {
                    builder.append(" ");
                }
            }
            builder.append(" ");
        }
        return builder.toString();
    } // TODO 更换实现方式 (双s)

    private @NotNull String humanReadableTypeName(Class<?> type) {
        if (isNumberClass(type)) {
            if (Double.class.equals(type) || Float.class.equals(type)) { // 不可能为基本类型
                return "小数";
            }
            return "整数";
        }
        // 不可能为字符串
        return type.getSimpleName();
    }

    private @NotNull String generateParameterDeclaration(java.lang.reflect.Parameter parameter) {
        StringBuilder builder = new StringBuilder("@net.polarmc.bukkitplugin.poleax.kit.command.annotation.Para(\"");
        builder.append(getParameterName(parameter)).append("\") ");
        for (Annotation declaredAnnotation : parameter.getDeclaredAnnotations()) {
            builder.append(declaredAnnotation.annotationType().getCanonicalName()).append(" ");
        }
        builder.append(parameter.getType().getCanonicalName()).append(" ").append(parameter.getName());
        return builder.toString();
    }

    private @Nullable String getParameterName(java.lang.reflect.Parameter parameter) {
        if (parameter.isNamePresent()) {
            return parameter.getName();
        } else if (parameter.isAnnotationPresent(Para.class)) {
            return parameter.getAnnotation(Para.class).value();
        } else {
            return null;
        }
    }

    @Override
    public boolean onCommand(CommandSender sender, org.bukkit.command.Command command, String label, String[] args) {
        return parse(sender, label, args);
    }

    @Override
    public List<String> onTabComplete(CommandSender sender, org.bukkit.command.Command command, String alias,
                                      String[] args) {
        int i;
        List<String> result = new ArrayList<>();
        List<List<Object>> copy = new ArrayList<>(routeRaw);
        List<List<Object>> removeQueue = new ArrayList<>(); //删除与遍历不能并行操作
        for (List<Object> route : copy) {
            if (copy.size() == 1) {
                break;
            }
            i = 0;
            for (Object arg : route) {
                if (i >= args.length) {
                    break;
                }
                if (arg instanceof String && !StringUtil.startsWithIgnoreCase((String) arg, args[i])) {
                    removeQueue.add(route);
                    break;
                }
                if (arg instanceof Parameter) {
                    try {
                        Class<?> type = ((Parameter) arg).getType();
                        if (!checkType(args[i], type)) {
                            removeQueue.add(route);
                            break;
                        }
                    } catch (NoSuchMethodException | IllegalAccessException e) {
                        sender.sendMessage(messagePrefix + ChatColor.RED + "错误: 内部错误 (0x01).");
                        e.printStackTrace();
                        return new ArrayList<>(0);
                    }
                }
                i++;
            }
        }
        copy.removeAll(removeQueue);
        for (List<Object> route : copy) {
            Object temp = null;
            Object tail = route.get(route.size() - 1);
            if (tail instanceof Parameter && ((Parameter) tail).getType().isArray()) {
                if (route.size() < args.length) {
                    temp = tail;
                }
                if (route.size() >= args.length) {
                    temp = route.get(args.length - 1);
                }
            } else {
                if (route.size() < args.length) {
                    continue;
                }
                temp = route.get(args.length - 1);
            }
            if (temp instanceof String) {
                result.add((String) temp);
            } else if (temp instanceof Parameter) {
                if (((Parameter) temp).getCustomParameterId() != null) {
                    Completer completerFunction = customCompleters.get(((Parameter) temp).getCustomParameterId());
                    if (completerFunction != null) {
                        result.addAll(completerFunction.complete());
                    } else {
                        MessageUtil.error(String.format("未找到自定义 Tab 补全方法: %s.", ((Parameter) temp).getCustomParameterId()));
                        return result;
                    }
                }
                Class<?> type = ((Parameter) temp).getType();
                TabType tabType = ((Parameter) temp).getTabType();
                if (type == Boolean.class || (type.getComponentType() != null && type.getComponentType() == Boolean.class)) { // 不可能为基本类型
                    result.add("true");
                    result.add("false");
                } else if (type.isEnum() || (type.getComponentType() != null && type.getComponentType().isEnum())) {
                    result.addAll(Arrays.asList(Util.getOptions(type)));
                } else if (tabType == TabType.PLAYER) {
                    StringUtil.copyPartialMatches(args[args.length - 1], Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(Collectors.toList()), result);
                } else if (tabType == TabType.WORLD) {
                    StringUtil.copyPartialMatches(args[args.length - 1], Bukkit.getWorlds().stream().map(World::getName).collect(Collectors.toList()), result);
                } else if (tabType == TabType.COMMAND) {
                    List<String> commands = new ArrayList<>();
                    for (HelpTopic cmdLabel : Bukkit.getServer().getHelpMap().getHelpTopics()) { //TODO 取消在此获取
                        commands.add(cmdLabel.getName().replace("/", ""));
                    }
                    if (args.length == route.size()) {
                        StringUtil.copyPartialMatches(args[args.length - 1], commands, result);
                    } else {
                        PluginCommand targetCommand = Bukkit.getPluginCommand(args[route.size() - 1].replace("/", "")); //之前已确保 TabType.COMMAND 参数是最后一个参数
                        if (targetCommand != null) {
                            result.addAll(targetCommand.tabComplete(sender, args[route.size() - 1], Arrays.copyOfRange(args, route.size(), args.length)));
                        }
                    }
                }
            }
        }
        return result;
    }

    private class BasicCommandModel extends CommandModel {

        @SuppressWarnings("unused")
        @Command("help")
        @Help("显示命令帮助")
        public boolean help() {
            sender.sendMessage(helpText.toString().replace("%label", label).trim());
            return true;
        }

    }

    @SuppressWarnings("unused")
    public static Method getToBeWrappedMethod(int index) {
        return toBeWrappedMethods.get(index);
    }

}

最终达成的效果是可以像这样编写命令处理函数而无需从字符串数组开始处理,类似另一个更加成熟且仍在更新的开源插件 ACF。(懒了,用 AI 生成一个用例。)

// Generated by Claude Opus 4.6

import net.polarmc.bukkitplugin.poleax.kit.command.CommandModel;
import net.polarmc.bukkitplugin.poleax.kit.command.annotation.*;
import org.bukkit.Location;

@Help("示例命令组")
@PlayerSenderRequired
public class ExampleModel extends CommandModel {

    // 自定义枚举, 用于演示枚举参数与枚举数组参数
    public enum Mode { NORMAL, FAST, SLOW }

    // 1. 无参命令 + @Help + @Permission
    @Command("info")
    @Help("显示信息")
    @Permission({"example.info", "example.admin"})
    public boolean info() {
        sender.sendMessage("label=" + label);
        return true;
    }

    // 2. 基本类型参数 + @Para命名 + @Range范围限制 + @Tab玩家补全
    @Command("give")
    @Help("给予物品")
    public boolean give(
            @Para("玩家") @Tab(TabType.PLAYER) String target,
            @Para("数量") @Range(min = 1, max = 64) Integer amount
    ) {
        sender.sendMessage("give " + target + " x" + amount);
        return true;
    }

    // 3. @Optional可选参数 + 枚举参数 + Double类型
    @Command("set mode")
    @Help("设置模式")
    public boolean setMode(
            @Para("模式") Mode mode,
            @Para("倍率") @Optional Double multiplier
    ) {
        sender.sendMessage("mode=" + mode + " mul=" + multiplier);
        return true;
    }

    // 4. @Tab(WORLD) 世界补全 + Boolean参数
    @Command("tp world")
    @Help("传送到世界")
    @PlayerSenderRequired
    public boolean tpWorld(
            @Para("世界") @Tab(TabType.WORLD) String world,
            @Para("安全") @Optional Boolean safe
    ) {
        return true;
    }

    // 5. Location参数 (自动展开为x,y,z) + @Tab(COMMAND)命令数组补全
    @Command("exec at")
    @Help("在指定位置执行命令")
    public boolean execAt(
            @Para("位置") Location loc,
            @Para("命令") @Tab(TabType.COMMAND) String[] cmd
    ) {
        sender.sendMessage("loc=" + loc + " cmd=" + String.join(" ", cmd));
        return true;
    }

    // 6. 数组可变参数 + @Range + @CustomTab自定义补全
    @Command("sum")
    @Help("求和")
    public boolean sum(
            @Para("数字") @Range(min = 0, max = 1000) @CustomTab("numberHints") Integer[] numbers
    ) {
        int s = 0;
        for (Integer n : numbers) s += n;
        sender.sendMessage("sum=" + s);
        return true;
    }

    // 7. 枚举数组可变参数
    @Command("modes")
    @Help("设置多个模式")
    public boolean modes(@Para("模式列表") Mode[] modes) {
        return true;
    }

    // 8. 内部类作为子命令组 (@Command on TYPE)
    @Command("admin")
    @Help("管理员命令组")
    @PlayerSenderRequired
    public static class AdminSubModel extends CommandModel {

        @Command("reload")
        @Help("重载配置")
        @Permission({"example.admin"})
        public boolean reload() {
            sender.sendMessage("reloaded");
            return true;
        }

        @Command("debug")
        @Help("调试信息")
        public boolean debug(@Para("详细") @Optional Boolean verbose) {
            msg("debug verbose=" + verbose); // 使用继承的 msg()
            return true;
        }
    }
}

《复杂》捡罐子机器人 Roby —— C 语言实现

高中时读到一本书叫《复杂》(Complexity),讲的是大量的简单个体表现出的复杂行为。似乎是目前 LLM 的理论基础之一?虽然很喜欢这本书但我现在没在做 LLM 相关的工作(不太符合我对计科的想象)。书中有一个例子是一种假想的机器人 Roby 在 2D 网格中移动,它只能看见自己前后左右以及脚下这五个格子。地图中某些格子会有一个“罐子”,地图边界是不能通过的墙壁。使用遗传算法机器学习反复模拟即可得到连人类也难以想出的策略,而这些“策略”只是一串数字。

// SPDX-License-Identifier: MIT
// Copyright (c) 2023 rik

#define _GNU_SOURCE

#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define WIDTH 10
#define HEIGHT 10
#define CANS 50
static_assert(CANS <= WIDTH * HEIGHT, "Too many cans");
#define RULES 200
#define EPOCHS 1000
#define STEPS 200
#define EXECS 100
#define MIXES 2
#define WEIGHT M_E

#define PICK_UP_CAN_REWARD 10
#define PICK_UP_AIR_PUNISH -1
#define KICK_WALL_PUNISH -5

#define PROGRESS_BAR_UNITS 10
static_assert(!(EPOCHS % PROGRESS_BAR_UNITS),
              "EPOCHS is not divisible by PROGRESS_BAR_UNITS");
#define WEIGHTED(X, MAX)                                                       \
    ((size_t)(powf(WEIGHT, -((float)(X) / MAX * (logf(MAX) / logf(WEIGHT)))) *    \
              (MAX)) -                                                         \
     1)
#define _SITUATIONS 243
#define _ACTIONS 7
#define _DIRECTIONS 4

enum action {
    move_north,
    move_south,
    move_east,
    move_west,
    stand_still,
    pick_up,
    move_random,
};

struct result {
    enum action rule[_SITUATIONS];
    long score;
};

struct history {
    long strategy[_SITUATIONS][_ACTIONS][PROGRESS_BAR_UNITS];
    enum action best_rule[_SITUATIONS];
    long best_score;
};

// static uint64_t state = 0x853c49e6748fea9bULL;

// static uint32_t fast_rand(void) {
//     uint64_t oldstate = state;
//     state = oldstate * 6364136223846793005ULL + 0xda3e39cb94b95bdbULL;
//     uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u;
//     uint32_t rot = oldstate >> 59u;
//     return (xorshifted >> rot) | (xorshifted << ((-rot) & 31));
// }

// static void fast_srand(const unsigned int seed) {
//     state = seed ^ 0x853c49e6748fea9bULL;
// }

// #define random fast_rand
// #define srandom fast_srand

static void create_random_world(bool world[restrict HEIGHT][WIDTH]) {
    bool *target;
    long i;
    for (i = 0; i < HEIGHT; i++) {
        memset(world[i], false, sizeof(bool) * WIDTH);
    }
    for (i = 0; i < CANS; i++) {
        do {
            target = world[random() % HEIGHT] + random() % WIDTH;
        } while (*target);
        *target = true;
    }
}

static void create_random_rule(enum action rule[restrict _SITUATIONS]) {
    for (long i = 0; i < _SITUATIONS; i++) {
        rule[i] = random() % _ACTIONS;
    }
}

static long get_situation(const long x, const long y,
                          const bool world[restrict HEIGHT][WIDTH]) {
    long situation = 0;
    if (y != 0) {
        situation += world[y - 1][x] ? 1 : 2;
    }
    situation *= 3;
    if (y != HEIGHT - 1) {
        situation += world[y + 1][x] ? 1 : 2;
    }
    situation *= 3;
    if (x != WIDTH - 1) {
        situation += world[y][x + 1] ? 1 : 2;
    }
    situation *= 3;
    if (x != 0) {
        situation += world[y][x - 1] ? 1 : 2;
    }
    situation *= 3;
    situation += world[y][x] ? 1 : 2;
    return situation;
}

#define UNLIKELY(X) __builtin_expect((X), false)

static long act(long *restrict x, long *restrict y,
                bool world[restrict HEIGHT][WIDTH], const enum action action) {
    switch (action) {
    case move_north:
        if (UNLIKELY(*y == 0)) {
            return KICK_WALL_PUNISH;
        } else {
            *y -= 1;
            return 0;
        }
    case move_south:
        if (UNLIKELY(*y == HEIGHT - 1)) {
            return KICK_WALL_PUNISH;
        } else {
            *y += 1;
            return 0;
        }
    case move_east:
        if (UNLIKELY(*x == WIDTH - 1)) {
            return KICK_WALL_PUNISH;
        } else {
            *x += 1;
            return 0;
        }
    case move_west:
        if (UNLIKELY(*x == 0)) {
            return KICK_WALL_PUNISH;
        } else {
            *x -= 1;
            return 0;
        }
    case stand_still:
        return 0;
    case pick_up:
        if (world[*y][*x]) {
            world[*y][*x] = false;
            return PICK_UP_CAN_REWARD;
        } else {
            return PICK_UP_AIR_PUNISH;
        }
    case move_random:
        return act(x, y, world, random() % _DIRECTIONS);
    }
    return 0;
}

static void mix_rules(const enum action rule1[restrict _SITUATIONS],
                      const enum action rule2[restrict _SITUATIONS],
                      enum action result1[restrict _SITUATIONS],
                      enum action result2[restrict _SITUATIONS]) {
    long i;
    long split = random() % (_SITUATIONS - 1);
    memcpy(result1, rule1, split * sizeof(enum action));
    memcpy(result2, rule2, split * sizeof(enum action));
    memcpy(result1 + split, rule2 + split,
           (_SITUATIONS - split) * sizeof(enum action));
    memcpy(result2 + split, rule1 + split,
           (_SITUATIONS - split) * sizeof(enum action));
    for (i = 0; i < MIXES; i++) {
        result1[random() % _SITUATIONS] = random() % _ACTIONS;
        result2[random() % _SITUATIONS] = random() % _ACTIONS;
    }
}

static int compare_results(const void *restrict result1,
                           const void *restrict result2) {
    return (int)(((struct result *)result2)->score -
                 ((struct result *)result1)->score);
}

static void train(struct history *restrict history, FILE *restrict log_file) {
    long i, j, k, l;
    long x, y, score, progress;
    enum action rules[RULES][_SITUATIONS];
    bool world[HEIGHT][WIDTH];
    bool temp_world[HEIGHT][WIDTH];
    struct result results[RULES];

    progress = 0;
    for (i = 0; i < RULES; i++) {
        create_random_rule(rules[i]);
    }

    printf("    Progress: ");
    for (i = 0; i < PROGRESS_BAR_UNITS; i++) {
        putchar('_');
    }
    for (i = 0; i < PROGRESS_BAR_UNITS; i++) {
        putchar('\b');
    }
    fflush(stdout);
    for (i = 0; i < EPOCHS; i++) {
        for (k = 0; k < RULES; k++) {
            results[k].score = 0;
        }

        for (j = 0; j < EXECS; j++) {
            create_random_world(world);

            for (k = 0; k < RULES; k++) {
                memcpy(temp_world, world, sizeof(bool) * HEIGHT * WIDTH);
                x = 0;
                y = 0;
                score = 0;

                for (l = 0; l < STEPS; l++) {
                    score += act(&x, &y, temp_world,
                                 rules[k][get_situation(x, y, temp_world)]);
                }
                results[k].score += score;
            }
        }

        for (j = 0; j < RULES; j++) {
            memcpy(results[j].rule, rules[j],
                   sizeof(enum action) * _SITUATIONS);
        }
        qsort(results, RULES, sizeof(struct result), compare_results);

        if (results[0].score > history->best_score) {
            memcpy(history->best_rule, results[0].rule,
                   sizeof(enum action) * _SITUATIONS);
            history->best_score = results[0].score;
        }
        fprintf(log_file, "%ld, ", results[0].score / EXECS);
        for (j = 0; j < _SITUATIONS; j++) {
            fprintf(log_file, "%u,", results[0].rule[j]);
            history->strategy[j][results[0].rule[j]][progress]++;
        }
        fputc('\n', log_file);
        if ((i + 1) % (EPOCHS / PROGRESS_BAR_UNITS) == 0) {
            putchar('*');
            fflush(stdout);
            progress++;
        }
        if (i == EPOCHS - 1) {
            putchar('\n');
            break;
        }

        for (j = 0; j < RULES - 1; j += 2) {
            mix_rules(results[WEIGHTED(random() % RULES, RULES)].rule,
                      results[WEIGHTED(random() % RULES, RULES)].rule, rules[j],
                      rules[j + 1]);
        }
    }
}

static void release_history(struct history *restrict history,
                            FILE *restrict log_file) {
    static const char *block_state[] = {"WALL", "CAN", "AIR"};
    long north, south, east, west, center;
    long i, _i, j;

    for (i = 0; i < _SITUATIONS; i++) {
        _i = i;
        center = _i % 3;
        _i -= center;
        _i /= 3;
        west = _i % 3;
        _i -= west;
        _i /= 3;
        east = _i % 3;
        _i -= east;
        _i /= 3;
        south = _i % 3;
        _i -= south;
        _i /= 3;
        north = _i % 3;

        fprintf(log_file, "\t%s\t\tmove_north: ", block_state[north]);
        for (j = 0; j < PROGRESS_BAR_UNITS; j++) {
            fprintf(log_file, "%ld", history->strategy[i][move_north][j]);
            if (j != PROGRESS_BAR_UNITS - 1) {
                fputs(" -> ", log_file);
            }
        }
        fputs(", move_south: ", log_file);
        for (j = 0; j < PROGRESS_BAR_UNITS; j++) {
            fprintf(log_file, "%ld", history->strategy[i][move_south][j]);
            if (j != PROGRESS_BAR_UNITS - 1) {
                fputs(" -> ", log_file);
            }
        }
        fprintf(log_file, "\n%s\t%s\t%s\tmove_east: ", block_state[west],
                block_state[center], block_state[east]);
        for (j = 0; j < PROGRESS_BAR_UNITS; j++) {
            fprintf(log_file, "%ld", history->strategy[i][move_east][j]);
            if (j != PROGRESS_BAR_UNITS - 1) {
                fputs(" -> ", log_file);
            }
        }
        fputs(", move_west: ", log_file);
        for (j = 0; j < PROGRESS_BAR_UNITS; j++) {
            fprintf(log_file, "%ld", history->strategy[i][move_west][j]);
            if (j != PROGRESS_BAR_UNITS - 1) {
                fputs(" -> ", log_file);
            }
        }
        fprintf(log_file, "\n\t%s\t\tpick_up: ", block_state[south]);
        for (j = 0; j < PROGRESS_BAR_UNITS; j++) {
            fprintf(log_file, "%ld", history->strategy[i][pick_up][j]);
            if (j != PROGRESS_BAR_UNITS - 1) {
                fputs(" -> ", log_file);
            }
        }
        fputs(", move_random: ", log_file);
        for (j = 0; j < PROGRESS_BAR_UNITS; j++) {
            fprintf(log_file, "%ld", history->strategy[i][move_random][j]);
            if (j != PROGRESS_BAR_UNITS - 1) {
                fputs(" -> ", log_file);
            }
        }
        fputs("\n\n\n", log_file);
    }
}

static void release_best_steps(struct history *restrict history,
                               FILE *restrict log_file) {
    long i, j, k;
    long x, y;
    bool world[HEIGHT][WIDTH];
    create_random_world(world);

    fprintf(log_file, "The best rule's result (average score = %ld/%u):\n\n",
            history->best_score / EXECS, CANS * PICK_UP_CAN_REWARD);

    x = 0;
    y = 0;
    for (i = 0; i <= STEPS; i++) {
        fprintf(log_file, "Step %ld:\n", i);
        for (j = 0; j < (WIDTH + 1) * 2; j++) {
            fputc('-', log_file);
        }
        fputc('\n', log_file);
        for (j = 0; j < HEIGHT; j++) {
            fputc('|', log_file);
            for (k = 0; k < WIDTH; k++) {
                if (j == y && k == x) {
                    if (world[j][k]) {
                        fputs("@ ", log_file);
                    } else {
                        fputs("O ", log_file);
                    }
                } else {
                    if (world[j][k]) {
                        fputs(". ", log_file);
                    } else {
                        fputs("  ", log_file);
                    }
                }
            }
            fputs("|\n", log_file);
        }
        for (j = 0; j < (WIDTH + 1) * 2; j++) {
            fputc('-', log_file);
        }
        fputs("\n\n\n", log_file);

        if (i == STEPS) {
            break;
        }

        act(&x, &y, world, history->best_rule[get_situation(x, y, world)]);
    }

    fputs("Done.", log_file);
}

int main(void) {
    struct history history = {0};
    FILE *train_log, *history_log, *best_steps_log;
    unsigned int seed;
    const char *seed_str;

    puts("Initializing...");
    train_log = fopen("train_log.csv", "w");
    history_log = fopen("strategy.txt", "w");
    best_steps_log = fopen("best_steps.txt", "w");
    if (train_log == NULL || history_log == NULL || best_steps_log == NULL) {
        perror("Unable to open log file");
        exit(EXIT_FAILURE);
    }
    seed_str = getenv("ROBY_SEED");
    if (seed_str) {
        seed = atoi(seed_str);
    } else {
        seed = (unsigned int)time(NULL);
    }
    srandom(seed);

    puts("Training...");
    train(&history, train_log);

    puts("Releasing...");
    release_history(&history, history_log);
    release_best_steps(&history, best_steps_log);

    fclose(train_log);
    fclose(history_log);
    fclose(best_steps_log);
    puts("Done.");
    return 0;
}

物品分拣机器人模拟

链接 我觉得盯着这玩意的感觉有点像盯着火焰或者滚筒洗衣机之类,可以看很久(