关灯游戏AI扩展

来源:互联网 发布:mac ruby安装目录 编辑:程序博客网 时间:2024/05/16 12:05

  • 前言
  • 站在AI的角度
    • AI应提供直接的方法解决任何关灯游戏的问题
    • AI20设计
    • 实现AI20
    • Redesign DHLightView
    • Redesign DHLightGameManager
  • 优化策略
    • 从算法角度
    • 从数学角度
  • Extra Hyperlink

前言

在我发布昨天的博客第二天(不就是今天么)就有小伙伴来告诉我:“DHDH,你的关灯游戏AI好牛逼(崇拜脸),但是啊,这个AI只能解决你自己的DHLightGameManager控制的游戏,我想用你的AI去网上玩关灯游戏就没法用了,有没有办法把AI扩展到任意的关灯游戏?”

我一想,还真是这样。我之前是站在了实现游戏的角度而不是实现AI的角度在做昨天的AI。于是乎,我又来了,咱们的AI即将进化为2.0版本,出任CEO,迎娶白富美,走上人生的巅峰!

站在AI的角度

我们的主角是AI而不是GM,所以应该将AI实实在在的封装起来而不是一个私有类+协议,私有类+协议是把GM当做主角,AI是幕后工程师。
好的,现在AI就要从幕后走到台前了。
既然要走到前台,肯定需要为它分配一个角色,一个主角,那么就是把AI作为一个独立的类,很明显是一个单例,我取名为DHLightsOffAI。

AI应提供直接的方法解决任何关灯游戏的问题

站在AI的角度,我们就应该直接调用AI的方法来解决问题了,而不是像昨天那样,将AI隐藏在GM中,只暴露协议和一个id类型的实现了协议的对象,幕后英雄的名字不得而知。
我们的AI是一个类似于Utinity的类,它只负责计算解法,而不懂得展示解法。如果我们需要把某局关灯游戏的解法通过界面展示出来,就又需要借助GM来实现效果了。
这当然是我们这个版本需要做到的。
在这之前,我们先设计好AI的接口和算法,剩下的就交给GM去调用并实现界面。
当然,这个类被封装好了以后可以应用到任何的关灯游戏里面。

AI2.0设计

我们的AI既然能解决任何类型的局,那怎样才能确保任意性?
考虑这种问题只需要思考:决定这局游戏与众不同的因素在哪里?
应该能很快想到:行数、列数、当前哪些灯是开着的。
我们转换为代码的声明:

NSUInteger row;NSUInteger column;NSArray * lightsOnCoordinates

也就是说,如果我们要让AI能解决任意一局的关灯游戏,就要让它拥有解决“对于任意的row、column、lightsOnCoordinates都能寻找解决方法”的能力。

那么AI只需要一个方法就OK了,传入row、column、lightsOnCoordinates三个参数,AI就能通过这三个参数寻找这局关灯游戏的解法。

现在来考虑如何将结果传出。传出结果的时机有两种:
1、调用上面的接口后立即传出
2、调用上面的接口后延迟传出

第一种通常用返回值来把解决的结果传出,第二种通常用一个属性来记录计算的结果,外面想什么时候用这个属性就什么时候用,AI能确定的是一旦你调用了我解决问题的接口,那么这个属性里面就一定有你要的内容了。

当然这里还有第三种传出的方式:回调。
考虑到我们在寻找解法的过程是一种穷举的方式,所以应该被归结为一种“耗时的操作”。这种“耗时的操作”就应该放进子线程,操作完了以后通过回调函数的参数来把操作的结果传出,当然在OC中回调函数可以轻易地用block来代替。我们选择GCD来实现异步处理的话,还可以考虑由外界提供GCD队列,如果外界不提供(传入nil)则我们就使用默认的全局队列。

这样这个方法的声明就可以写出来了:

- (void)startResolveWithRow:(NSUInteger)row                     column:(NSUInteger)column        lightsOnCoordinates:(NSArray *)coordinates                    onQueue:(dispatch_queue_t)queue          completionHandler:(void(^)(NSArray * results, BOOL success))completion;

顺便就可以一气把类声明给写出来了

