You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实现Apollo服务器端井字棋游戏逻辑的设计模式咨询

问题

我正在学习GraphQL与Apollo,开发一款井字棋游戏。目前服务器存储所有游戏信息,客户端点击格子时会向服务器发送mutation来更新GameState类型并返回新状态。我想让服务器处理所有游戏逻辑,包括检查移动是否合法、执行电脑走棋、判断胜负,之后返回新的GameState。

我原本打算把所有逻辑都放进mutation解析器,但教程强烈建议保持解析器尽可能精简,这似乎不是最佳设计。但我想不到其他实现方式,想问下Apollo服务器通常不会这么用吗?

相关代码

Schema

const typeDefs = gql`
    type GameState {
        board: [[String!]!]!
        status: String!
        winner: String
    }
    type Mutation {
        placeO(x: Int!, y: Int!): GameState
    }
    type Query {
        "Query to get tracks array for the homepage grid"
        getGameState: GameState
    }
`;

Mutation Resolver

Mutation: {
    placeO: (_, { x, y }, { dataSources }) => {
      // check_legal_move(x, y, dataSources.state.board); ??
      dataSources.state.board[x][y] = 'O';
      // computer_move(dataSources.state.board); ??
      return dataSources.state;
    }
  },

Client Mutation Request

const PLACE_O = gql`
    mutation placeO($x: Int!, $y: Int!) {
        placeO(x: $x, y: $y) {
            board,
            status,
            winner,
        }
    }
`;
回答

Apollo服务器完全可以处理游戏逻辑,但把所有逻辑塞在解析器里确实不是最佳实践——解析器的核心职责是衔接GraphQL Schema和业务逻辑层,本身不该承载复杂业务代码,否则会导致解析器臃肿、代码难以复用和维护。

你可以把游戏逻辑抽离到专门的业务服务层,比如创建一个GameService类,把检查合法移动、电脑走棋、判断胜负这些逻辑封装成独立方法,然后在解析器里调用这个服务的方法即可。

改造示例

1. 封装游戏逻辑服务

class GameService {
  constructor(state) {
    this.state = state;
  }

  // 检查移动是否合法
  isMoveLegal(x, y) {
    return this.state.board[x][y] === null || this.state.board[x][y] === '';
  }

  // 执行玩家走棋
  makePlayerMove(x, y) {
    if (!this.isMoveLegal(x, y)) {
      throw new Error('Invalid move');
    }
    this.state.board[x][y] = 'O';
  }

  // 电脑走棋逻辑
  makeComputerMove() {
    const emptyCells = [];
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        if (this.isMoveLegal(i, j)) {
          emptyCells.push([i, j]);
        }
      }
    }
    if (emptyCells.length > 0) {
      const [x, y] = emptyCells[Math.floor(Math.random() * emptyCells.length)];
      this.state.board[x][y] = 'X';
    }
  }

  // 判断胜负
  checkWinner() {
    const board = this.state.board;
    const lines = [
      [[0,0], [0,1], [0,2]],
      [[1,0], [1,1], [1,2]],
      [[2,0], [2,1], [2,2]],
      [[0,0], [1,0], [2,0]],
      [[0,1], [1,1], [2,1]],
      [[0,2], [1,2], [2,2]],
      [[0,0], [1,1], [2,2]],
      [[0,2], [1,1], [2,0]],
    ];
    for (const line of lines) {
      const [a, b, c] = line;
      if (board[a[0]][a[1]] && board[a[0]][a[1]] === board[b[0]][b[1]] && board[a[0]][a[1]] === board[c[0]][c[1]]) {
        this.state.winner = board[a[0]][a[1]];
        this.state.status = 'Game Over';
        return;
      }
    }
    // 检查平局
    if (!board.flat().includes(null) && !board.flat().includes('')) {
      this.state.status = 'Draw';
      return;
    }
    this.state.status = 'In Progress';
  }

  // 处理完整走棋流程
  handlePlayerMove(x, y) {
    this.makePlayerMove(x, y);
    this.checkWinner();
    if (this.state.status === 'In Progress') {
      this.makeComputerMove();
      this.checkWinner();
    }
    return this.state;
  }
}

2. 简化Mutation解析器

在Apollo Server的dataSources里注入这个服务,然后在解析器里调用:

// 配置dataSources
const dataSources = () => ({
  gameService: new GameService(initialState),
});

// 改造后的Mutation解析器
Mutation: {
  placeO: (_, { x, y }, { dataSources }) => {
    return dataSources.gameService.handlePlayerMove(x, y);
  }
},

这种设计的优势很明确:

  • 解析器保持精简,只负责调用业务方法
  • 游戏逻辑被封装在独立服务中,方便测试、复用和后续扩展(比如增加多人对战模式)
  • 代码结构更清晰,符合关注点分离的原则

另外需要注意:如果你的dataSources.state是全局共享的,要考虑多用户场景下的状态隔离问题——单实例服务器会让所有用户共享同一个游戏状态,这显然不合理。可以用会话或者数据库存储每个用户的游戏状态,在GameService中读取和写入对应状态。

内容的提问来源于stack exchange,提问作者aProfessionalHelpNeeder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 06:55:36