每日算法 — 使用java实现相扑:重心博弈与推力平衡算法

引言

相扑是日本传统的摔跤竞技运动,两名力士在土俵(圆形比赛场地)中竞技,通过推、摔、拉等技巧使对手倒地或逼出界外。本文用Java实现相扑对抗的核心物理模拟与推力平衡算法,讲解重心控制、摩擦力学与博弈决策在竞技对抗中的应用。

物理模型建立

核心物理量

相扑对抗的本质是重心控制与力的平衡。两名力士可简化为具有质量、重心位置和支撑面的刚体模型:

  • 质量与身高:影响惯性和稳定性
  • 重心高度:重心越低越稳定
  • 支撑面:两脚之间的区域构成支撑多边形
  • 摩擦力:防止滑动的最大静摩擦力
  • 推力:对手施加的水平力

胜负判定条件

  1. 倒地:重心投影超出支撑面
  2. 出界:身体任何部位触及土俵外
  3. 触地:除脚底外任何部位接触地面

核心算法设计

1. 力士数据结构与状态

import java.util.*;

/**
 * 相扑力士类
 * 包含物理属性、位置状态和竞技行为
 */
class SumoWrestler {
    // 力士名称
    String name;
    // 质量 (kg)
    double mass;
    // 身高 (m)
    double height;
    // 重心高度 (m),下蹲时降低
    double centerOfMassHeight;
    // 当前位置 (x, z)
    double posX, posZ;
    // 当前速度
    double velX, velZ;
    // 两脚间距,决定支撑面宽度
    double stanceWidth;
    // 是否下蹲(降低重心)
    boolean isCrouching;
    // 是否倒地
    boolean isDown;
    // 是否出界
    boolean isOut;
    // 剩余体力
    double stamina;
    // 最大推力
    double maxPushForce;

    public SumoWrestler(String name, double mass, double height) {
        this.name = name;
        this.mass = mass;
        this.height = height;
        this.centerOfMassHeight = height * 0.55; // 站立时重心约在身高55%处
        this.stanceWidth = 0.6; // 默认站距
        this.stamina = 100.0;
        this.maxPushForce = mass * 2.0; // 最大推力约体重的2倍
        this.isDown = false;
        this.isOut = false;
        this.isCrouching = false;
    }

    /**
     * 获取当前重心在水平面上的投影坐标
     * 相扑中通过调整姿势可使重心前后移动
     */
    public double[] getCenterOfMassProjection() {
        // 前倾时重心前移
        double leanOffset = isCrouching ? 0.05 : 0.0;
        return new double[]{posX + leanOffset, posZ};
    }

    /**
     * 获取支撑面边界
     * 支撑面为两脚之间的矩形区域
     * 
     * @return double[]{minX, maxX, minZ, maxZ}
     */
    public double[] getSupportBounds() {
        double halfWidth = stanceWidth / 2;
        return new double[]{
            posX - halfWidth, posX + halfWidth,
            posZ - 0.15, posZ + 0.15
        };
    }

    /**
     * 判断当前姿势是否稳定
     * 重心投影必须在支撑面内
     */
    public boolean isStable() {
        double[] com = getCenterOfMassProjection();
        double[] bounds = getSupportBounds();
        return com[0] >= bounds[0] && com[0] <= bounds[1]
            && com[1] >= bounds[2] && com[1] <= bounds[3];
    }

    /**
     * 下蹲动作:降低重心提高稳定性,但移动速度下降
     */
    public void crouch() {
        isCrouching = true;
        centerOfMassHeight = height * 0.35; // 下蹲后重心降低
        stanceWidth = 0.8; // 蹲姿两脚更宽
        maxPushForce = mass * 2.5; // 蹲姿推力更大
    }

    /**
     * 站起动作:恢复站立姿势
     */
    public void standUp() {
        isCrouching = false;
        centerOfMassHeight = height * 0.55;
        stanceWidth = 0.6;
        maxPushForce = mass * 2.0;
    }

    /**
     * 应用推力,更新速度和位置
     * 考虑摩擦力的限制
     * 
     * @param forceX X方向推力
     * @param forceZ Z方向推力
     * @param dt 时间步长
     */
    public void applyForce(double forceX, double forceZ, double dt) {
        // 地面最大静摩擦力 = 摩擦系数 * 正压力
        double frictionCoeff = 0.7; // 土俵表面摩擦系数
        double normalForce = mass * 9.8; // 正压力 = 重力
        double maxFriction = frictionCoeff * normalForce;

        // 总推力大小
        double totalForce = Math.sqrt(forceX * forceX + forceZ * forceZ);

        // 如果推力超过最大静摩擦力,力士开始滑动
        if (totalForce > maxFriction) {
            // 滑动时受动摩擦力(较小)
            double kineticFriction = 0.5 * normalForce;
            double scale = kineticFriction / totalForce;
            forceX *= scale;
            forceZ *= scale;
        }

        // 更新加速度、速度、位置
        double accelX = forceX / mass;
        double accelZ = forceZ / mass;

        velX += accelX * dt;
        velZ += accelZ * dt;

        // 速度阻尼(模拟地面阻力)
        velX *= 0.95;
        velZ *= 0.95;

        posX += velX * dt;
        posZ += velZ * dt;

        // 消耗体力
        stamina -= totalForce * dt * 0.001;
    }
}

2. 土俵场地与比赛规则

/**
 * 相扑土俵场地类
 * 直径4.55米的圆形场地
 */
class Dohyo {
    // 土俵半径 (m)
    static final double RADIUS = 2.275;
    // 中心位置
    double centerX, centerZ;

    public Dohyo(double centerX, double centerZ) {
        this.centerX = centerX;
        this.centerZ = centerZ;
    }

    /**
     * 判断位置是否在土俵内
     */
    public boolean isInside(double x, double z) {
        double dx = x - centerX;
        double dz = z - centerZ;
        return Math.sqrt(dx * dx + dz * dz) <= RADIUS;
    }

    /**
     * 获取到土俵边界的距离
     */
    public double distanceToEdge(double x, double z) {
        double dx = x - centerX;
        double dz = z - centerZ;
        double distFromCenter = Math.sqrt(dx * dx + dz * dz);
        return RADIUS - distFromCenter;
    }
}

3. 推力平衡与对抗模拟

/**
 * 相扑对抗物理引擎
 * 模拟两名力士的力学交互
 */
class SumoPhysics {

    /**
     * 计算两名力士之间的相互作用力
     * 基于弹性碰撞模型,考虑身体接触变形
     * 
     * @param w1 力士1
     * @param w2 力士2
     * @return double[]{forceOnW1X, forceOnW1Z, forceOnW2X, forceOnW2Z}
     */
    public static double[] calculateInteractionForces(SumoWrestler w1, SumoWrestler w2) {
        double dx = w2.posX - w1.posX;
        double dz = w2.posZ - w1.posZ;
        double distance = Math.sqrt(dx * dx + dz * dz);

        // 接触距离阈值(两人身体半径之和约0.6m)
        double contactThreshold = 0.6;

        if (distance > contactThreshold) {
            // 未接触,无相互作用力
            return new double[]{0, 0, 0, 0};
        }

        // 接触时的弹性斥力(防止穿透)
        double overlap = contactThreshold - distance;
        double springConstant = 50000; // 弹性系数
        double elasticForce = springConstant * overlap;

        // 方向归一化
        double nx = dx / distance;
        double nz = dz / distance;

        // 推力:每名力士主动施加的力
        double push1 = w1.maxPushForce * (w1.stamina / 100.0);
        double push2 = w2.maxPushForce * (w2.stamina / 100.0);

        // 合力计算
        // 力士1受到的力:力士2的推力 + 弹性斥力(方向远离力士2)
        double forceOn1X = nx * (push2 + elasticForce);
        double forceOn1Z = nz * (push2 + elasticForce);

        // 力士2受到的力:力士1的推力 + 弹性斥力(方向远离力士1)
        double forceOn2X = -nx * (push1 + elasticForce);
        double forceOn2Z = -nz * (push1 + elasticForce);

        return new double[]{forceOn1X, forceOn1Z, forceOn2X, forceOn2Z};
    }
}

4. AI决策算法:推力平衡策略

/**
 * 相扑AI决策类
 * 使用推力平衡博弈算法决定每一步动作
 */
class SumoAI {