@interface DHLightsOffAI : NSObject <NSCopying>+ (DHLightsOffAI *)sharedAI;- (id)copyWithZone:(NSZone *)zone;+ (instancetype)allocWithZone:(struct _NSZone *)zone;/** *  使用AI寻找一局关灯游戏的解法 * *  @param row         要解决的这局关灯游戏灯的行数 *  @param column      要解决的这局关灯游戏灯的列数 *  @param coordinates 要解决的这局关灯游戏中当前哪些灯是亮着的,传入它们的坐标数组,数组中的元素是由NSValue对象代表的CGPoint *  @param queue       解决这局关灯游戏是异步进行的,这个参数指定了异步操作的队列。如果传入nil,则表示是默认的全局队列 *  @param completion  解决完成后的回调block,results参数表示找到的解法,元素是由NSValue对象代表的CGPoint,success表示此局游戏是否有解 */- (void)startResolveWithRow:(NSUInteger)row                     column:(NSUInteger)column        lightsOnCoordinates:(NSArray *)coordinates                    onQueue:(dispatch_queue_t)queue          completionHandler:(void(^)(NSArray * results, BOOL success))completion;@end

实现AI2.0

类实现和我们昨天的AI没什么太大的区别。

static DHLightsOffAI * lightsOffAI_ = nil;@interface DHLightsOffAI ()@property (nonatomic, strong) NSMutableArray * results;@property (nonatomic, strong) NSArray * lightsOnCoordinate;@property (nonatomic, assign) NSUInteger row;@property (nonatomic, assign) NSUInteger column;/** *  找到第一排灯的正确状态以确保在用最后一排的灯关掉倒数一排的灯后最后一排的灯直接全部处于关闭状态 */- (BOOL)_findFirstRowState;/** *  模拟关掉C语言二维数组中坐标为x,y的那个元素 * *  @param x x *  @param y y */- (void)_turnLightAtX:(int)x y:(int)y forLights:(int **)lightStates;@end@implementation DHLightsOffAI#pragma mark - singleton+ (DHLightsOffAI *)sharedAI{    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        lightsOffAI_ = [[self alloc] init];    });    return lightsOffAI_;}- (id)copyWithZone:(NSZone *)zone{    return self;}+ (instancetype)allocWithZone:(struct _NSZone *)zone{    if (!lightsOffAI_) {        lightsOffAI_ = [super allocWithZone:zone];    }    return lightsOffAI_;}#pragma mark - interface methods- (void)startResolveWithRow:(NSUInteger)row column:(NSUInteger)column lightsOnCoordinates:(NSArray *)coordinates onQueue:(dispatch_queue_t)queue completionHandler:(void(^)(NSArray * results, BOOL success))completion{    [self.results removeAllObjects];    self.row = row;    self.column = column;    self.lightsOnCoordinate = coordinates;    if (!queue) {        queue = dispatch_get_global_queue(0, 0);    }    dispatch_async(queue, ^{        BOOL state = [self _findFirstRowState];        if (completion) {            dispatch_async(dispatch_get_main_queue(), ^{                completion(_results, state);            });        }    });}#pragma mark - private methods- (BOOL)_findFirstRowState{    // no表示无解    BOOL state = NO;    NSUInteger row = _row;    NSUInteger column = _column;    // 用一个C语言二维数组来代表每个灯的状态,0表示关,1表示开    int ** stateArray = malloc(sizeof(int *) * row);    // 将数组里的元素先全部置零    for (int i = 0; i < row; i++) {        stateArray[i] = malloc(sizeof(int) * column);        memset(stateArray[i], 0, sizeof(int) * column);    }    // 将数组里的元素按照lightsOnCoordinate里的指示置为1    [self.lightsOnCoordinate enumerateObjectsUsingBlock:^(NSValue * coordinateValue, NSUInteger idx, BOOL *stop) {        CGPoint coordinate = [coordinateValue CGPointValue];        NSUInteger x = coordinate.x;        NSUInteger y = coordinate.y;        stateArray[y][x] = 1;    }];    // 用一个column位的二进制数表示第一排灯按不按的状态,1表示按,0表示不按    // firstLineState二进制表示的后column位是第一排灯按不按    int firstLineState = 0;    for (int i = 0; i < pow(2, column); i++) {        // n = 1000...00, 1后面column-1个0        int n = pow(2, column-1);        // 比如resultArray里面的内容是 00101,则表示第一排第三个和第五个灯泡按一下        int * resultArray = malloc(sizeof(int) * column);        memset(resultArray, 0, sizeof(int) * column);        for (int j = 0; j < column; j++) {            int result = firstLineState & n;            n = n >> 1;            if (result) {                resultArray[j] = 1;            }        }        // 拷贝一份stateArray        int ** temp = malloc(sizeof(int *) * row);        for (int i = 0; i < row; i++) {            temp[i] = malloc(sizeof(int) * column);            memcpy(temp[i], stateArray[i], sizeof(int) * column);        }        // 根据resultArray关掉第一排        for (int i = 0; i < column; i++) {            if (resultArray[i]) {                [self _turnLightAtX:i y:0 forLights:temp];                [self.results addObject:[NSValue valueWithCGPoint:CGPointMake(i, 0)]];            }        }        // 依次关掉后面的灯        for (int i = 1; i < row; i++) {            for (int j = 0; j < column; j++) {                if (temp[i-1][j] == 1) {                    [self _turnLightAtX:j y:i forLights:temp];                    [self.results addObject:[NSValue valueWithCGPoint:CGPointMake(j, i)]];                }            }        }        // 看最后一行里面是不是全为0,如果全为0,则break        int lastResult = 0;        for (int i = 0; i < column; i++) {            lastResult += temp[row - 1][i];        }        if (lastResult == 0) {            free(resultArray);            for (int i = 0; i < row; i++) {                free(temp[i]);            }            free(temp);            state = YES;            NSLog(@"解法:%@",self.results);            break;        } else {            [self.results removeAllObjects];        }        firstLineState++;        free(resultArray);        for (int i = 0; i < row; i++) {            free(temp[i]);        }        free(temp);    }    for (int i = 0; i < row; i++) {        free(stateArray[i]);    }    free(stateArray);    return state;}- (void)_turnLightAtX:(int)x y:(int)y forLights:(int **)lightStates{    lightStates[y][x] = !lightStates[y][x];    if (y-1 >= 0) {        lightStates[y-1][x] = !lightStates[y-1][x];    }    if (x-1 >= 0) {        lightStates[y][x-1] = !lightStates[y][x-1];    }    if (y+1 <  _row) {        lightStates[y+1][x] = !lightStates[y+1][x];    }    if (x+1 <  _column) {        lightStates[y][x+1] = !lightStates[y][x+1];    }}#pragma mark - getter- (NSMutableArray *)results{    if (!_results) {        _results = [NSMutableArray arrayWithCapacity:0];    }    return _results;}@end

Redesign DHLightView

DHLightView将根据我们的需求进行更新。我们这个版本主要是寻找任意关灯游戏的解并用界面展示出来。

如何展示该局游戏的解法呢?我想到的一种是用户首先根据他想要解的游戏设置行数和列数,界面就会按行数和列数绘制出来,此时所有灯都是关闭状态。然后用户可以点击界面上的灯来设置出他想要解的那局游戏当前哪些灯是亮着的,这时点击灯就只是单纯的修改点击的这个灯的状态而不影响其他的灯。当用户设置完了以后使用AI求解,解出来的结果将所有需要点击的灯闪烁起来以告知用户这些就是你应该去点击的灯。
用户可以随意修改灯的状态并以当前状态再次求解。

效果设计好了以后,DHLightView就需要新增一个功能:闪烁动画。而为了实现用户随时可以修改当前游戏的状态并再次求解,灯需要随时停止闪烁,所以要有方法停止闪烁动画。

于是我们的DHLightView的类声明就可以写出来了:

#define DH_INLINE static inlinetypedef struct _DHCoordinate {    NSUInteger x;    NSUInteger y;} DHCoordinate;DH_INLINE DHCoordinate DHCoordinateMake(NSUInteger x, NSUInteger y) {    DHCoordinate coordinate;    coordinate.x = x; coordinate.y = y;    return coordinate;}DH_INLINE DHCoordinate DHCoordinateMakeWithCGPoint(CGPoint point) {    DHCoordinate coordinate;    coordinate.x = (NSUInteger)point.x;    coordinate.y = (NSUInteger)point.y;    return coordinate;}@interface DHLightView : UIControl/** *  坐标,第几行第几列 */@property (nonatomic, assign) DHCoordinate coordinate;/** *  默认关闭 */@property (nonatomic, assign) BOOL isOn;- (void)startFocusAnimation;- (void)stopFocusAnimation;- (instancetype)initWithFrame:(CGRect)frame coordinate:(DHCoordinate)coordinate;- (instancetype)initWithFrame:(CGRect)frame;@end

实现和昨天的博客没多大区别,主要是现在的DHLightView默认都是关闭状态而不是随机状态了。

#import "DHLightView.h"static NSString * const kDHLightViewFocusAnimationKey = @"kDHLightViewFocusAnimationKey";@implementation DHLightView- (instancetype)initWithFrame:(CGRect)frame coordinate:(DHCoordinate)coordinate{    self = [super initWithFrame:frame];    if (self) {        self.coordinate = coordinate;        self.layer.borderColor = [UIColor blackColor].CGColor;        self.layer.borderWidth = 2;        self.layer.cornerRadius = 3;        self.layer.masksToBounds = YES;        self.isOn = NO;    }    return self;}- (instancetype)initWithFrame:(CGRect)frame{    self = [self initWithFrame:frame coordinate:DHCoordinateMake(0, 0)];    return self;}#pragma mark - interface methods- (void)startFocusAnimation{    CABasicAnimation * animation = [CABasicAnimation animation];    animation.keyPath = @"opacity";    animation.fromValue = @1;    animation.toValue = @0.1;    animation.duration = 1;    animation.autoreverses = YES;    animation.repeatCount = CGFLOAT_MAX;    [self.layer addAnimation:animation forKey:kDHLightViewFocusAnimationKey];}- (void)stopFocusAnimation{    [self.layer removeAnimationForKey:kDHLightViewFocusAnimationKey];}#pragma mark - setter- (void)setIsOn:(BOOL)isOn{    _isOn = isOn;    if (isOn) {        self.backgroundColor = [UIColor yellowColor];    } else {        self.backgroundColor = [UIColor darkGrayColor];    }}@end

Redesign DHLightGameManager

我们的GM现在不需要AI属性了,它现在只负责展示界面,甚至关灯游戏的逻辑都不需要再去实现了(当然除非你需要)。
现在的GM只负责根据用户传入的行数和列数初始化一个关灯游戏界面,灯全部为关闭状态。然后用户点击一盏灯改变这盏灯的状态。用户可以随时调用方法进行求解,求解成功后就在界面上让那些需要进行点击的灯闪烁起来。

于是乎GM相对于昨天,将多一个方法:

- (void)showResolveResultsWithFailureHandler:(void(^)(void))failure;

现在,我们来实现GM的类声明

#import <Foundation/Foundation.h>#import <UIKit/UIKit.h>@interface DHLightGameManager : NSObject<NSCopying>@property (nonatomic, assign, readonly) NSUInteger row;@property (nonatomic, assign, readonly) NSUInteger column;/** *  主要负责显示的视图,需手动设置frame */@property (nonatomic, strong, readonly) UIView * mainView;+ (DHLightGameManager *)defaultManager;- (id)copyWithZone:(NSZone *)zone;+ (instancetype)allocWithZone:(struct _NSZone *)zone;/** *  强制移除视图所占的内存 */- (void)forceDealloc;/** *  重新开始游戏 */- (void)reset;/** *  对所有的灯泡进行布局 * *  @param row    灯泡的行数 *  @param column 灯泡的列数 */- (void)layoutLightsWithRow:(NSUInteger)row column:(NSUInteger)column;/** *  展示解法,解法将通过对需要点击的灯进行闪烁来展示 * *  @param failure 如果没有解,将回调这个block */- (void)showResolveResultsWithFailureHandler:(void(^)(void))failure;@end

在用户想要求解的时候,GM将调用AI的方法获取解法的数组,然后通过这个数组来闪烁对应的灯。
GM调用AI的方法必须要知道当前哪些灯是开着的状态,所以需要一个数组来记录开着的灯的坐标。
所以在延展里面多了一个私有属性:

@interface DHLightGameManager ()@property (nonatomic, strong) UIView * mainView;@property (nonatomic, strong) NSMutableArray * lightsOnCoordinate;@property (nonatomic, strong) NSMutableArray * lightsViewContainer;- (DHLightView *)_lightViewWithCoordinate:(DHCoordinate)coordinate;@end

GM和昨天相比需要多实现一个接口方法,具体的实现就是调用AI的方法在成功后根据解法数组将对应的灯闪烁起来。

- (void)showResolveResultsWithFailureHandler:(void (^)(void))failure{    [self.lightsViewContainer makeObjectsPerformSelector:@selector(stopFocusAnimation)];    [[DHLightsOffAI sharedAI] startResolveWithRow:_row column:_column lightsOnCoordinates:self.lightsOnCoordinate onQueue:nil completionHandler:^(NSArray *results, BOOL success) {        if (success) {            [results enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {                NSValue * coordinateValue = (NSValue *)obj;                DHCoordinate coordinate = DHCoordinateMakeWithCGPoint([coordinateValue CGPointValue]);                DHLightView * lightView = [self _lightViewWithCoordinate:coordinate];                [lightView startFocusAnimation];            }];        } else {            if (failure) {                failure();            }        }    }];}

完整的类实现:

#import "DHLightGameManager.h"#import "DHLightView.h"#import "DHLightsOffAI.h"static DHLightGameManager * manager_ = nil;static const CGFloat size_ = 40;static const CGFloat interval_ = 2;@implementation DHLightGameManager#pragma mark - singleton+ (DHLightGameManager *)defaultManager{    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        manager_ = [[self alloc] init];    });    return manager_;}- (id)copyWithZone:(NSZone *)zone{    return self;}+ (instancetype)allocWithZone:(struct _NSZone *)zone{    if (!manager_) {        manager_ = [super allocWithZone:zone];    }    return manager_;}#pragma mark - interface methods- (void)forceDealloc{    [self.lightsViewContainer makeObjectsPerformSelector:@selector(removeFromSuperview)];    self.lightsViewContainer = nil;    [self.mainView removeFromSuperview];    self.mainView = nil;    self.lightsOnCoordinate = nil;}- (void)reset{    [self.lightsOnCoordinate removeAllObjects];    [self.lightsViewContainer enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {        DHLightView * lightView = obj;        lightView.isOn = arc4random()%2;        [lightView stopFocusAnimation];    }];    [self.lightsViewContainer removeAllObjects];}- (void)layoutLightsWithRow:(NSUInteger)row column:(NSUInteger)column{    self.mainView.frame = CGRectMake(0, 0, column * size_ + (column - 1) * interval_, row * size_ + (row - 1) * interval_);    [self reset];    _row = row;    _column = column;    for (int i = 0; i < row; i++) {        for (int j = 0; j < column; j++) {            DHLightView * lightView = [[DHLightView alloc] initWithFrame:CGRectMake(0, 0, size_, size_) coordinate:DHCoordinateMake(j, i)];            lightView.center = CGPointMake(j * (size_ + interval_)+ size_/2, i * (size_ + interval_) + size_/2);            [lightView addTarget:self action:@selector(onLight:) forControlEvents:UIControlEventTouchUpInside];            [self.lightsViewContainer addObject:lightView];            [self.mainView addSubview:lightView];        }    }}- (void)showResolveResultsWithFailureHandler:(void (^)(void))failure{    [self.lightsViewContainer makeObjectsPerformSelector:@selector(stopFocusAnimation)];    [[DHLightsOffAI sharedAI] startResolveWithRow:_row column:_column lightsOnCoordinates:self.lightsOnCoordinate onQueue:nil completionHandler:^(NSArray *results, BOOL success) {        if (success) {            [results enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {                NSValue * coordinateValue = (NSValue *)obj;                DHCoordinate coordinate = DHCoordinateMakeWithCGPoint([coordinateValue CGPointValue]);                DHLightView * lightView = [self _lightViewWithCoordinate:coordinate];                [lightView startFocusAnimation];            }];        } else {            if (failure) {                failure();            }        }    }];}#pragma mark - private methods- (DHLightView *)_lightViewWithCoordinate:(DHCoordinate)coordinate{    // 根据总行数和当前坐标算出数组下标    // 因为放进数组的顺序是一行一行的放的    // 每行有_row个灯    NSInteger index = coordinate.y * _column + coordinate.x;    return self.lightsViewContainer[index];}#pragma mark - action- (void)onLight:(DHLightView *)sender{    sender.isOn = !sender.isOn;    CGPoint point = CGPointMake(sender.coordinate.x, sender.coordinate.y);    if (sender.isOn) {        [self.lightsOnCoordinate addObject:[NSValue valueWithCGPoint:point]];    } else {        [self.lightsOnCoordinate removeObject:[NSValue valueWithCGPoint:point]];    }}#pragma mark - getter- (UIView *)mainView{    if (!_mainView) {        _mainView = [[UIView alloc] init];        _mainView.backgroundColor = [UIColor groupTableViewBackgroundColor];    }    return _mainView;}- (NSMutableArray *)lightsOnCoordinate{    if (!_lightsOnCoordinate) {        _lightsOnCoordinate = [NSMutableArray arrayWithCapacity:0];    }    return _lightsOnCoordinate;}- (NSMutableArray *)lightsViewContainer{    if (!_lightsViewContainer) {        _lightsViewContainer = ({            NSMutableArray * array = [NSMutableArray arrayWithCapacity:0];            array;        });    }    return _lightsViewContainer;}@end

最后在ViewController中简单的调用一下:

@implementation ViewController- (void)viewDidLoad {    [super viewDidLoad];    DHLightGameManager * manager = [DHLightGameManager defaultManager];    [manager layoutLightsWithRow:6 column:5];    [self.view addSubview:manager.mainView];    manager.mainView.center = self.view.center;}- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{    [[DHLightGameManager defaultManager] showResolveResultsWithFailureHandler:^{        NSLog(@"无解");    }];}@end

这里必须说一下,写出这个工程以后我发现我昨天说的只有一盏灯亮着无解是错的。。上一篇博客的内容我不会改了,当给自己一个教训,也希望大家引以为鉴:凡事不要想当然,下结论要对得起自己的逻辑。

就一盏灯亮着怎么关?我来给你们演示一哈
因为没有实现关灯游戏的主要逻辑,我就自己把要点的那盏灯周围的灯都手动的点一下。
5

再来一发:

6

优化策略

从算法角度

我们的算法是穷举第一排点击的情况,既然我们可以用后一排关掉前一排,同样的可以用后一列关掉前一列,算法是差不多的。遍历第一列要考虑最多2^row种情况,遍历第一行要考虑2^column种情况。如果列数大于行数,那么遍历第一排的成本就大于遍历第一列的成本,我们就可以选择用后一列关掉前一列的算法,反之如果行数大于列数,那么遍历第一列的成本就大于遍历第一排,我们就选择用后一排关掉前一排的算法,这是从算法上可以进行优化的地方

从数学角度

如果引进线性代数,我们整个算法就将被完全颠覆。
这里我之所以没有用线性代数来解,因为这需要开发人员具备良好的线性代数知识,很明显这门大学的课程往往不被软件专业的大学生所重视。

没有,我的意思是,我的线性代数其实还挺不错,但是我怕我写在这里,读者不太能理解(仰天大笑)。

所以干脆就把传送门放在这里

关灯游戏中的代数学

如果这个看起来太吃力,这里有一篇稍微简单一点的

求解关灯游戏

Extra Hyperlink

这个新的工程文件放到了这个Git仓库里。

顺便如果没有看过我的上一篇博客,请前往观摩。

0 0
原创粉丝点击