in General

NSPredicates – SUBQuery on a parent-child relationship

I am currently working on a game where there is a parent-child relationship in a quite small array where;

A `Group` (parent)
has many `Child` objects
where 1 `Child` object can be owned by only 1 `Player`
and a `Player` can only ever own 1 `Child` per group

In the game, a `Child` object is called `Locomotive` (the game is about selling Locomotives) and a given `Player` can own a single locomotive in a given group.

I needed a way to grab all the child objects in a given array that met a series of conditions; in short, I’m after a list of child objects that I can `legally` purchase.

By `legally` I mean from the following rules (in no particular order)

  • I want to remove all children where I do not own anything
  • I want to remove all children which I already own a locomotive
  • I want to remove all children where I don’t have enough money to buy it
  • I want to remove all children by inspecting the group to only show those children where the sum total of orders > 0
  • I want to remove all children where something has been bought already

Phew, quite a lot of conditions. At first I was not sure how to solve so many conditions.

My first solution was to use a NSPredicate… with a block statement and simply loop through everything.


NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
EngYard_LocomotiveGroup *group = evaluatedObject;
for (EngYard_Locomotive *child in group.locomotives) {
if ( ([child.purchased boolValue] == NO) && (child.owner != player) && (group.purchasePrice < player.cash) && (group.initialOrders>0 || [group.totalOrders integerValue]>0 || group.unlocked==YES) )
{
return YES;
break;
}
return NO;
break;
}
return NO;
}];

This seemed quite a lot of code. Surely there is an easier way to do this?

SUBQUERY to the rescue!

Having played around with the SUBQUERY for a while, I finally got a single-line solution;


NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SUBQUERY(locomotives, $rv, $rv.purchased = %@).@count > 0 AND (purchasePrice < %d) AND (initialOrders>0 OR orders.@sum.integerValue>0 OR unlocked==YES)", @NO, player.cash];

I double checked both instances in my logger; and they both return my expected results.

Now on to more complex matters — how to make the AI make a decision to buy.