    /**
     * 评估当前局面的得分
     * 分数越高对我方越有利
     * 
     * @param me 我方力士
     * @param opponent 对手力士
     * @param dohyo 土俵
     * @return 局面评分
     */
    public static double evaluatePosition(SumoWrestler me, SumoWrestler opponent, Dohyo dohyo) {
        double score = 0;

        // 1. 稳定性得分(重心越居中越稳定)
        double[] com = me.getCenterOfMassProjection();
        double[] bounds = me.getSupportBounds();
        double centerX = (bounds[0] + bounds[1]) / 2;
        double stabilityScore = 1.0 - Math.abs(com[0] - centerX) / ((bounds[1] - bounds[0]) / 2);
        score += stabilityScore * 30;

        // 2. 位置得分(离土俵中心越近越有利)
        double distToCenter = Math.sqrt(
            (me.posX - dohyo.centerX) * (me.posX - dohyo.centerX) +
            (me.posZ - dohyo.centerZ) * (me.posZ - dohyo.centerZ)
        );
        score += (1.0 - distToCenter / Dohyo.RADIUS) * 20;

        // 3. 对手不利得分(对手越靠近边界越好)
        double opponentDistToEdge = dohyo.distanceToEdge(opponent.posX, opponent.posZ);
        score += (1.0 - opponentDistToEdge / Dohyo.RADIUS) * 25;

        // 4. 体力优势
        score += (me.stamina - opponent.stamina) * 0.5;

        // 5. 重心高度优势(越低越稳定)
        double heightAdvantage = (opponent.centerOfMassHeight - me.centerOfMassHeight) * 10;
        score += heightAdvantage;

        return score;
    }

    /**
     * 贪心策略:选择最优动作
     * 评估所有候选动作,选择使局面评分最高的
     * 
     * @param me 我方力士
     * @param opponent 对手力士
     * @param dohyo 土俵
     * @return 最优动作
     */
    public static Action chooseBestAction(SumoWrestler me, SumoWrestler opponent, Dohyo dohyo) {
        List<Action> candidates = generateActions(me);

        Action bestAction = null;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (Action action : candidates) {
            // 模拟执行动作
            simulateAction(me, opponent, dohyo, action);
            double score = evaluatePosition(me, opponent, dohyo);
            undoAction(me, opponent, action);

            if (score > bestScore) {
                bestScore = score;
                bestAction = action;
            }
        }

        return bestAction != null ? bestAction : new Action(ActionType.PUSH_FORWARD, 0.5);
    }

    /**
     * 生成候选动作列表
     */
    private static List<Action> generateActions(SumoWrestler me) {
        List<Action> actions = new ArrayList<>();

        // 前冲(主动进攻)
        for (double intensity = 0.3; intensity <= 1.0; intensity += 0.35) {
            actions.add(new Action(ActionType.PUSH_FORWARD, intensity));
        }

        // 侧向移动
        actions.add(new Action(ActionType.STEP_LEFT, 0.5));
        actions.add(new Action(ActionType.STEP_RIGHT, 0.5));

        // 下蹲防守
        actions.add(new Action(ActionType.CROUCH, 1.0));

        // 站起恢复
        if (me.isCrouching) {
            actions.add(new Action(ActionType.STAND_UP, 1.0));
        }

        // 后退诱敌(风险高但可诱使对手失衡)
        actions.add(new Action(ActionType.RETREAT, 0.3));

        return actions;
    }

