Java棋牌游戏开发实战:斗地主与斗牛核心算法与Swing界面实现
1. 从零拆解一个Java棋牌游戏项目的整体设计思路做Java桌面棋牌游戏这件事我前前后后折腾过好几轮。最早是帮朋友的一个小工作室做内部娱乐工具后来自己也拿它当练手项目反复重构。斗地主和斗牛这两个玩法放在同一个项目里看起来只是两个独立的小游戏但真正动手之后你会发现它们共享了大量的底层能力——牌面建模、发牌逻辑、牌型判断、胜负结算、界面渲染。如果一开始不把这些公共部分抽出来代码会迅速膨胀成一团乱麻。这个项目的核心目标很明确用Java实现斗地主和斗牛两款经典棋牌游戏支持本地运行、人机对战或多人轮流操作具备完整的发牌、出牌、牌型识别、胜负判定和界面交互能力。它解决的问题其实很实际——很多初学者学完Java基础后找不到合适的练手项目做管理系统太枯燥做算法题又缺乏工程感而棋牌游戏恰好卡在一个甜点位上逻辑复杂度适中、有明确的规则约束、能练到面向对象设计、集合操作、算法判断和GUI编程。适合谁来参考如果你已经掌握了Java的基本语法能写类、会用集合、了解一点Swing或JavaFX那这个项目非常适合你。它不需要数据库、不需要网络编程、不需要框架纯Java SE就能跑起来。但如果你连面向对象的基本概念都还没理清建议先把继承、多态、接口这些过一遍再回来。我选择的技术方案是Java SE Swing。为什么不用JavaFXSwing虽然老但胜在资料多、坑少、不需要额外配置模块化依赖对于一个小型桌面游戏来说完全够用。为什么不做网络版网络版涉及Socket通信、多线程同步、断线重连等一堆问题会把项目复杂度拉高一个量级不适合作为入门到进阶的过渡项目。先把单机版做扎实后续想扩展成联机版也有清晰的路径。整个项目的架构我分成了四层牌面模型层负责定义扑克牌、牌堆、花色、点数规则引擎层负责牌型识别、大小比较、出牌合法性校验游戏流程层负责控制回合流转、发牌、叫地主、结算界面层负责渲染手牌、响应点击、显示提示信息。这四层之间通过接口解耦斗地主和斗牛各自实现自己的规则引擎但共用牌面模型和界面基础组件。注意不要一上来就写界面。我见过太多人打开IDE就开始拖按钮结果逻辑和界面缠在一起改一个规则要动五个文件。先把牌面模型和规则引擎用纯逻辑写出来用main方法测试通过之后再接界面效率会高很多。2. 牌面模型与核心数据结构的细节设计2.1 扑克牌怎么建模才合理一张扑克牌有两个核心属性花色和点数。花色用枚举表示最合适因为它的取值是固定的四种。点数从3到2共13个值但斗地主里大小顺序和斗牛不一样所以点数的比较逻辑不能写死在枚举里要交给各自的规则引擎处理。public enum Suit { SPADE, HEART, CLUB, DIAMOND } public enum Rank { THREE(3), FOUR(4), FIVE(5), SIX(6), SEVEN(7), EIGHT(8), NINE(9), TEN(10), JACK(11), QUEEN(12), KING(13), ACE(14), TWO(15), JOKER_SMALL(16), JOKER_BIG(17); private final int value; Rank(int value) { this.value value; } public int getValue() { return value; } }这里有个细节大小王没有花色但为了统一建模我给它们分配了SPADE花色在显示时特殊处理。这样做的好处是牌堆可以用统一的List 管理不需要为大小王单独开一个类型。Card类本身很简单持有Suit和Rank两个字段重写equals和hashCode方便后续比较和去重。toString方法用来调试和界面显示。public class Card implements ComparableCard { private final Suit suit; private final Rank rank; public Card(Suit suit, Rank rank) { this.suit suit; this.rank rank; } Override public int compareTo(Card other) { return Integer.compare(this.rank.getValue(), other.rank.getValue()); } Override public boolean equals(Object o) { if (this o) return true; if (!(o instanceof Card)) return false; Card card (Card) o; return suit card.suit rank card.rank; } Override public int hashCode() { return Objects.hash(suit, rank); } }2.2 牌堆的构建与洗牌算法一副牌54张斗地主用整副斗牛通常只用52张去掉大小王。牌堆的构建逻辑很直接双重循环遍历花色和点数再加上大小王。洗牌我用的是Collections.shuffle()底层是Fisher-Yates算法时间复杂度O(n)对于54张牌来说性能完全不是问题。但这里有个坑如果你在发牌过程中反复调用shuffle会导致牌序不可复现调试的时候很痛苦。我的做法是在洗牌前把随机种子固定下来调试模式下用固定种子正式运行再用随机种子。public class Deck { private final ListCard cards; public Deck(boolean includeJokers) { cards new ArrayList(); for (Suit suit : Suit.values()) { for (Rank rank : Rank.values()) { if (rank Rank.JOKER_SMALL || rank Rank.JOKER_BIG) continue; cards.add(new Card(suit, rank)); } } if (includeJokers) { cards.add(new Card(Suit.SPADE, Rank.JOKER_SMALL)); cards.add(new Card(Suit.SPADE, Rank.JOKER_BIG)); } } public void shuffle(Random random) { Collections.shuffle(cards, random); } public Card draw() { if (cards.isEmpty()) throw new IllegalStateException(牌堆已空); return cards.remove(cards.size() - 1); } }实操心得发牌时用remove(cards.size() - 1)从尾部取牌比remove(0)效率高。ArrayList头部删除需要移动所有后续元素虽然54张牌影响不大但养成好习惯没坏处。2.3 玩家手牌的排序与展示玩家拿到手牌后需要排序斗地主和斗牛的排序规则不同。斗地主按点数从大到小排方便玩家快速识别牌型斗牛通常按点数分组方便计算牛几。我的做法是在Player类里持有一个List 提供一个sort方法具体比较器由游戏规则决定。public class Player { private final String name; private final ListCard hand; public Player(String name) { this.name name; this.hand new ArrayList(); } public void sortHand(ComparatorCard comparator) { hand.sort(comparator); } public void addCard(Card card) { hand.add(card); } public ListCard getHand() { return Collections.unmodifiableList(hand); } }界面展示时手牌需要重叠排列否则17张牌横着放屏幕根本放不下。我的做法是计算每张牌的偏移量让它们部分重叠鼠标悬停时再弹起来。这个交互细节后面在界面章节会详细说。3. 斗地主规则引擎的完整实现3.1 牌型识别的核心算法斗地主的牌型有十几种单张、对子、三张、三带一、三带二、顺子、连对、飞机、炸弹、火箭、四带二等等。识别牌型的思路是先统计每种点数出现的次数然后根据次数分布来判断。public class DouDizhuRuleEngine { public CardType recognize(ListCard cards) { if (cards null || cards.isEmpty()) return CardType.INVALID; MapRank, Integer countMap new TreeMap(); for (Card card : cards) { countMap.merge(card.getRank(), 1, Integer::sum); } int size cards.size(); ListInteger counts new ArrayList(countMap.values()); Collections.sort(counts, Collections.reverseOrder()); // 火箭大小王 if (size 2 countMap.containsKey(Rank.JOKER_SMALL) countMap.containsKey(Rank.JOKER_BIG)) { return CardType.ROCKET; } // 炸弹四张同点数 if (size 4 counts.get(0) 4) { return CardType.BOMB; } // 单张 if (size 1) return CardType.SINGLE; // 对子 if (size 2 counts.get(0) 2) return CardType.PAIR; // 三张 if (size 3 counts.get(0) 3) return CardType.TRIPLE; // 三带一 if (size 4 counts.get(0) 3 counts.get(1) 1) { return CardType.TRIPLE_WITH_SINGLE; } // 三带二 if (size 5 counts.get(0) 3 counts.get(1) 2) { return CardType.TRIPLE_WITH_PAIR; } // 顺子至少5张连续单牌 if (size 5 isConsecutive(countMap, 1)) { return CardType.STRAIGHT; } // 连对至少3对连续 if (size 6 size % 2 0 isConsecutive(countMap, 2)) { return CardType.CONSECUTIVE_PAIRS; } // 飞机、飞机带翅膀等复杂牌型... // 此处省略部分分支完整实现需要处理所有合法组合 return CardType.INVALID; } private boolean isConsecutive(MapRank, Integer countMap, int expectedCount) { ListRank ranks new ArrayList(countMap.keySet()); // 排除2和大小王它们不能参与顺子 for (Rank rank : ranks) { if (rank.getValue() Rank.TWO.getValue()) return false; if (countMap.get(rank) ! expectedCount) return false; } Collections.sort(ranks); for (int i 1; i ranks.size(); i) { if (ranks.get(i).getValue() - ranks.get(i - 1).getValue() ! 1) { return false; } } return true; } }这段代码的关键在于countMap的构建和counts的排序。用TreeMap而不是HashMap是为了让点数有序方便后续判断顺子。counts降序排列后counts.get(0)就是出现次数最多的那个点数很多牌型判断都依赖这个。注意顺子判断里必须排除2和大小王。斗地主规则中2不能参与顺子这是新手最容易写错的地方。我当初调试的时候发现3-4-5-6-7能识别但10-J-Q-K-A也能识别唯独A-2-3-4-5不行查了半天才发现是2的问题。3.2 牌型大小比较的逻辑牌型识别出来之后比较大小是另一个核心问题。规则是同类型牌型才能比较炸弹和火箭可以压任何非炸弹牌型大炸弹压小炸弹。public boolean canBeat(ListCard current, ListCard previous) { if (previous null || previous.isEmpty()) return true; CardType currentType recognize(current); CardType previousType recognize(previous); if (currentType CardType.INVALID) return false; // 火箭最大 if (currentType CardType.ROCKET) return true; if (previousType CardType.ROCKET) return false; // 炸弹比较 if (currentType CardType.BOMB previousType ! CardType.BOMB) return true; if (currentType ! CardType.BOMB previousType CardType.BOMB) return false; if (currentType CardType.BOMB previousType CardType.BOMB) { return getMaxRank(current) getMaxRank(previous); } // 同类型比较 if (currentType ! previousType) return false; if (current.size() ! previous.size()) return false; return getMaxRank(current) getMaxRank(previous); } private Rank getMaxRank(ListCard cards) { return cards.stream() .map(Card::getRank) .max(Comparator.comparingInt(Rank::getValue)) .orElseThrow(); }这里有个容易忽略的点顺子和连对的比较不仅要看最大牌还要看长度。比如3-4-5-6-7和4-5-6-7-8-9后者长度更长不能互相比较。我在canBeat里加了size判断确保只有长度相同的同类型牌才能比较。3.3 叫地主与回合流转叫地主的逻辑相对简单三个玩家轮流叫分叫分最高的当地主如果都不叫则重新发牌。我实现了一个简单的状态机来管理回合流转。public enum GamePhase { DEALING, BIDDING, PLAYING, GAME_OVER } public class DouDizhuGame { private GamePhase phase; private ListPlayer players; private Player landlord; private Player currentPlayer; private ListCard lastPlayed; private Player lastPlayer; public void nextTurn() { int currentIndex players.indexOf(currentPlayer); currentPlayer players.get((currentIndex 1) % players.size()); // 如果轮回到上次出牌的玩家说明其他人都不要可以重新出牌 if (currentPlayer lastPlayer) { lastPlayed null; } } public boolean playCards(Player player, ListCard cards) { if (player ! currentPlayer) return false; if (!player.getHand().containsAll(cards)) return false; if (!ruleEngine.canBeat(cards, lastPlayed)) return false; player.getHand().removeAll(cards); lastPlayed cards; lastPlayer player; if (player.getHand().isEmpty()) { phase GamePhase.GAME_OVER; } else { nextTurn(); } return true; } }nextTurn里的逻辑是斗地主的核心回合机制如果下一个玩家就是上次出牌的玩家说明中间所有人都选择了不要此时清空lastPlayed让该玩家自由出牌。这个判断条件看似简单但如果没有处理好会出现玩家无法出牌或者可以无限压牌的bug。4. 斗牛游戏的规则实现与特殊处理4.1 斗牛的点数计算与牛几判定斗牛和斗地主的规则差异很大。斗牛每人5张牌需要从中选出3张牌使它们的点数之和为10的倍数剩下的2张牌点数之和的个位数就是牛几。如果剩下2张之和也是10的倍数那就是牛牛最大的牌型。点数计算有个特殊规则J、Q、K都算10点A算1点其他牌按面值算。这和斗地主的点数体系完全不同所以斗牛需要自己的点数映射。public class NiuNiuRuleEngine { public int getPoint(Card card) { Rank rank card.getRank(); if (rank Rank.JOKER_SMALL || rank Rank.JOKER_BIG) { return 0; // 斗牛通常不用大小王 } if (rank.getValue() Rank.TEN.getValue()) { return 10; } return rank.getValue(); } public NiuNiuType calculateNiu(ListCard cards) { if (cards.size() ! 5) return NiuNiuType.INVALID; int[] points cards.stream().mapToInt(this::getPoint).toArray(); // 枚举所有3张牌的组合 for (int i 0; i 5; i) { for (int j i 1; j 5; j) { for (int k j 1; k 5; k) { int sum3 points[i] points[j] points[k]; if (sum3 % 10 ! 0) continue; // 剩下2张牌 int sum2 0; for (int m 0; m 5; m) { if (m ! i m ! j m ! k) { sum2 points[m]; } } int niu sum2 % 10; if (niu 0) return NiuNiuType.NIU_NIU; return NiuNiuType.values()[niu]; // NIU_1 到 NIU_9 } } } return NiuNiuType.NO_NIU; } }这段代码用三重循环枚举所有3张牌的组合虽然看起来有点笨但5张牌总共只有C(5,3)10种组合性能完全没问题。找到第一组满足条件的3张牌后计算剩下2张的和即可。实操心得斗牛里有个容易搞混的地方——如果存在多组3张牌之和为10的倍数应该取哪种结果标准规则是取最大的牛几。比如同时能找到牛5和牛8的组合应该算牛8。我最初的实现找到第一组就返回了导致结果不稳定。后来改成遍历所有组合记录最大的牛几才符合规则。4.2 特殊牌型的倍数计算斗牛里有些特殊牌型会翻倍牛牛通常3倍五花牛全是JQK4倍五小牛所有牌点数都小于5且总和不超过105倍。这些倍数规则需要在结算时处理。public int getMultiplier(ListCard cards, NiuNiuType type) { if (type NiuNiuType.NIU_NIU) return 3; boolean allFace cards.stream().allMatch(c - c.getRank().getValue() Rank.JACK.getValue() c.getRank().getValue() Rank.KING.getValue()); if (allFace) return 4; int total cards.stream().mapToInt(this::getPoint).sum(); boolean allSmall cards.stream().allMatch(c - getPoint(c) 5); if (allSmall total 10) return 5; return 1; }五花牛的判断要注意大小王不算JQK如果牌堆里包含大小王需要先排除。五小牛的判断是每张牌点数小于5且总和不超过10这两个条件缺一不可。4.3 多人比牌与庄家轮转斗牛通常是多人游戏有一个庄家其他玩家和庄家比牌。比牌规则是先比牛几牛几大的赢牛几相同比最大单张牌最大单张相同比花色。庄家轮转可以用简单的取模运算实现。public class NiuNiuGame { private ListPlayer players; private int bankerIndex; public void settle() { Player banker players.get(bankerIndex); NiuNiuType bankerType ruleEngine.calculateNiu(banker.getHand()); for (Player player : players) { if (player banker) continue; NiuNiuType playerType ruleEngine.calculateNiu(player.getHand()); int result compareNiu(playerType, player.getHand(), bankerType, banker.getHand()); if (result 0) { // 闲家赢 int multiplier ruleEngine.getMultiplier(player.getHand(), playerType); transferChips(banker, player, multiplier); } else if (result 0) { // 庄家赢 int multiplier ruleEngine.getMultiplier(banker.getHand(), bankerType); transferChips(player, banker, multiplier); } // result 0 平局不转移筹码 } bankerIndex (bankerIndex 1) % players.size(); } }比牌时的花色比较顺序通常是黑桃红桃梅花方块这个顺序没有统一标准可以根据实际需求调整。我在实现时把它做成了可配置的方便不同地区的玩法差异。5. 界面交互与用户体验的实操细节5.1 手牌渲染与鼠标交互Swing里渲染手牌我用的自定义JPanel加重写paintComponent。每张牌画成一个圆角矩形中间显示点数和花色。手牌重叠排列鼠标悬停时该牌上移。public class HandPanel extends JPanel { private ListCard cards; private int hoverIndex -1; private static final int CARD_WIDTH 80; private static final int CARD_HEIGHT 110; private static final int OVERLAP 30; public HandPanel() { addMouseMotionListener(new MouseAdapter() { Override public void mouseMoved(MouseEvent e) { int index getCardIndexAt(e.getX(), e.getY()); if (index ! hoverIndex) { hoverIndex index; repaint(); } } }); } private int getCardIndexAt(int x, int y) { for (int i cards.size() - 1; i 0; i--) { int cardX i * OVERLAP; if (x cardX x cardX CARD_WIDTH y 0 y CARD_HEIGHT) { return i; } } return -1; } Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2d (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); for (int i 0; i cards.size(); i) { int x i * OVERLAP; int y (i hoverIndex) ? -15 : 0; drawCard(g2d, cards.get(i), x, y); } } }getCardIndexAt从后往前遍历因为后面的牌覆盖在前面的牌上面点击时应该优先选中视觉上在最上层的牌。这个细节如果不注意会出现点击响应和视觉不一致的问题。5.2 出牌区域的动画与提示出牌后牌需要从手牌区域飞到出牌区域。我用了一个简单的Timer动画每帧更新牌的位置到达目标位置后停止。public void animatePlay(ListCard cards, Point from, Point to) { Timer timer new Timer(16, null); final int totalFrames 15; final int[] frame {0}; timer.addActionListener(e - { frame[0]; float progress (float) frame[0] / totalFrames; float eased 1 - (1 - progress) * (1 - progress); // ease-out int currentX (int) (from.x (to.x - from.x) * eased); int currentY (int) (from.y (to.y - from.y) * eased); updateCardPosition(cards, currentX, currentY); repaint(); if (frame[0] totalFrames) { timer.stop(); onAnimationComplete(cards); } }); timer.start(); }16毫秒一帧大约是60FPS15帧总共250毫秒左右动画不会太慢也不会太快。ease-out的缓动函数让动画结尾减速看起来更自然。注意Swing的Timer回调在EDT线程上执行不要在回调里做耗时操作否则界面会卡顿。如果需要在动画结束后执行复杂逻辑用SwingUtilities.invokeLater包一层。5.3 界面布局与适配整个游戏界面用BorderLayout分成几个区域中间是出牌区底部是玩家手牌区左右和上方是其他玩家信息区。窗口大小变化时需要重新计算各区域的位置和尺寸。public class GameFrame extends JFrame { private CardLayout cardLayout; private JPanel mainPanel; public GameFrame() { setTitle(棋牌游戏); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(1024, 768); setLocationRelativeTo(null); cardLayout new CardLayout(); mainPanel new JPanel(cardLayout); mainPanel.add(createMenuPanel(), menu); mainPanel.add(createDouDizhuPanel(), doudizhu); mainPanel.add(createNiuNiuPanel(), niuniu); add(mainPanel); cardLayout.show(mainPanel, menu); } }用CardLayout管理不同界面之间的切换比手动setVisible方便得多。菜单界面选择玩法点击后切换到对应的游戏面板。6. 常见问题与排查技巧实录6.1 牌型识别错误的排查思路牌型识别出错是最常见的问题。我的排查步骤是先打印出countMap的内容确认每种点数出现的次数是否正确再检查counts排序后的顺序是否符合预期最后逐个分支检查判断条件。问题现象可能原因排查方法顺子识别失败包含2或大小王检查isConsecutive里是否排除了2和大小王三带一识别成炸弹counts判断顺序错误先判断炸弹再判断三带一连对识别失败未检查每对数量是否为2确认countMap中每个值都等于2飞机识别失败未处理带翅膀的情况分别判断飞机不带、带单、带对6.2 界面卡顿与闪烁的优化Swing界面卡顿通常有两个原因在EDT线程里做了耗时操作或者频繁repaint导致重绘风暴。我的优化经验是所有游戏逻辑放在独立线程里计算计算完成后用SwingUtilities.invokeLater更新界面重绘时只repaint需要更新的区域而不是整个面板。// 错误做法在EDT里做耗时计算 button.addActionListener(e - { ListCard aiCards aiPlayer.calculateBestMove(); // 可能耗时 updateUI(aiCards); }); // 正确做法后台线程计算EDT更新界面 button.addActionListener(e - { new Thread(() - { ListCard aiCards aiPlayer.calculateBestMove(); SwingUtilities.invokeLater(() - updateUI(aiCards)); }).start(); });实操心得开启双缓冲可以显著减少闪烁。在JPanel的构造函数里调用setDoubleBuffered(true)Swing会自动在内存里先画好再一次性输出到屏幕。这个设置对于频繁重绘的棋牌游戏界面效果很明显。6.3 AI出牌逻辑的简化实现如果不想写复杂的AI可以用简单的贪心策略优先出最小的单张或对子能压就压不能压就过。虽然不聪明但至少能跑起来。public ListCard simpleAI(ListCard hand, ListCard lastPlayed) { if (lastPlayed null || lastPlayed.isEmpty()) { // 自由出牌出最小的单张 Card smallest hand.stream() .min(Comparator.comparingInt(c - c.getRank().getValue())) .orElseThrow(); return Collections.singletonList(smallest); } // 尝试找能压过的牌 CardType lastType ruleEngine.recognize(lastPlayed); for (Card card : hand) { ListCard candidate Collections.singletonList(card); if (ruleEngine.canBeat(candidate, lastPlayed)) { return candidate; } } return Collections.emptyList(); // 过牌 }这个AI只会在单张层面做判断不会拆牌也不会配合队友但作为占位实现完全够用。后续想提升AI水平可以引入牌型拆分、手牌评估、蒙特卡洛搜索等策略。6.4 游戏状态保存与恢复单机游戏也需要保存进度比如玩家中途退出后下次继续。我用Java序列化把游戏状态写到文件里简单粗暴但有效。public class GameState implements Serializable { private static final long serialVersionUID 1L; private ListCard deck; private ListPlayer players; private GamePhase phase; private int currentPlayerIndex; // ... 其他状态字段 } public void saveGame(GameState state, String filePath) throws IOException { try (ObjectOutputStream oos new ObjectOutputStream( new FileOutputStream(filePath))) { oos.writeObject(state); } } public GameState loadGame(String filePath) throws IOException, ClassNotFoundException { try (ObjectInputStream ois new ObjectInputStream( new FileInputStream(filePath))) { return (GameState) ois.readObject(); } }序列化时要注意所有被序列化的类都必须实现Serializable接口包括Card、Player、GameState。如果某个字段不需要序列化用transient修饰。7. 项目扩展方向与个人实操体会这个项目做完基础版本后有很多可以继续深挖的方向。比如把斗地主的AI从贪心策略升级成基于手牌评估的搜索算法让电脑对手更有挑战性比如给斗牛增加下注和筹码系统模拟真实的赌桌体验比如把界面从Swing换成JavaFX获得更现代的视觉效果和更好的动画支持。我自己在实际操作中的体会是棋牌游戏项目最大的价值不在于最终做出来的成品有多完善而在于过程中被迫解决的每一个具体问题。牌型识别逼着你把集合操作和条件判断练到熟练回合流转逼着你理解状态机的设计界面交互逼着你处理事件驱动编程的各种边界情况。这些东西看教程是学不会的只有自己一行一行写出来、一个bug一个bug调过去才能真正变成自己的东西。最后再分享一个小技巧写规则引擎的时候把每个牌型的判断逻辑单独写成一个方法用单元测试覆盖所有边界情况。比如顺子的测试用例要包含最短顺子、最长顺子、包含2的非法顺子、包含大小王的非法顺子。测试通过之后再集成到游戏流程里能省掉大量调试时间。我当初就是靠这套测试用例在重构规则引擎时保证了行为一致性没有引入回归bug。

相关新闻

PLC与组态软件:智能停车场收费系统的核心架构与实战

PLC与组态软件:智能停车场收费系统的核心架构与实战

道闸抬杆的那一刹那,大多数车主都不会去想,真正控制这根闸杆的并不是旁边那台标着“AI车牌识别”的摄像头,而是一台装在灰色铁箱里、看起来毫不起眼的PLC。我接触停车场收费系统好几年,从最早跟着前辈布线、递工具开始&#xff0c…

2026/10/10 4:46:19 阅读更多 →
AnyPS5:PS5手柄安卓兼容驱动技术解析

AnyPS5:PS5手柄安卓兼容驱动技术解析

我无法基于当前输入生成符合要求的博文。原因如下:输入中仅提供了项目标题"AnyPS5",但未提供任何实质性的【项目正文】、【关键词】或【摘要描述】;所谓“相关热搜词”和“最新网络热词”字段为空,无实际内容可供分析&a…

2026/10/10 4:46:19 阅读更多 →
110kV线路三段式相间距离保护Simulink仿真建模与整定计算全解析

110kV线路三段式相间距离保护Simulink仿真建模与整定计算全解析

作为一名在电力系统继电保护方向摸爬滚打了多年的工程师,我深知光看课本上的原理图是不够的。当你真正坐在电脑前,面对一个需要验证保护逻辑、整定配合和动作行为的实际项目时,你会发现在Simulink里搭一个60公里长的110kV线路模型&#xff0c…

2026/10/10 4:46:19 阅读更多 →

最新新闻

Spring AI 实战:从配置到对话,ChatClient 链式调用与上下文管理

Spring AI 实战:从配置到对话,ChatClient 链式调用与上下文管理

1. 从配置文件到对话窗口:Spring AI 到底简化了什么第一次接触 Spring AI 的时候,我脑子里其实带着一个很具体的疑问:过去在 Java 项目里接一个大模型对话能力,光是 HTTP 客户端封装、请求体拼装、响应解析、异常重试这些杂活&…

2026/10/10 5:17:30 阅读更多 →
如何安全管理 OpenFlux 共享密钥:传输、存储与轮换实战指南

如何安全管理 OpenFlux 共享密钥:传输、存储与轮换实战指南

如何安全管理 OpenFlux 共享密钥:传输、存储与轮换实战指南 OpenFlux 是一款网络栈研究工具,通过可插拔的传输层构建 TCP 隧道。当启用传输加密时,客户端与出口节点共用的**共享密钥(shared secret)**就是整条隧道的安…

2026/10/10 5:17:30 阅读更多 →
Ant Design Blazor Affix 滚动容器实战:用 TargetSelector 将固钉绑定到指定滚动元素

Ant Design Blazor Affix 滚动容器实战:用 TargetSelector 将固钉绑定到指定滚动元素

前端UI组件设计系统 【免费下载链接】ant-design-blazor 基于 Ant Design 与 Blazor 的前端组件库。让开发者解放生产力,实现更大价值。 项目地址: https://gitcode.com/ant-design-blazor/ant-design-blazor 点击查看 免费下载 本篇指南围绕 Ant Desig…

2026/10/10 5:17:30 阅读更多 →
x64dbg 调试器插件开发指南:深入解析 DbgScriptBpToggle 脚本断点切换 API 及其完整调用链

x64dbg 调试器插件开发指南:深入解析 DbgScriptBpToggle 脚本断点切换 API 及其完整调用链

逆向工程调试器开发工具应用安全 【免费下载链接】x64dbg An open-source user mode debugger for Windows. Optimized for reverse engineering and malware analysis. 项目地址: https://gitcode.com/gh_mirrors/x6/x64dbg 点击查看 免费下载 导读 DbgScriptBpT…

2026/10/10 5:17:30 阅读更多 →
LogicStack-LeetCode 题解:813. 最大平均值和的分组——「序列 DP + 前缀和」求连续段平均值之和最大值

LogicStack-LeetCode 题解:813. 最大平均值和的分组——「序列 DP + 前缀和」求连续段平均值之和最大值

教程文档 【免费下载链接】LogicStack-LeetCode 公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码 项目地址: https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode 点击查看 免费下载 导读 本篇以「宫水三叶的刷题日记」系列仓库(LogicSta…

2026/10/10 5:17:30 阅读更多 →
GPS天线设计 GNSS天线设计建议

GPS天线设计 GNSS天线设计建议

GPS天线设计 GNSS天线设计建议 天线作为导航定位设备中最重要的接收器件,它起到的作用就像是人的“耳朵”;是将卫星发送下来的电磁波能量变换成电子器件可解析的电流。因此天线的性能好坏将直接关系到GPS整机的产品性能。目前GNSS系统开放民用定位系统主要是美国GPS…

2026/10/10 5:16:30 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →