前言
在我发布昨天的博客第二天(不就是今天么)就有小伙伴来告诉我:“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(), ^{