    /**
     * 模拟执行一个动作
     */
    private static void simulateAction(SumoWrestler me, SumoWrestler opponent, 
                                       Dohyo dohyo, Action action) {
        double dt = 0.1; // 模拟时间步

        switch (action.type) {
            case PUSH_FORWARD:
                // 向对手方向施加推力
                double dx = opponent.posX - me.posX;
                double dz = opponent.posZ - me.posZ;
                double dist = Math.sqrt(dx * dx + dz * dz);
                if (dist > 0) {
                    double force = me.maxPushForce * action.intensity;
                    me.applyForce(dx / dist * force, dz / dist * force, dt);
                }
                break;

            case STEP_LEFT:
                me.applyForce(-me.maxPushForce * 0.3, 0, dt);
                break;

            case STEP_RIGHT:
                me.applyForce(me.maxPushForce * 0.3, 0, dt);
                break;

            case CROUCH:
                me.crouch();
                break;

            case STAND_UP:
                me.standUp();
                break;

            case RETREAT:
                double rdx = me.posX - opponent.posX;
                double rdz = me.posZ - opponent.posZ;
                double rdist = Math.sqrt(rdx * rdx + rdz * rdz);
                if (rdist > 0) {
                    double rforce = me.maxPushForce * 0.5;
                    me.applyForce(rdx / rdist * rforce, rdz / rdist * rforce, dt);
                }
                break;
        }

        // 模拟对手的被动反应(简化:对手也前冲)
        double odx = me.posX - opponent.posX;
        double odz = me.posZ - opponent.posZ;
        double odist = Math.sqrt(odx * odx + odz * odz);
        if (odist > 0) {
            double oforce = opponent.maxPushForce * 0.5;
            opponent.applyForce(odx / odist * oforce, odz / odist * oforce, dt);
        }
    }

    /**
     * 撤销动作(简化恢复)
     */
    private static void undoAction(SumoWrestler me, SumoWrestler opponent, Action action) {
        // 在实际应用中需要保存/恢复完整状态
        // 这里简化处理
    }
}

/**
 * 动作类型枚举
 */
enum ActionType {
    PUSH_FORWARD,  // 前冲
    STEP_LEFT,     // 左移
    STEP_RIGHT,    // 右移
    CROUCH,        // 下蹲
    STAND_UP,      // 站起
    RETREAT        // 后退
}

/**
 * 动作类
 */
class Action {
    ActionType type;
    double intensity; // 动作强度 0~1

    public Action(ActionType type, double intensity) {
        this.type = type;
        this.intensity = intensity;
    }
}

5. 完整比赛模拟

/**
 * 相扑比赛模拟器
 */
public class SumoGame {

    public static void main(String[] args) {
        // 创建土俵
        Dohyo dohyo = new Dohyo(0, 0);

        // 创建两名力士
        SumoWrestler wrestler1 = new SumoWrestler("横纲·山岚", 180, 1.85);
        SumoWrestler wrestler2 = new SumoWrestler("大关·海鸣", 165, 1.78);

        // 初始位置:土俵两侧
        wrestler1.posX = -0.8;
        wrestler1.posZ = 0;
        wrestler2.posX = 0.8;
        wrestler2.posZ = 0;

        System.out.println("=== 相扑比赛开始 ===");
        System.out.printf("红方: %s (%.0fkg, %.2fm)%n", wrestler1.name, wrestler1.mass, wrestler1.height);
        System.out.printf("蓝方: %s (%.0fkg, %.2fm)%n", wrestler2.name, wrestler2.mass, wrestler2.height);

        double dt = 0.05; // 50ms时间步
        double totalTime = 0;
        int step = 0;
        final int MAX_STEPS = 2000;

        while (step < MAX_STEPS) {
            step++;
            totalTime += dt;

            // 双方AI决策
            Action action1 = SumoAI.chooseBestAction(wrestler1, wrestler2, dohyo);
            Action action2 = SumoAI.chooseBestAction(wrestler2, wrestler1, dohyo);

            // 执行动作
            executeAction(wrestler1, wrestler2, dohyo, action1, dt);
            executeAction(wrestler2, wrestler1, dohyo, action2, dt);

            // 计算相互作用力
            double[] forces = SumoPhysics.calculateInteractionForces(wrestler1, wrestler2);
            wrestler1.applyForce(forces[0], forces[1], dt);
            wrestler2.applyForce(forces[2], forces[3], dt);

            // 检查稳定性
            if (!wrestler1.isStable()) {
                wrestler1.isDown = true;
            }
            if (!wrestler2.isStable()) {
                wrestler2.isDown = true;
            }

            // 检查出界
            if (!dohyo.isInside(wrestler1.posX, wrestler1.posZ)) {
                wrestler1.isOut = true;
            }
            if (!dohyo.isInside(wrestler2.posX, wrestler2.posZ)) {
                wrestler2.isOut = true;
            }

            // 输出状态(每100步输出一次)
            if (step % 100 == 0) {
                System.out.printf("[%5.1fs] %s: pos(%+.2f, %+.2f) stamina=%.1f | %s: pos(%+.2f, %+.2f) stamina=%.1f%n",
                    totalTime,
                    wrestler1.name, wrestler1.posX, wrestler1.posZ, wrestler1.stamina,
                    wrestler2.name, wrestler2.posX, wrestler2.posZ, wrestler2.stamina);
            }

            // 判定胜负
            String winner = null;
            if (wrestler1.isDown || wrestler1.isOut) {
                winner = wrestler2.name;
            } else if (wrestler2.isDown || wrestler2.isOut) {
                winner = wrestler1.name;
            }

            if (winner != null) {
                System.out.println("\n=== 比赛结束 ===");
                System.out.println("获胜者: " + winner);
                System.out.printf("总用时: %.1f秒, 总步数: %d%n", totalTime, step);
                break;
            }
        }

        if (step >= MAX_STEPS) {
            System.out.println("\n=== 比赛时间到 ===");
            // 根据位置判定(更接近中心者胜)
            double d1 = Math.sqrt(wrestler1.posX * wrestler1.posX + wrestler1.posZ * wrestler1.posZ);
            double d2 = Math.sqrt(wrestler2.posX * wrestler2.posX + wrestler2.posZ * wrestler2.posZ);
            System.out.println("判定结果: " + (d1 < d2 ? wrestler1.name : wrestler2.name));
        }
    }

    private static void executeAction(SumoWrestler me, SumoWrestler opponent, 
                                      Dohyo dohyo, Action action, double dt) {
        switch (action.type) {
            case PUSH_FORWARD:
                double dx = opponent.posX - me.posX;
                double dz = opponent.posZ - me.posZ;
                double dist = Math.sqrt(dx * dx + dz * dz);
                if (dist > 0) {
                    double force = me.maxPushForce * action.intensity;
                    me.applyForce(dx / dist * force, dz / dist * force, dt);
                }
                break;
            case STEP_LEFT:
                me.applyForce(-me.maxPushForce * 0.3, 0, dt);
                break;
            case STEP_RIGHT:
                me.applyForce(me.maxPushForce * 0.3, 0, dt);
                break;
            case CROUCH:
                me.crouch();
                break;
            case STAND_UP:
                me.standUp();
                break;
            case RETREAT:
                double rdx = me.posX - opponent.posX;
                double rdz = me.posZ - opponent.posZ;
                double rdist = Math.sqrt(rdx * rdx + rdz * rdz);
                if (rdist > 0) {
                    double rforce = me.maxPushForce * 0.5;
                    me.applyForce(rdx / rdist * rforce, rdz / rdist * rforce, dt);
                }
                break;
        }
    }
}

算法复杂度分析

组件 时间复杂度 空间复杂度
重心计算 O(1) O(1)
稳定性判断 O(1) O(1)
作用力计算 O(1) O(1)
AI决策(贪心) O(K) O(1)
单步模拟 O(K) O(1)
完整比赛 O(N·K) O(1)

其中 K 为候选动作数,N 为模拟步数。

策略分析

相扑的核心策略

  1. 低重心原则:下蹲降低重心,扩大支撑面,是最基础也最稳定的防守姿态
  2. 中心优势:占据土俵中心位置可最大化利用场地空间
  3. 推力博弈:主动前冲可压制对手,但过度前倾会牺牲自身稳定性
  4. 体力管理:持续高强度对抗会快速消耗体力,需要把握进攻节奏

算法扩展方向

  1. 深度博弈树:使用Minimax算法预测多步对抗
  2. 强化学习:通过自我对弈训练最优策略网络
  3. 多体物理:引入更精确的刚体动力学和碰撞检测
  4. 不确定因素:模拟裁判误判、地面湿滑等随机因素

总结

本文通过Java实现了相扑对抗的核心物理模拟与推力平衡算法。关键算法包括:
– 重心稳定性模型:通过重心投影与支撑面的关系判断倾倒
– 弹性碰撞力学:模拟两人接触时的推力与反作用力
– 贪心博弈策略:评估局面并选择最优动作

相扑是研究重心控制、摩擦力学与博弈决策的经典场景,其算法思想在机器人摔跤、自动驾驶避障等领域有广泛应用。