精华内容
下载资源
问答
  • 2018-05-23 23:40:58

    这个阶段就要结束了,现在我们要结束游戏了,按常规操作来说,当外星人撞到飞船或是屏幕底端时便结束游戏,更进一步,我们会给玩家三次机会,对,就像小时候玩的游戏一样我们有三条命,ok,开始操作:
    首先我们要创建一个新类,他负责跟踪游戏统计信息,先看一下:
    class GameSta():
        def __init__(self,ai_settings):
            self.ai_settings=ai_settings
            self.reset_sta()
            self.game_active=True
        def reset_sta(self):
            self.ship_counts=self.ai_settings.ship_limit  #将初始化时的飞船数量设置为自定义的最大限制

    该类位于新文件game_sta.py中,用于初始化统计信息,ship_limit的值可以在settings.py中设置:
     self.ship_limit=3 #玩家最初拥有的飞船数量

    在主程序中创建GameSta实例,不要忘了导入他:
    from game_sta import GameSta
     sta=GameSta(ai_settings)
     gf.update_aliens(ai_settings,aliens,screen,ship,bullets,sta)

    很明显第三条代码是在主循环中的,我们在后边还要进行设置。
    在game_functions.py中我们要新建两个函数来处理外星人撞击到飞船和屏幕底端的措施:
    def ship_col(ai_settings,aliens,screen,ship,bullets,sta):
      if sta.ship_counts>0:  
        sta.ship_counts-=1  #每次检测到撞击飞船数量减少1
        aliens.empty()
        bullets.empty()  #清空外星人和当前的子弹
        creat_aliens(ai_settings,screen,aliens,ship)  #创建一批新的外星人
        ship.ship_center()  #将飞船重新放到屏幕中央
        sleep(1)  #睡眠1秒钟
      else:
          sta.game_active=False
    def check_aliens_bottom(ai_settings,aliens,screen,ship,bullets,sta):#检测飞船是否撞到屏幕底部
        for alien in aliens.sprites():
            if alien.rect.bottom>=screen.get_rect().bottom:#检测外星人和屏幕的bottom属性
               ship_col(ai_settings,aliens,screen,ship,bullets,sta) 
               break  #有一个外星人撞到底部便结束本轮游戏
                    
    def update_aliens(ai_settings,aliens,screen,ship,bullets,sta): #更新编组中所有外星人的位置,将自动对每个外星人调用方法update()
        check_aliens_edge(ai_settings,aliens)
        aliens.update() 
        if pygame.sprite.spritecollideany(ship,aliens): #如果飞船和外星人发生碰撞
            ship_col(ai_settings,aliens,screen,ship,bullets,sta)
        check_aliens_bottom(ai_settings,aliens,screen,ship,bullets,sta) #检测外星人是否撞击到了屏幕底部    

    ok,为了方便我们把最终的操作也加上了,第一个函数是处理外星人撞击到飞船的,如果飞船的数量大于零就执行一系列合理操作,否则将属性game_active置为False,(在类GameSta中我们初始化时将game_active设置为True)该属性后边马上讲到……这里有个ship.ship_center(),这是我们自定义的,在ship.py中我们创建此方法:
    def ship_center(self):
            self.center=self.screen_rect.centerx #将飞船的center设置为屏幕中心的x坐标
    我们还用到了sleep(),这个需要导入:from time import sleep
    第二个函数是处理外星人撞击到屏幕底端操作的,用到了bottom属性,然后执行内容和飞船一样~~

    第三个函数的第四行用到了spritecollideany(),他检测两个参数是否发生碰撞,若发生则返回True,然后执行ship_col操作。
    最后更新一下主程序:
     while True:
            gf.check_events(ship,ai_settings,screen,bullets) #更改事件监听函数
            if sta.game_active:
                ship.update()  #每次循环都访问该函数,保证飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新
                gf.update_bullets(ai_settings,screen,ship,bullets,aliens)  
                gf.update_aliens(ai_settings,aliens,screen,ship,bullets,sta)
            gf.update_screen(ai_settings,screen,ship,bullets,aliens)      

    这是主循环,我们通过属性game_active来保证当飞船用完后就不更新飞船外星人以及子弹,也就是进入静止状态,效果是这样的:
    对,当外星人撞到飞船时就静止了~~~暂时就是这些,下一步就要开始完善其功能了……
    更多相关内容
  • //游戏结束之后,给外包发送一个结束消息 gameCommonService.sendFinishToClient(game); locker.unlock(); } log.debug("game {} is finished.", gameId); } } } 代码如下: @Transactional(rollbackFor = ...

    废弃原因:使用redis缓存失效监听会有一定的延时,dev环境下延时已经达到90s左右,线上可能更甚,所以必须更换方案。

    (基本上,expired事件是在Redis服务器删除键的时候生成的,而不是在理论上生存时间达到零值时生成的。)

    可参考文章:

    https://blog.csdn.net/a13935302660/article/details/121285975

    http://www.redis.cn/topics/notifications.html 

    技术选型

    1.定时任务。-》因为游戏的开始时间和结束时间不确定,所以定时任务不可以用。

    2. 消息中间件。-》公司目前使用的消息中间件是rocketmq,在rocketmq官网找到rocketmq目前仅支持指定时间片轮转。所以也不能使用mq实现定时功能。

    3. redis缓存失效监听。-》利用redis提供的特性,key失效之后可以通知客户端对应的失效key值,将对应的信息放入key,对key进行过滤,实现自动开始\结束游戏。(已废弃)

    流程图

    代码

    redis监听过期key的配置如下:

    @Configuration
    public class RedisListenerConfig {
    
        @Bean
        RedisMessageListenerContainer container(@Qualifier("getJedisConnectionFactory") RedisConnectionFactory connectionFactory) {
            RedisMessageListenerContainer container = new RedisMessageListenerContainer();
            container.setConnectionFactory(connectionFactory);
            return container;
        }
    }

    对应redis监听key过期的处理器逻辑如下:

    /**
     * redis key过期监听器
     *
     * @author yangjh
     **/
    @Slf4j
    @Component
    public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
    
        private final LockService lockService;
        private final GmGameService gameService;
        private final GameCommonService gameCommonService;
    
        public static final String SEPARATOR_CHARS = ":";
    
        public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer, LockService lockService,
                GmGameService gameService, GameCommonService gameCommonService) {
            super(listenerContainer);
            this.lockService = lockService;
            this.gameService = gameService;
            this.gameCommonService = gameCommonService;
        }
    
        /**
         * 针对 redis 数据失效事件,进行数据处理
         * @param message 失效的key
         * @param pattern 过滤的表达式
         */
        @Override
        public void onMessage(Message message, byte[] pattern) {
            // 获取到失效的 key,进行取消订单业务处理
            String expiredKey = message.toString();
            if (expiredKey.startsWith(GAME_START_PREFIX)) {
                //到达游戏开始时间了,更新一波游戏状态
                String[] keyArr = StringUtils.split(expiredKey, SEPARATOR_CHARS);
                long gameId = Long.parseLong(keyArr[keyArr.length-1]);
                log.debug("game {} is starting.", gameId);
                try (RLockSupport locker = RLockSupport.ofLocker(lockService)){
                    locker.acquire(String.format(GAME_START_LOCK, gameId), INT_5, TimeUnit.SECONDS);
                    GmGame game = gameService.getById(gameId);
                    game.setUpdateTime(LocalDateTime.now());
                    game.setUpdator(StringUtils.EMPTY);
                    game.setGameStatus(GameStatusEnum.STARTED.getStatus());
                    gameService.updateById(game);
    
                    locker.unlock();
                }
                log.debug("game {} is started.", gameId);
            } else if (expiredKey.startsWith(GAME_FINISH_PREFIX)) {
                //到达游戏开始时间了,更新一波游戏状态
                String[] keyArr = StringUtils.split(expiredKey, SEPARATOR_CHARS);
                long gameId = Long.parseLong(keyArr[keyArr.length-1]);
                log.debug("game {} is finishing.", gameId);
                try (RLockSupport locker = RLockSupport.ofLocker(lockService)){
                    locker.acquire(String.format(GAME_AUTO_FINISH_LOCK, gameId), INT_5, TimeUnit.SECONDS);
                    GmGame game = gameService.getById(gameId);
                    game.setUpdateTime(LocalDateTime.now());
                    game.setUpdator(StringUtils.EMPTY);
                    game.setGameStatus(GameStatusEnum.FINISHED.getStatus());
                    gameService.updateById(game);
    
                    //游戏结束之后,给外包发送一个结束消息
                    gameCommonService.sendFinishToClient(game);
    
                    locker.unlock();
                }
                log.debug("game {} is finished.", gameId);
            }
        }
    
    }

    代码如下:

    @Transactional(rollbackFor = Exception.class)
    public Long publishGame(Long gameId) {
        GmGame game = getById(gameId);
    
        //只有未发布状态的游戏才可以发布
        gameCommonService.checkGamePublishStatus(game);
    
        //判断游戏下面是否有题目以及成员,如果没有题目和成员,也不可以发布
        Integer userCount = gameUserMapper.selectCount(
                new QueryWrapper<GmGameUser>().lambda().eq(GmGameUser::getDeleted, NOT_DELETED)
                        .eq(GmGameUser::getGameId, gameId));
        Validate.isTrue(userCount > INT_0, GmcenterExceptionKeys.APIS_GAME_PUBLISH_USER_NOT_EMPTY);
    
        Integer subjectCount = gameSubjectMapper.selectCount(
                new QueryWrapper<GmGameSubject>().lambda().eq(GmGameSubject::getDeleted, NOT_DELETED)
                        .eq(GmGameSubject::getGameId, gameId));
        Validate.isTrue(subjectCount > INT_0, GmcenterExceptionKeys.APIS_GAME_PUBLISH_SUBJECT_NOT_EMPTY);
    
        getAndSetStatus(game);
        gameCommonService.setUpdateField(game);
    
        updateById(game);
    
        setRedisClock(game);
    
        return game.getId();
    }
    
    /**
     * 设置redis定时开始/结束游戏
     *
     * @param game 游戏
     */
    private void setRedisClock(GmGame game) {
        Long gameId = game.getId();
        int gameStatus = game.getGameStatus();
        LocalDateTime endTime = game.getEndTime();
        LocalDateTime startTime = game.getStartTime();
    
        LocalDateTime now = LocalDateTime.now();
        long startSecond = LocalDateTimeUtil.between(now, startTime, ChronoUnit.SECONDS);
        long endSecond = LocalDateTimeUtil.between(now, endTime, ChronoUnit.SECONDS);
    
        if (UN_START.getStatus().equals(gameStatus)) {
            // case1:未开始状态->设置开始时间redis->判断endTime是否设置->设置结束redis
            redissonClient.getBucket(GAME_START_PREFIX + gameId)
                    .set(gameId.toString(), Math.abs(startSecond), TimeUnit.SECONDS);
            if (!DateUtil.isDefaultDateTime(endTime)) {
                redissonClient.getBucket(GAME_FINISH_PREFIX + gameId)
                        .set(gameId.toString(), Math.abs(endSecond), TimeUnit.SECONDS);
            }
        } else if (STARTED.getStatus().equals(gameStatus) && !DateUtil.isDefaultDateTime(endTime)) {
            // case2:开始状态->那就判断结束时间是否设置了->如果设置了就设置redis,没设置,就什么都不用设置了
            redissonClient.getBucket(GAME_FINISH_PREFIX + gameId)
                    .set(gameId.toString(), Math.abs(endSecond), TimeUnit.SECONDS);
        }
        // case3:已结束状态->nothing(发布即结束)
    }
    
    /**
     * 获取当前游戏状态,并根据开始结束时间设置发布之后的游戏状态
     *
     * @param game db中的游戏
     */
    private void getAndSetStatus(GmGame game) {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime startTime = game.getStartTime();
        LocalDateTime endTime = game.getEndTime();
    
        //now < startTime
        if (now.compareTo(startTime) < INT_0) {
            //当前时间小于开始时间,说明游戏还没开始
            game.setGameStatus(UN_START.getStatus());
            return;
        }
        //当前时间>=开始时间,标识游戏已经开始了,直接修改游戏状态为已开始
        game.setGameStatus(STARTED.getStatus());
        //当前时间>=开始时间 && 当前时间>=结束时间,直接修改游戏状态为已结束
        if (!DateUtil.isDefaultDateTime(endTime) && now.compareTo(endTime) >= INT_0) {
            game.setGameStatus(FINISHED.getStatus());
        }
    }

    简单逻辑是:

    游戏发布的时候,会判断是否到达开始时间和结束时间了:

    switch 开始时间:

       case 未到达:设置ttl为当前时间到开始时间的缓存

       case  到达:设置游戏状态为已开始,并判断是否到达结束时间

    switch 结束时间:

        case  未到达:设置ttl为当前时间到开始时间的缓存

        case   到达:设置游戏状态为已结束

    另外需要在:游戏取消、游戏结束(手动)、游戏下所有人员都已完成游戏  之后取消相对应的缓存

    展开全文
  • 技术选型 使用分布式延时队列来控制游戏的开始或结束 redisson 延时队列使用参考: https://www.javadoc.io/doc/org.redisson/redisson/latest/org/redisson/api/RDelayedQueue.html ...

    技术选型

    使用分布式延时队列来控制游戏的开始或结束

    redisson 延时队列使用参考:

    https://www.javadoc.io/doc/org.redisson/redisson/latest/org/redisson/api/RDelayedQueue.html

    https://github.com/redisson/redisson/wiki/7.-Distributed-collections

    流程图

    代码

    发布游戏的时候,向延时队列里面放入对应的延时key

    controller层代码:

    @ApiOperation("发布游戏")
    @PostMapping(value = "publish")
    @Auth(value = GmcenterConstants.LOG_OBJ_GAME, action = Constants.LOG_TYPE_UPDATESINGLE, type = AuthType.TOKEN)
    @NonDuplicateRequest(value = "T(com.yxt.gmcenter.app.common.GmcenterRedisKeys).GAME_PUBLISH_LOCK + #currentUserId + ':' + #gameId", fetchCurrentUser = true)
    public Long gamePublish(Long gameId) {
        log.debug("GameCenterController#gamePublish params: {}", gameId);
        Validate.isNotNull(gameId, GmcenterExceptionKeys.APIS_GAME_STATUS_NOT_FOUND);
    
        return gameService.publishGame(gameId);
    }

    service层代码:

    @Transactional(rollbackFor = Exception.class)
    public Long publishGame(Long gameId) {
        GmGame game = getById(gameId);
    
        //只有未发布状态的游戏才可以发布
        gameCommonService.checkGamePublishStatus(game);
    
        Integer subjectCount = gameSubjectMapper.selectCount(
                new QueryWrapper<GmGameSubject>().lambda().eq(GmGameSubject::getDeleted, NOT_DELETED)
                        .eq(GmGameSubject::getGameId, gameId));
        Validate.isTrue(subjectCount > INT_0, GmcenterExceptionKeys.APIS_GAME_PUBLISH_SUBJECT_NOT_EMPTY);
    
        //判断游戏下面是否有题目以及成员,如果没有题目和成员,也不可以发布
        Integer userCount = gameUserMapper.selectCount(
                new QueryWrapper<GmGameUser>().lambda().eq(GmGameUser::getDeleted, NOT_DELETED)
                        .eq(GmGameUser::getGameId, gameId));
        Validate.isTrue(userCount > INT_0, GmcenterExceptionKeys.APIS_GAME_PUBLISH_USER_NOT_EMPTY);
    
        getAndSetStatus(game);
        gameCommonService.setUpdateField(game);
    
        updateById(game);
    
        setRedisClock(game);
    
        //发布完成之后,redis设置发布的次数,默认为1,发布+1,撤销-1
        changePublishCount(game, GameOperateEnum.PUBLISH);
    
        return game.getId();
    }
    
    
    /**
     * 设置redis定时开始/结束游戏
     *
     * @param game 游戏
     */
    private void setRedisClock(GmGame game) {
        Long gameId = game.getId();
        int gameStatus = game.getGameStatus();
        LocalDateTime endTime = game.getEndTime();
        LocalDateTime startTime = game.getStartTime();
    
        LocalDateTime now = LocalDateTime.now();
        long startSecond = LocalDateTimeUtil.between(now, startTime, ChronoUnit.SECONDS);
        long endSecond = LocalDateTimeUtil.between(now, endTime, ChronoUnit.SECONDS);
    
        RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque(GAME_AUTO_ACTION_DELAY_QUEUE);
        //获取到延迟队列
        RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
        if (UN_START.getStatus().equals(gameStatus)) {
            // case1:未开始状态->设置开始时间redis->判断endTime是否设置->设置结束redis
            delayedQueue
                    .offer(StringUtils.joinWith("_", ACTION_START, gameId, startTime.format(PURE_DATETIME_FORMATTER)),
                            Math.abs(startSecond), TimeUnit.SECONDS);
            setEndTime(game, endTime, startTime, startSecond, endSecond, delayedQueue);
        } else if (STARTED.getStatus().equals(gameStatus)) {
            // case2:开始状态->那就判断结束时间是否设置了->如果设置了就设置redis,没设置,就什么都不用设置了
            setEndTime(game, endTime, startTime, startSecond, endSecond, delayedQueue);
        }
        delayedQueue.destroy();
        // case3:已结束状态->nothing(发布即结束)
    }
    
    
    
    
    /**
     * 设置结束时间
     *
     * @param game         游戏
     * @param endTime      结束时间
     * @param startTime    开始时间
     * @param startSecond  从当前到开始时间的秒数
     * @param endSecond    从当前时间到结束时间的秒数
     * @param delayedQueue 延时队列
     */
    private void setEndTime(GmGame game, LocalDateTime endTime, LocalDateTime startTime, long startSecond,
            long endSecond, RDelayedQueue<String> delayedQueue) {
        if (!DateUtil.isDefaultDateTime(endTime) && GameTypeEnum.TOWER.getStatus().equals(game.getGameType())) {
            //如果是智慧塔,且设置了结束时间,正常设置结束时间
            delayedQueue.offer(StringUtils
                            .joinWith("_", ACTION_FINISH, game.getId(), endTime.format(PURE_DATETIME_FORMATTER)),
                    Math.abs(endSecond), TimeUnit.SECONDS);
        } else if (DateUtil.isDefaultDateTime(endTime) && GameTypeEnum.ARENA.getStatus().equals(game.getGameType())) {
            //如果是头脑竞技场,结束时间是后台算出来的,所以也需要设置自动结束
            long useTime = gameCommonService.calcExamUseTime(game);
            delayedQueue.offer(StringUtils.joinWith("_", ACTION_FINISH, game.getId(),
                    startTime.format(PURE_DATETIME_FORMATTER) + "&" + gameCommonService.subjectCount(game)),
                    Math.abs(startSecond) + useTime, TimeUnit.SECONDS);
        } else {
            //不是头脑竞技场,又不是智慧塔,那你就别发布了
            throw new ApiException(GmcenterExceptionKeys.APIS_GAME_TYPE_NOT_SUPPORT);
        }
    }

    放入延时队列之后,再写一个监听阻塞队列的后台任务:

    @Slf4j
    @Component
    @RequiredArgsConstructor
    public class GameAutoListener implements CommandLineRunner {
        private final LockService lockService;
        private final GmGameService gameService;
        private final RedissonClient redissonClient;
        private final GameCommonService gameCommonService;
        private final GmGameSubjectService subjectService;
    
        @Override
        public void run(String... args) throws Exception {
            log.debug("start GameAutoListener...");
            RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque(GAME_AUTO_ACTION_DELAY_QUEUE);
    
            //noinspection InfiniteLoopStatement
            while (true) {
                //根据key不同,进行不同的操作,使用take可以在队列为空的时候阻塞队列,使线程处于wait状态,防止占用过多cpu时间
                String actionAndGameId = blockingDeque.take();
                log.debug("get blockingDeque game: {}", actionAndGameId);
                try {
                    String[] s = StringUtils.split(actionAndGameId, '_');
    
                    String action = s[0];
                    String gameId = s[1];
                    //当action为start时:该字段为startTime
                    //当action为finish时,该字段针对智慧塔类型为endTime,针对头脑竞技场类型为startTime&subCount
                    String timeAndSubCount = s[2];
    
                    GmGame game = gameService.getById(gameId);
                    Integer gameStatus = game.getGameStatus();
                    String dbStartTime = game.getStartTime().format(PURE_DATETIME_FORMATTER);
                    String dbEndTime = game.getEndTime().format(PURE_DATETIME_FORMATTER);
                    Integer gameType = game.getGameType();
    
                    if (action.equals(ACTION_START)) {
                        log.debug("game {} is starting.", gameId);
    
                        //加锁更新数据库之前,先查看缓存的开始时间和需要开始的游戏的开始时间是否相等
                        if (StringUtils.equals(dbStartTime, timeAndSubCount) && game.getDeleted().equals(NOT_DELETED)
                                && GameStatusEnum.UN_START.getStatus().equals(gameStatus)) {
                            startGame(game);
                        }else {
                            log.debug("game start failed. startTime changed or deleted or is not un-start. game: {}", game);
                        }
                    } else if (action.equals(ACTION_FINISH)) {
                        log.debug("game {} is finishing.", gameId);
                        if (game.getDeleted().equals(NOT_DELETED) && GameStatusEnum.STARTED.getStatus()
                                .equals(gameStatus)) {
                            if (GameTypeEnum.TOWER.getStatus().equals(gameType)) {
                                //智慧塔直接判断结束时间是否相等
                                if (timeAndSubCount.equals(dbEndTime)) {
                                    finishGame(game);
                                } else {
                                    log.debug("game had updated. endTime changed. game: {}", game);
                                }
                            } else if (GameTypeEnum.ARENA.getStatus().equals(gameType)) {
                                //如果是头脑竞技场,判断题目数量及开始时间是否相等
                                String[] split = timeAndSubCount.split("&");
                                String cacheStartTime = split[0];
                                String cacheSubCount = split[1];
                                int count = subjectService.count(new QueryWrapper<GmGameSubject>().lambda()
                                        .eq(GmGameSubject::getDeleted, NOT_DELETED)
                                        .eq(GmGameSubject::getGameId, game.getId()));
                                if (Integer.parseInt(cacheSubCount) == count && cacheStartTime.equals(dbStartTime)) {
                                    finishGame(game);
                                } else {
                                    log.debug("game no finish. subject count or startTime changed. game: {}", game);
                                }
                            } else {
                                log.error("game finish failed. game type is not supported. game: {}", game);
                            }
                        } else {
                            log.debug("game deleted or not started, cannot be finished. game: {}", game);
                        }
                    } else {
                        log.error("GameAutoListener action undefined. action: {}", actionAndGameId);
                    }
                } catch (Exception e) {
                    log.error("GameAutoListener failed. actionAndGameId: {} err: {}", actionAndGameId,
                            ExceptionUtils.getStackTrace(e));
                }
            }
        }
    
        /**
         * 设置游戏结束状态
         *
         * @param game 游戏
         */
        private void finishGame(GmGame game) {
            try (RLockSupport locker = RLockSupport.ofLocker(lockService)) {
                locker.acquire(String.format(GAME_AUTO_FINISH_LOCK, game.getId()), INT_2, TimeUnit.SECONDS);
                game.setUpdateTime(LocalDateTime.now());
                game.setUpdator(StringUtils.EMPTY);
                game.setGameStatus(GameStatusEnum.FINISHED.getStatus());
                gameService.updateById(game);
    
                //游戏结束之后,异步给外包发送一个结束消息,失败的时候发mq死信队列
                CompletableFuture.runAsync(() -> gameCommonService.sendFinishToClient(game));
                log.debug("game {} finished.", game.getId());
    
                locker.unlock();
            }
        }
    
        /**
         * 设置游戏开始状态
         *
         * @param game 游戏
         */
        private void startGame(GmGame game) {
            try (RLockSupport locker = RLockSupport.ofLocker(lockService)) {
                locker.acquire(String.format(GAME_AUTO_START_LOCK, game.getId()), INT_2, TimeUnit.SECONDS);
    
                game.setUpdateTime(LocalDateTime.now());
                game.setUpdator(StringUtils.EMPTY);
                game.setGameStatus(GameStatusEnum.STARTED.getStatus());
                gameService.updateById(game);
                log.debug("game {} started.", game.getId());
    
                locker.unlock();
            }
        }
    }

    游戏发布的时候,设置一个发布标志缓存,只有发布次数为1的游戏,才允许自动开始:

    /**
     * 设置发布次数
     *  @param game 游戏
     * @param operateEnum 操作枚举
     */
    private void changePublishCount(GmGame game, GameOperateEnum operateEnum) {
        //这里controller层添加了针对用户和game的防重复提交锁,就不需要再次加分布式锁了
        RBucket<Integer> bucket = redissonClient
                .getBucket(String.format(GmcenterRedisKeys.GAME_CANCEL_COUNT_PREFIX, game.getId()));
    
        if (GameOperateEnum.PUBLISH.equals(operateEnum)) {
            Integer count = bucket.get();
            if (count == null) {
                bucket.set(INT_1);
            } else {
                bucket.set(count + INT_1);
            }
        } else if (GameOperateEnum.CANCEL.equals(operateEnum)) {
            Integer count = bucket.get();
            if (count != null) {
                bucket.set(count - INT_1);
            }
        } else if (GameOperateEnum.FINISH.equals(operateEnum)) {
            bucket.delete();
        }
    }

    在游戏取消的时候,需要让发布次数缓存-1,平衡取消之后的再次发布:

    @Transactional(rollbackFor = Exception.class)
    public Long gameCancel(Long gameId) {
        GmGame game = getById(gameId);
        gameCommonService.checkGameCancelStatus(game);
    
        game.setGameStatus(GameStatusEnum.UN_PUBLISH.getStatus());
        gameCommonService.setUpdateField(game);
    
        updateById(game);
    
        changePublishCount(game, GameOperateEnum.CANCEL);
    
        return game.getId();
    }

    当游戏被手动结束的时候,也需要删除游戏对应的计次buket:

    @Transactional(rollbackFor = Exception.class)
    public Long finishGame(Long gameId) {
        GmGame game = getById(gameId);
        gameCommonService.checkGameFinishStatus(game);
    
        game.setGameStatus(FINISHED.getStatus());
        gameCommonService.setUpdateField(game);
    
        updateById(game);
    
        changePublishCount(game, GameOperateEnum.FINISH);
        gameCommonService.sendFinishToClient(game);
    
        return game.getId();
    }
    展开全文
  • 用 JavaScript 实现一个 TicTacToe 游戏 —— 编程训练

    万次阅读 多人点赞 2020-10-30 08:28:45
    「3」判断输赢 我们的游戏到这里已经可以开始玩了,但是一个游戏不能没有结局吧,所以我们还需要让它可以判断输赢。 在了解 TicTacToe 这个游戏的时候,我们知道这个游戏是有几个条件可以胜利的,就是一方的棋子在...


    同学们好,我是来自 《技术银河》的 💎 三钻

    这里我们给大家讲讲一个好玩的编程练习,很多同学想到编程练习就会觉得与算法有关。但是往往在编程的过程中,我们要实现某种逻辑或者是功能的时候,确实是需要用到算法。但是我觉得 Winter 老师说的也挺对的。

    编程练习有一部分是与算法和数据结构密切相关的,但是也有一部分是跟语言比较相关的。我们既要知道这个算法我们怎么去写,我们还要跟语言相结合,就是怎么去用我们的语言更好的去表达。不过编程练习的核心还是提升我们编程的能力。

    TicTacToe 是一个非常著名的一个小游戏,国外叫做 TicTacToe,国内我们叫它 “三子棋” 或者 “一条龙”。

    如果我们要实现这个小游戏,我们首先就需要了解这个游戏的规则。如果不懂这个游戏的规则,我们是无法用代码语言来表达的。

    「一」规则

    • 棋盘:3 x 3 方格
    • 双方分别持有 ⭕️ 和 ❌ 两种棋子
    • 双方交替落子
    • 率先连成三子直线的一方获胜
    • 这个直线分别可以是“横”,“竖”,“斜” 三种

    「二」代码实现

    「1」创建棋盘

    这个游戏是基于拥有一个可以放棋子的棋盘,换做我们的程序的话,就是一个存放数据的地方,记录着每个棋子所放在的位置。

    这里我们可以用一个二维数字来存放:

    let parttern = [
      [2, 0, 0],
      [0, 1, 0],
      [0, 0, 0]
    ]
    
    console.log(pattern)
    
    • 0 表示为没有棋子存放在这个棋盘的位置
    • 1 表示为在其面上有 ⭕️ 的棋子
    • 2 表示为在其面上有 ❌ 的棋子

    我们拥有棋盘的数据之后,因为这是一个可以给用户玩的游戏,我们当然需要展示在浏览器上的。所以这里我们就需要加入 HTML 和 CSS。

    <style>
          * {
            box-sizing: border-box;
            background: #0f0e18;
          }
          .container {
            margin: 3rem;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
          }
          h1 {
            color: #ea4dc5;
          }
          #board {
            width: 300px;
            height: 300px;
            display: flex;
            flex-wrap: wrap;
          }
          .cell {
            width: 100px;
            height: 100px;
            background: #2d2f42;
            border: 1px solid #0f0e18;
            cursor: pointer;
            display: flex;
            justify-content: center;
            align-items: center;
            transition: background 500ms ease;
          }
          .cell:hover {
            background: #454966;
          }
          .cell .iconfont {
            background: transparent;
            width: 100%;
            height: 100%;
            font-size: 50px;
            line-height: 100px;
            text-align: center;
            vertical-align: middle;
          }
          .blue {
            color: #7ceefc;
          }
          .purple {
            color: #ea4dc5;
          }
          #tips p {
            color: #dddddd;
          }
    </style>
    
    <div class="container">
      <h1>Tic-tac-toe Simulator</h1>  <!-- 标题 -->
      <div id="board"></div>  <!-- 棋盘 -->
      <div id="tips"></div> <!-- 这里是用于提示的,后面的功能会用到 -->
    </div>
    

    写好了上边的 HTML 和 CSS,我们会发现棋盘上是一个空 div,棋盘上的格子还没有被加上。

    这里我们是需要根据我们的 pattern 中的数据来创建棋盘的。所以我们需要加入 JavaScript ,根据我们的棋盘数据来创建我们棋盘上的格子和棋子。

    // 棋盘
    let pattern = [
      [2, 0, 0],
      [0, 1, 0],
      [0, 0, 0]
    ];
    
    let chess = 1;
    
    /** 渲染棋盘 */
    function build() {
      // 获取棋盘元素
      let board = document.getElementById('board');
    
      board.innerHTML = '';
    
      // 填充棋盘
      for (let y = 0; y < 3; y++) {
        for (let x = 0; x < 3; x++) {
          let cell = document.createElement('div');
          cell.classList.add('cell');
    
          // 创建圆圈棋子
          let circle = document.createElement('i');
          circle.classList.add('iconfont', 'icon-circle', 'blue');
          // 创建叉叉棋子
          let cross = document.createElement('i');
          cross.classList.add('iconfont', 'icon-cross', 'purple');
          // 创建空棋子
          let empty = document.createElement('i');
    
          let chessIcon = pattern[y][x] == 2 ? cross : pattern[y][x] == 1 ? circle : empty;
          cell.appendChild(chessIcon);
          board.appendChild(cell);
        }
      }
    }
    

    创建这个棋盘我们使用了以下思路:

    • 首先循环一遍我们的二维数组 pattern
    • 一个双循环就等同于我们从上到下,从左到右的走了一篇这个棋盘数据了
    • 在循环这个棋盘的同时我们需要把棋子也同时放入棋盘中
    • 首先我们创建一个棋盘格子 div 元素,给予它一个 class 名为 cell
    • 如果我们遇到 1 的时候就放入 ⭕️ 到 cell 里面
    • 如果我们遇到 2 的时候就放入 ❌ 到 cell 里面
    • 如果我们遇到 0 的时候就放入一个 “空” 到 cell 里面
    • 棋子这里我给了一个 i 元素,并且它的 class 用了 iconfont
    • 当然如果我们也是可以用 emoji 替代这部分内容,直接给 cell 元素加入文本 (例如:cell.innerText = '⭕️'
    • 最后把 cell 加入到棋盘 board 里面即可

    这里的代码我使用了 “阿里巴巴” 的 iconfont,当然我们也可以直接用 emoji。跟着我的文章练习的同学,也可以使用我在用的 iconfont。这里我附上我在使用的 iconfont 地址:

    <link rel="stylesheet" href="//at.alicdn.com/t/font_2079768_2oql7pr49rm.css" />
    

    最后显示出来的就是这样的效果:

    「2」落棋子

    我们已经拥有一个 3 x 3 的棋盘了,下来就是实现落棋子的动作的方法。我们想要达到的效果就是让用户点击一个格子的时候,就把棋子落到对应点击的位子。如果该位置已经有棋子了就不生效。

    /**
     * 把棋子放入棋盘
     *
     *   - 先把当前棋子代号给予当前 x,y 位置的元素
     *
     * @param {Number} x x轴
     * @param {Number} y y轴
     */
    function move(x, y) {
      if (pattern[y][x]) return;
    
      pattern[y][x] = chess;
    
      chess = 3 - chess;
    
      build();
    }
    

    这段代码的逻辑很简单:

    • 如果当前 xy 位置已经有棋子,那必然就不是 0 ,如果是 0 就直接返回,推出此方法即可
    • 如果可以落下棋子,就给当前位置赋予棋子的代码 1 就是 ⭕️, 2 就是 ❌
    • 这里我们使用了 1 和 2 的对等特性, 3 − 1 = 2 3-1=2 31=2,同样 3 − 2 = 1 3-2=1 32=1 ,用这样的对等换算我们就可以反正当前棋子了
    • 也就是说上一位玩家的棋子是 1 3 − 当 前 棋 子 = 下 一 位 玩 家 的 棋 子 3-当前棋子=下一位玩家的棋子 3= ,那就是 2
    • 最后调用我们的棋盘构建方法 build 重新构建棋盘即可

    这个方法写了,但是我们发现我们根本没有调用到它,所以在棋盘上点击的时候是无任何效果的。

    所以这里我们要在构建棋盘的时候,就给每一个格子加上一个 “点击 (click)” 事件的监听。

    /** 渲染棋盘 */
    function build() {
      //... 省略了这部分代码
    
      let chessIcon = pattern[y][x] == 2 ? cross : pattern[y][x] == 1 ? circle : empty;
      cell.appendChild(chessIcon);
      cell.addEventListener('click', () => move(x, y)); // 《 == 这里加入监听事件
      board.appendChild(cell);
      
     // ... 省略了这部分代码
    }
    

    这样我们的棋盘就可以点击格子放下棋子了!

    「3」判断输赢

    我们的游戏到这里已经可以开始玩了,但是一个游戏不能没有结局吧,所以我们还需要让它可以判断输赢。

    在了解 TicTacToe 这个游戏的时候,我们知道这个游戏是有几个条件可以胜利的,就是一方的棋子在“”,“”,“”连成一线就可以赢得游戏。所以这里我们就需要分别检测这三种情况。

    通过分析我们就有 4 种情况:

    • 竖行有 3 个棋子都是一样的
    • 横行有 3 个棋子都是一样的
    • 正斜行 “/” 有 3 个棋子都是一样的
    • 反斜行 “\” 有 3 个棋子都是一样的

    那么我们就写一个 check() 方法来检测:

    /**
     * 检查棋盘中的所有棋子
     *
     *  - 找出是否已经有棋子获胜了
     *  - 有三个棋子连成一线就属于赢了
     *
     * @param {Array} pattern 棋盘数据
     * @param {Number} chess 棋子代号
     * @return {Boolean}
     */
    function check(pattern, chess) {
      // 首先检查所有横行
      for (let i = 0; i < 3; i++) {
        let win = true;
        for (let j = 0; j < 3; j++) {
          if (pattern[i][j] !== chess) win = false;
        }
        if (win) return true;
      }
    
      // 检查竖行
      for (let i = 0; i < 3; i++) {
        let win = true;
        for (let j = 0; j < 3; j++) {
          if (pattern[j][i] !== chess) win = false;
        }
        if (win) return true;
      }
    
      // 检查交叉行
      // 这里用花括号 "{}" 可以让 win 变量
      // 变成独立作用域的变量,不受外面的
      // win 变量影响
    
      // "反斜行 \ 检测"
      {
        let win = true;
        for (let j = 0; j < 3; j++) {
          if (pattern[j][j] !== chess) win = false;
        }
        if (win) return true;
      }
    
      // "正斜行 / 检测"
      {
        let win = true;
        for (let j = 0; j < 3; j++) {
          if (pattern[j][2 - j] !== chess) win = false;
        }
        if (win) return true;
      }
    
      return false;
    }
    

    有了这个检测输赢的方法,我们就可以把它放到一个地方让它检测游戏的赢家了。

    我们可以把这个检测放入用户落棋子的时候,在棋子类型反转和重建之前,就检测当前玩家是否胜利了。

    /** 全局变量 —— 是否有赢家了 */
    let hasWinner = false
    
    /**
     * 把棋子放入棋盘
     *
     *   - 先把当前棋子代号给予当前 x,y 位置的元素
     *   - 检测是否有棋子已经赢了
     *
     * @param {Number} x x轴
     * @param {Number} y y轴
     */
    function move(x, y) {
      if (hasWinner || pattern[y][x]) return;
    
      pattern[y][x] = chess;
    
      // 这里加入了胜负判断
      if (check(pattern, chess);) {
        tips(chess == 2 ? '❌ is the winner!' : '⭕️ is the winner!');
      }
    
      chess = 3 - chess;
    
      build();
    }
    

    这里我们需要加入一个 hasWinner 的全局变量,这个是用来记录这个游戏是否已经有赢家了,如果有赢家,就不能让用户在落棋子了。所以在 move 方法的开头就判断了,如果有赢家了就直接返回,退出方法。

    加入这段代码我们就可以判断胜负的,但是我们还需要在页面上提示用户到底是谁赢了才完美嘛。所以这里我们加入了一个提示插入的方法:

    /**
     * 插入提示
     * @param {String} message 提示文案
     */
    function tips(message) {
      let tips = document.getElementById('tips');
    
      tips.innerHTML = '';
    
      let text = document.createElement('p');
      text.innerText = message;
      tips.appendChild(text);
    }
    

    最终的效果如下:

    「三」实现 AI

    现在我们已经拥有了一个可以玩的 “TicTacToe” 游戏了。但是在这个时代,没有一点 AI 支持的程序,怎么能成为一个好的产品呢?所以这里我们来一起给我们的游戏加入一下 AI 的功能。

    「1」预判下一步是否会赢

    我们首先整理一下这个需求,在某一个玩家落棋之后,就可以检测这盘棋的下一个玩家是否即将会赢。

    要判断下一个玩家是否即将会赢,我们就需要模拟下一个玩家落棋子的位置,其实对我们的程序来说,就是把棋子依次放入现在棋盘中空出来的格子,然后判断下一个玩家会不会赢了游戏。

    实现思路:

    • 我们的时机是在上一个玩家落下棋子后,开始模拟下一个玩家所有可能走的位置
    • 这个时候我们可以循环现在的棋盘上的格子,模拟下一个玩家把棋子放入每一个非空的格子的结果
    • 如果遇到有一个格子放入棋子后会赢的话,那下一个玩家就是可以赢了!

    这里我们要注意的是,我们需要模拟下一个玩家在当前局面下走了每一个空格子的结果,这个时候如果我们用原来的 pattern 数据来模拟,就会影响了现在游戏里棋子的位置。所以我们需要不停的克隆现在棋盘的数据来模拟。这样才不会影响当前棋盘的数据。

    实现预测方法:willWin()

    /**
     * 检测当前棋子是否要赢了
     *
     *   - 循环整个棋盘
     *   - 跳过所有已经有棋子的格子
     *   - 克隆棋盘数据(因为我们要让下一个棋子都走一遍所有空位的地方
     *     看看会不会赢,如果直接在原来的棋盘上模拟,就会弄脏了数据)
     *   - 让当前棋子模拟走一下当前循环到的空位子
     *   - 然后检测是否会赢了
     *
     * @param {Array} pattern 棋盘数据
     * @param {Number} chess 棋子代号
     * @return {boolean}
     */
    function willWin(pattern, chess) {
      for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
          if (pattern[i][j]) continue;
          let tmp = clone(pattern);
          tmp[i][j] = chess;
          if (check(tmp, chess)) {
            return true;
          }
        }
      }
      return false;
    }
    

    克隆方法:clone()

    /**
     * 克隆棋盘数据
     * @param {Array} pattern 棋盘数据
     * @return {Array} 克隆出来的棋盘数据
     */
    function clone(pattern) {
      return JSON.parse(JSON.stringify(pattern));
    }
    

    最后我们需要在上一个玩家落棋,之后加入输赢预判方法:“改装我们的 move() 方法即可”

    /**
     * 把棋子放入棋盘
     *
     *   - 先把当前棋子代号给予当前 x,y 位置的元素
     *   - 检测是否有棋子已经赢了
     *   - 反转上一个棋子的代号,并且重新渲染棋盘
     *
     * @param {Number} x x轴
     * @param {Number} y y轴
     */
    function move(x, y) {
      if (hasWinner || pattern[y][x]) return;
    
      pattern[y][x] = chess;
    
      hasWinner = check(pattern, chess);
      if (hasWinner) {
        tips(chess == 2 ? '❌ is the winner!' : '⭕️ is the winner!');
      }
    
      chess = 3 - chess;
    
      build();
    
      if (hasWinner) return;
    
      // 这里加入了输赢预判
      if (willWin(pattern, chess)) {
        tips(chess == 2 ? '❌ is going to win!' : '⭕️ is going to win!');
      }
    }
    

    这里还加入了一个判断:if(hasWinner) return;,这个是为了如果这步棋有玩家已经赢了,我们就不需要再预判输赢了,可以直接返回了。

    就这样我们就实现了一个,智能的输赢预判功能了,最后的效果如下图:

    「2」预判游戏胜负

    上面我们实现的 AI 只能给我们预判下一步棋是否会赢。但是并没有给我们预判出,以现在的局面最终谁会赢。

    这里我们一起来实现一个更加智能的 AI,让程序在每一个玩家落子之后,判断以现在棋子的局面,最终谁会赢,或者是否结果是和棋。

    实现思路:

    • 首先我们要给我们游戏的最终结果定义好标识
    • 结果是 -1 就是最后会输
    • 结果是 0 就是最后会和
    • 结果是 1 就是最后会赢
    • 这里胜负是正负相反的,这个设计就是为了让我们更好的判断输赢
    • 也可以这么理解,对方的棋子放入了可以赢的位置,那么我们的结果就肯定是输,这个结果就是刚好相反的,所以我们用了正负的标识来表达就非常方便我们用程序来判断
    • 使用我们上面说到的逻辑,我们就可以锁定一个思路,如果我们找到对方要输的棋子的位置,那我们就是会赢的位置,如果我们找到对方要赢的位置,我们就要输
    • 利用这样的逻辑我们可以用一个递归的方法来循环模拟两个玩家的落子动作,并且判断出落棋后的结果,一直深度搜索直到我们找到一个赢家
    • 这个递归最终会模拟两个玩家走了这盘棋的所有情况并且找到一个能赢的局面,就可以结束循环了。这个也叫做“胜负节支”。赢已经是最好的结果了,我们并不需要继续模拟到所有的情况,我们已经找到最佳的情况了。
    • 当然在其他棋盘游戏中,可能有很多胜利的局面,有可能是赢了但是损失了很多,也有赢了但是又快又减少了损失。但是在这个 “TicTacToe” 当中就不需要考虑这些因素了。

    说了那么多,我们来看看代码是怎么实现的,我们先来实现一个寻找最佳结果的方法 bestChoice

    /**
     * 找到最佳结果
     *
     *   - 结果是 -1 就是最后会输
     *   - 结果是  1 就是最后会赢
     *   - 结果是  0 就是最后会和
     *
     * @param {Array} pattern 棋盘数据
     * @param {Number} chess 棋子代号
     */
    function bestChoice(pattern, chess) {
      // 定义可以赢的位置
      let point;
    
      // 如果当前局面,我们已经即将要赢了
      // 我们就可以直接返回结果了
      if ((point = willWin(pattern, chess))) {
        return {
          point: point,
          result: 1,
        };
      }
    
      // 定义一个结果,-2 是要比 -1,0,1 要小
      // 所以是一个最差的局面,我们需要从最差的局面开始
      // 数字变得越高,我们就越接近赢
      let result = -2;
      point = null;
      outer: for (let y = 0; y < 3; y++) {
        for (let x = 0; x < 3; x++) {
          // 跳过所有已经有棋子的地方(因为不能在这些地方放我们的棋子了)
          if (pattern[y][x]) continue;
    
          // 先克隆当前棋盘数据来做预测
          let tmp = clone(pattern);
    
          // 模拟我们的棋子下了这个位置
          tmp[y][x]= chess;
    
          // 找到我们下了这个棋子之后对手的最佳结果
          let opp = bestChoice(tmp, 3 - chess);
    
          // 记录最佳结果
          if (-opp.result >= result) {
            result = -opp.result;
            point = [x, y];
          }
    
          if (result === 1) break outer;
        }
      }
    

    这段代码做了什么?其实就是让我们的程序进行了自我博弈,A 方找到自己可以赢的落子位置,然后 B 方找自己可以赢的落子位置,知道最后进入一个结果,要不两方都赢不了,那就是和局,要不就是一方获胜为止。

    我们会关注到,这里 bestChoice 返回了一个对象,一个属性是 result, 这个就是预判出来这个游戏最后的结果。而另外一个是 point,这个就是当前玩家可以走的位置,也是可以达到最佳结果的位置。这个在我们实现最后一个 AI 功能的时候会用到。这一步我们只需要用到 result 属性来做判断,输出胜负提示即可。

    有了这个更高级的预判 AI,我们就可以把我们的 willWin() 替换下来了。

    这里我们改造一下我们的 move() 方法:

    /**
     * 把棋子放入棋盘
     *
     *   - 先把当前棋子代号给予当前 x,y 位置的元素
     *   - 检测是否有棋子已经赢了
     *   - 反转上一个棋子的代号,并且重新渲染棋盘
     *
     * @param {Number} x x轴
     * @param {Number} y y轴
     */
    function userMove(x, y) {
      if (hasWinner || pattern[y][x]) return;
    
      pattern[y][x] = chess;
    
      if ((hasWinner = check(pattern, chess))) {
        tips(chess == 2 ? '❌ is the winner!' : '⭕️ is the winner!');
      }
    
      chess = 3 - chess;
    
      build();
    
      if (hasWinner) return;
    
      let result = bestChoice(pattern, chess).result;
      let chessMark = chess == 2 ? '❌' : '⭕️';
      tips(
        result == -1
          ? `${chessMark} is going to loss!`
          : result == 0
          ? `This game is going to draw!`
          : `${chessMark} is going to win!`
      );
    }
    

    最后出来的效果就是如此:

    当然这个预判是在预判最好的结果,这里我们假设了两个玩家都是非常优秀的,每一步都是走了最佳的位置。但是如果玩家失误还是有可能反败为胜的哦!

    「3」加入电脑玩家

    我们前面实现的 AI,已经足够让我们实现一个很聪明的 AI 电脑玩家了。

    在上一步我们实现了 bestChoice() 方法的时候,这个方法返回的属性里,有一个 point 属性,这个point 其实就是玩家最佳落子的位置,我们只需要让程序自动落子到这个位置,我们就完成了电脑玩家的功能了。

    实现思路:

    • 上一个玩家落子之后,就可以调用我们电脑玩家落子方法
    • 使用 bestChoice 找到最佳结果的落子位子
    • 给最佳位子放下电脑玩家的棋子
    • 最后继续预测这个游戏的结局

    真的就是那么简单,我们来看看代码怎么实现:

    这里我们需要改造 move() 方法,改为 userMove(),并且创建一个 computerMove()

    /**
     * 把棋子放入棋盘
     *
     *   - 先把当前棋子代号给予当前 x,y 位置的元素
     *   - 检测是否有棋子已经赢了
     *   - 反转上一个棋子的代号,并且重新渲染棋盘
     *
     * @param {Number} x x轴
     * @param {Number} y y轴
     */
    function userMove(x, y) {
      if (hasWinner || pattern[y][x]) return;
    
      pattern[y][x] = chess;
    
      if ((hasWinner = check(pattern, chess))) {
        tips(chess == 2 ? '❌ is the winner!' : '⭕️ is the winner!');
      }
    
      chess = 3 - chess;
    
      build();
    
      if (hasWinner) return;
    
      computerMove();
    }
    
    /** 电脑自动走棋子 */
    function computerMove() {
      let choice = bestChoice(pattern, chess);
    
      if (choice.point) pattern[choice.point[1]][ choice.point[0]] = chess;
    
      if ((hasWinner = check(pattern, chess))) {
        tips(chess == 2 ? '❌ is the winner!' : '⭕️ is the winner!');
      }
    
      chess = 3 - chess;
      build();
    
      if (hasWinner) return;
    
      let result = bestChoice(pattern, chess).result;
      let chessMark = chess == 2 ? '❌' : '⭕️';
      tips(
        result == -1
          ? `${chessMark} is going to loss!`
          : result == 0
          ? `This game is going to draw!`
          : `${chessMark} is going to win!`
      );
    }
    

    就是这样我们就实现了电脑玩家,这样一个单身狗也可以玩 “TicTacToe” 了。 😂😂😂

    开个玩笑哈,说不定玩着玩着你就找到人生另一半的啦!加油哦!💪

    「四」优化

    写到这里,我们已经完成了一个 “TicTacToe” 游戏了。实现完一个功能后,我们都会问自己一个问题,这个程序有没有可以优化的地方呢?

    以我们上面的代码示例,其实是有一个地方可以优化的,那就是我们的棋盘数据。

    示例里面我们的棋盘数据是使用了一个二维数组的,这样在我们克隆的时候需要使用 JSON 转换来克隆,这个过程我们需要用到大量的内存空间。

    如果我们把棋盘的数据改造成一个一维数组的话,我们就可以用 JavaScript 里面的 Object.create(pattern) 来克隆了。这个方法创建了一个新对象,使用现有的对象来提供新创建的对象的 __proto__,这样的方式就能节省大量的内存空间。因为我们使用了原型克隆,而不是整个对象的克隆。

    首先我们改造棋盘数据

    // 棋盘
    let pattern = [
      0, 0, 0, 
      0, 0, 0, 
      0, 0, 0
    ];
    

    现在我们棋盘是一个一位数字,那么我们怎么换行呢?

    用数学去理解的话:当前行数 * 3 + 当前行的指针位置​,当然我们行数在数组是从0开始的。

    所以就是这样一个现象:

    [

    (0 * 3 + 1), (0 * 3 + 2), (0 * 3 + 3),

    ( 1 * 3 + 1), ( 1 * 3 + 2), ( 1 * 3 + 3),

    (2 * 3 + 1), (2 * 3 + 2), (2 * 3 + 3),

    ]

    最后得出的位置就是这样的:

    [

    1, 2, 3,

    4, 5, 6,

    7, 8, 9

    ]

    这样是不是就能找到我们 9 个格子的位置呀?

    所以在代码中,我们只需要把所有的 pattern[y][x] 改为 pattern[y * 3 + x] 即可!

    最后我们可以改造我们的 clone() 方法:

    /**
     * 克隆棋盘数据
     * @param {Array} pattern 棋盘数据
     * @return {Array} 克隆出来的棋盘数据
     */
    function clone(pattern) {
      return Object.create(pattern);
    }
    

    「终」总结

    其实这个 “TicTacToe” 练习的重点在于抽象思路。我们是怎么把一个游戏复杂的逻辑一步一步抽象成我们程序的代码,通过 if else 判断,加上 iteration 循环来实现我们的需求和功能。这个过程其实不单纯的锻炼我们的算法和数学,更多是编程能力。

    这里我又要感叹一下,覃超老师经常说的 “五毒神掌” 了。一切学习和技能不在于我们有多么好的天赋,更多的在于我们有没有反复练习。只有经历过无数遍磨练的知识和技能才会变成我们内力。


    我是来自《技术银河》的三钻:“学习是为了成长,成长是为了不退步。坚持才能成功,失败只是因为没有坚持。同学们加油哦!下期见!”


    推荐专栏

    小伙伴们可以查看或者订阅相关的专栏,从而集中阅读相关知识的文章哦。

    • 📖 《前端进阶》 — 这里包含的文章学习内容需要我们拥有 1-2 年前端开发经验后,选择让自己升级到高级前端工程师的学习内容(这里学习的内容是对应阿里 P6 级别的内容)。

    • 📖 《数据结构与算法》 — 到了如今,如果想成为一个高级开发工程师或者进入大厂,不论岗位是前端、后端还是AI,算法都是重中之重。也无论我们需要进入的公司的岗位是否最后是做算法工程师,前提面试就需要考算法。

    • 📖 《FCC前端集训营》 — 根据FreeCodeCamp的学习课程,一起深入浅出学习前端。稳固前端知识,一起在FreeCodeCamp获得证书

    • 📖 《前端星球》 — 以实战为线索,深入浅出前端多维度的知识点。内含有多方面的前端知识文章,带领不懂前端的童鞋一起学习前端,在前端开发路上童鞋一起燃起心中那团火🔥

    展开全文
  • 大家确认自己身份-> 晚上有能力的玩家使用技能 -> 天亮大家自由讨论 -> 然后投个票,OK游戏结束,图片可以开下一局了。就算是满员的8人局,一局也大概只需十几分钟而已。 没错游戏流程就是这么简洁,节奏就是这么快...
  • 用Python做一个超简单的小游戏(一就懂)

    万次阅读 多人点赞 2020-12-20 14:59:59
    有的人可能学过一点Python基础,但知道干什么好。今天就教大家做一个简单的小游戏 文章目录前言写它会用到源码先抛出来知识讲解random介绍语法方法参数表举例while语法举例拆分代码讲解 写它会用到 while 循环 ...
  • 小媛:bit 哥,bit 哥,好像 IVX 还可以做小游戏? ????1_bit:是的呢。 ????小媛:那你教我做小游戏好不?最近接了个单,说做个小游戏就给 1000。 ????1_bit:哈哈哈,你接单上瘾呀。 ????小媛:没呢,这是我在学校...
  • 官方摘星星游戏

    千次阅读 2021-11-18 20:45:36
    通过控制游戏主角(小怪物)跳跃,来触碰到随机生成的星星,如果在星星消失前触碰到,那么则积分+1,否则游戏结束。 实现 大致步骤 1、构建静态页面 2、往UI节点上添加自定义脚本组件(JS或TS编写的满足CocosCreator...
  • 这时设置暂停我们需要注意一点就是能让线程停止,我一开始试了很多方法,都是将run里的while死循环的判断条件改为了false,这样导致的结果是游戏确实暂停了,但是却继续了了,原因就是你的线程已经结束了(run...
  • 关于作者 简·麦格尼格尔是著名未来学家,未来...说句实话,我本人不是一个游戏爱好者,顶多玩一玩狼人杀,和大家切磋一下,要是等级再高一个档次,恐怕就不是我能招架得了的,废话多说!最近被安利了一本书,...
  • 【Python】 Python小游戏-贪吃蛇大冒险

    万次阅读 多人点赞 2021-04-17 17:26:59
    一、前言 距上次更新博客,又过去很长时间了,感觉再不更新一下,自己写博客的习惯就要废了,哈哈。从去年九月份开学之后,...1.1游戏操作及游戏规则 (1)游戏开始后,通过键盘上下左右键控制贪吃蛇移动寻找食物; (2
  • 俄罗斯方块游戏,重温经典(C++详解版)

    万次阅读 多人点赞 2021-08-10 07:51:04
    俄罗斯方块游戏大家应该非常熟悉,非常经典的一款游戏,本文来详细讲解下俄罗斯方块游戏的制作过程,赶紧来看下吧! 首先,看下效果图: 图1 游戏运行过程 下面详细讲解下制作过程。 一、主要文件 文件包含三...
  • 1小时1篇文学会python再做个飞机大战游戏

    万次阅读 多人点赞 2021-04-30 16:02:07
    注: 想慢慢学习的同学可以看我的 《python 入门到游戏实战专栏》(更新中) 想学 C 的同学可以看《大话系列之C语言》(基本更新完) 想学 C++ 的同学可以看《大话C++》(更新中) 想做习题的可以看《大学生C语言...
  • 微信飞机大战小游戏详细教程
  • Python使用tkinter模块实现推箱子游戏

    万次阅读 多人点赞 2019-10-02 19:28:31
    BoxGame:作为游戏的主入口,游戏的主要流程就在里面。老实说我Python学习的内容比较少,对Python的面向对象不是很熟悉,所有这个流程更偏向于面向过程的思想。 initGame:初始化或存储一些数据,如地图数据,人的...
  • 3.军事家,我从小比较喜欢看抗日电视剧,感觉我们国家从苦难中走出来很容易,我那时希望自己成为军事家,能够保卫好我们的国家! 4.科学家,老师跟我们说科学家是国家栋梁!我也希望能够成为杰出的科学家,提升...
  • 利用C++写一个简单的贪吃蛇小游戏

    千次阅读 多人点赞 2020-02-27 15:20:24
    //游戏结束时设计一个界面输出“游戏结束”以及分数 void finmatt ( const int score ) { system ( "cls" ) ; //清屏然后输出 gotoxy ( 40 , 14 ) ; cout "游戏结束" ; gotoxy ( 40 , ...
  • Python 用pygame 做一个游戏的开始界面(小白第一篇博客)主要功能实现本篇文章主要是实现了一个游戏开始...素材准备首先准备素材,为了方便起见,我从艺术字网上索取了我个人觉得比较好看的字体(开始游戏结束游戏...
  • 适合新手做,微信小游戏,小程序开发入门,扫雷游戏实现方案
  • C语言小游戏(一)----猜数游戏

    千次阅读 多人点赞 2022-02-11 14:59:54
    一样的方式讲解游戏程序,一步一步升级,对于想掌握实际编程能力的初级猿具有极大的帮助!其中干货多多,阅读后必能给你带来极大的收获!
  • 在 Unity 中保存和载入游戏

    万次阅读 多人点赞 2017-11-27 11:31:17
    可能让玩家一次就玩完整个游戏。允许玩家保存游戏游戏最基本的一个功能——哪怕仅仅保存玩家的得分记录。但如何创建一个存档文件,以及需要在里面保存什么东西?你必须在存档中保存玩家的设置吗?以及
  • 游戏开发实战】Unity手游第一人称视角,双摇杆控制,FPS射击游戏Demo(教程 | 含Demo工程源码)
  • JavaScript制作贪吃蛇小游戏

    千次阅读 多人点赞 2022-01-02 16:45:44
    揭秘贪吃蛇小游戏代码
  • 修改子弹类:public class Bullet { //子弹图片资源 public Bitmap bmpBullet; //子弹的坐标 public int bulletX, bulletY; //子弹的速度 public int speed; //子弹的种类以及常量 public int bulletType;...
  • 游戏同步方案——帧同步

    千次阅读 2020-06-27 17:22:26
    游戏同步方案——帧同步帧同步(Lockstep)和状态同步(State Synchronization)状态同步帧同步合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表...
  • 树莓派之Debian游戏(部分)

    千次阅读 2021-02-05 21:21:38
    这些游戏可以在树莓派自带的软件商店里找到并下载、安装 1.0ad(古代战争的实时战略游戏) 是一个自由、开源、跨平台的古代战争实时策略游戏。简而言之,它是一款基于历史的战争/经济游戏,允许游戏者再体验或改写...
  • 简介1.1 PIXI 简介1.2 坦克大战游戏简介2. 实现细节2.1 技术选型2.2 环境搭建2.3 创建舞台2.4 游戏状态机2.5 消息总线2.6 加载页面2.7 开始页面2.8 我们的坦克 1. 简介 1.1 PIXI 简介 PIXI JS是一款轻量级的HTML5的...
  • 在该init方法中还设置了count 用于累计猜测次数,tip当前提示,x为猜测值,isGame表示是否游戏结束。 三、判断所猜数字正确与否 接下来在游戏界面 game.wxml 中输入如下代码编写界面: <view> 猜数字游戏text> ...
  • 麻将游戏牌算法

    万次阅读 2015-11-04 11:04:29
    测试测试!!~~~~这两周都是在测试各种BUG,没事情的时候自己在网上学学新知识,也...打麻将的童靴一定知道什么叫牌,可是我打麻将最初也知道什么是牌,霍霍,好丢人啊,估计是以前有东南西北中发白的时候这
  • Java多线程游戏仿真实例分享

    万次阅读 多人点赞 2021-02-02 18:20:57
    这是一篇学习分享博客,这篇博客将会介绍以下几项内容: 1、如何让一个程序同时做多件事?(多线程的创建、多线程的应用、多线程的特点以及多...3、多线程游戏仿真实例分享(飞机大战、接豆人、双线挑战三个游戏实例)

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 51,641
精华内容 20,656
关键字:

不听游戏结束