1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 """A Python engine for Risk-like games
22 from .log import Logger
25 class PlayerError (Exception):
28 class NameMixin (object):
29 """Simple mixin for pretty-printing named objects.
31 def __init__(self, name):
39 class ID_CmpMixin (object):
40 """Simple mixin to ensure the fancier comparisons are all based on
43 def __cmp__(self, other):
44 return cmp(id(self), id(other))
45 def __eq__(self, other):
46 return self.__cmp__(other) == 0
47 def __ne__(self, other):
48 return self.__cmp__(other) != 0
50 class Territory (NameMixin, ID_CmpMixin, list):
51 """An occupiable territory.
53 Contains a list of neighboring territories.
55 def __init__(self, name, short_name=None, type=-1,
56 link_names=[], continent=None, player=None):
57 NameMixin.__init__(self, name)
58 ID_CmpMixin.__init__(self)
60 self.short_name = short_name
61 if short_name == None:
62 self.short_name = name
63 self._card_type = type # for Deck construction
64 self._link_names = list(link_names) # used by World._resolve_link_names
65 self.continent = continent # used by World.production
66 self.player = player # who owns this territory
67 self.armies = 0 # number of occupying armies
69 if self.short_name == self.name:
71 return '%s (%s)' % (self.name, self.short_name)
72 def borders(self, other):
74 if id(t) == id(other):
78 class Continent (NameMixin, ID_CmpMixin, list):
79 """A group of Territories.
81 Stores the army-production bonus if it's owned by a single player.
83 def __init__(self, name, production, territories=[]):
84 NameMixin.__init__(self, name)
85 ID_CmpMixin.__init__(self)
86 list.__init__(self, territories)
87 self.production = production
88 def append(self, territory):
89 """Add a new territory (setting the territory's .continent
92 list.append(self, territory)
93 territory.continent = self
94 def territory_by_name(self, name):
95 """Find a Territory instance by name (long or short, case
99 if name.lower() in [t.short_name.lower(), t.name.lower()]:
102 def single_player(self):
103 """Is the continent owned by a single player?
106 for territory in self:
107 if territory.player != p:
111 class World (NameMixin, ID_CmpMixin, list):
112 """Store the world map and current world state.
114 Holds list of Continents. Also controls territory-based army
115 production (via production).
117 def __init__(self, name, continents=[]):
118 NameMixin.__init__(self, name)
119 ID_CmpMixin.__init__(self)
120 list.__init__(self, continents)
121 self.initial_armies = { # num_players:num_armies
122 2: 40, 3:35, 4:30, 5:25, 6:20
124 def territories(self):
125 """Iterate through all the territories in the world.
127 for continent in self:
128 for territory in continent:
130 def territory_by_name(self, name):
131 """Find a Territory instance by name (long or short, case
134 for continent in self:
136 return continent.territory_by_name(name)
140 def continent_by_name(self, name):
141 """Find a Continent instance by name (case insensitive).
143 for continent in self:
144 if continent.name.lower() == name.lower():
147 def _resolve_link_names(self):
148 """Initialize Territory links.
150 The Territory class doesn't actually link to neighbors after
151 initialization, but one of each linked pair has the others
152 name in _link_names. This method goes through the territories,
153 looks up the referenced link target, and joins the pair.
155 self._check_short_names()
156 for territory in self.territories():
157 for name in territory._link_names:
158 other = self.territory_by_name(name)
159 if not territory.borders(other):
160 territory.append(other)
161 if not other.borders(territory):
162 other.append(territory)
163 def _check_short_names(self):
164 """Ensure there are no short_name collisions.
167 for t in self.territories():
168 if t.short_name.lower() not in ts:
169 ts[t.short_name.lower()] = t
171 raise ValueError('%s shared by %s and %s'
172 % (t.short_name.lower(), ts[t.short_name.lower()], t))
173 def production(self, player):
174 """Calculate the number of armies a player should earn based
175 on territory occupation.
177 ts = list(player.territories(self))
178 production = max(3, len(ts) / 3)
179 continents = set([t.continent.name for t in ts])
180 for c_name in continents:
181 c = self.continent_by_name(c_name)
182 if c.single_player() == True:
183 production += c.production
184 return (production, {})
185 def place_territory_production(self, territory_production):
186 """Place armies based on {territory_name: num_armies, ...}.
188 for territory_name,production in territory_production.items():
189 t = self.territory_by_name(territory_name)
190 t.armies += production
192 class Card (ID_CmpMixin):
193 """Represent a territory card (or wild)
195 Nothing exciting going on here, just a class for pretty-printing
198 def __init__(self, deck, type_, territory=None):
199 ID_CmpMixin.__init__(self)
201 self.territory = territory
204 if self.territory == None:
205 return '<Card %s>' % (self.deck.type_names[self.type])
207 return '<Card %s %s>' % (self.territory,
208 self.deck.type_names[self.type])
210 return self.__str__()
213 """All the cards yet to be handed out in a given game.
215 Controls the type branding (via type_names) and army production
216 values for scoring sets (via production_value).
218 def __init__(self, territories=[], num_wilds=2,
219 type_names=['Wild', 'Infantry', 'Cavalry', 'Artillery']):
220 list.__init__(self, [Card(self, t._card_type, t) for t in territories])
221 self.type_names = type_names
222 for i in range(num_wilds):
223 self.append(Card(self, 0))
224 self._production_sequence = [4, 6, 8, 10, 12, 15]
225 self._production_index = 0
227 """Shuffle the remaining cards in the deck.
230 def production_value(self, index):
233 >>> [d.production_value(i) for i in range(8)]
234 [4, 6, 8, 10, 12, 15, 20, 25]
236 if index < len(self._production_sequence):
237 return self._production_sequence[index]
238 extra = index - len(self._production_sequence) + 1
239 return self._production_sequence[-1] + 5 * extra
240 def production(self, player, cards=None):
243 >>> a = Player('Alice')
244 >>> b = Player('Bob')
245 >>> d.production(a, None)
247 >>> d.production(a, [Card(d, 1, Territory('a')),
248 ... Card(d, 1, Territory('b'))])
249 Traceback (most recent call last):
251 PlayerError: [<Card a Infantry>, <Card b Infantry>] is not a scoring set
252 >>> d.production(a, [Card(d, 1, Territory('a', player=a)),
253 ... Card(d, 1, Territory('b', player=b)),
254 ... Card(d, 1, Territory('c'))])
256 >>> p,tp = d.production(a, [Card(d, 1, Territory('a', player=a)),
257 ... Card(d, 2, Territory('b', player=a)),
258 ... Card(d, 0, Territory('c', player=a))])
261 >>> sorted(tp.items())
262 [('a', 1), ('b', 1), ('c', 1)]
268 p = self.production_value(self._production_index)
269 self._production_index += 1
270 territory_production = {}
272 if c.territory != None and c.territory.player == player:
273 territory_production[c.territory.name] = 1
274 return (p, territory_production)
275 raise PlayerError('%s is not a scoring set' % h)
278 """Represent a hand of cards.
280 This is the place to override the set of allowed scoring
281 combinations. You should override one of
287 Adding additional scoring methods as needed (e.g. flush).
289 def __init__(self, cards=[]):
290 list.__init__(self, cards)
294 s = sorted(set([card.type for card in self]))
296 or (len(s) == 2 and s[0] == 0):
302 if len(set([card.type for card in self])) == 3:
306 """The hand is any valid scoring combination.
308 return self.set() or self.run()
309 def subhands(self, lengths=None):
310 """Return all possible subhands.
312 Lengths can either be a list of allowed subhand lengths or
313 None. If None, all possible subhand lengths are allowed.
316 >>> h = Hand([Card(d, 1, Territory('a')),
317 ... Card(d, 1, Territory('b')),
318 ... Card(d, 1, Territory('c')),
319 ... Card(d, 1, Territory('d'))])
320 >>> for hand in h.subhands():
326 [<Card a Infantry>, <Card b Infantry>]
327 [<Card a Infantry>, <Card c Infantry>]
328 [<Card a Infantry>, <Card d Infantry>]
329 [<Card b Infantry>, <Card c Infantry>]
330 [<Card b Infantry>, <Card d Infantry>]
331 [<Card c Infantry>, <Card d Infantry>]
332 [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>]
333 [<Card a Infantry>, <Card b Infantry>, <Card d Infantry>]
334 [<Card a Infantry>, <Card c Infantry>, <Card d Infantry>]
335 [<Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
336 [<Card a Infantry>, <Card b Infantry>, <Card c Infantry>, <Card d Infantry>]
338 for i in range(len(self)):
339 i += 1 # check all sub-hands of length i
340 if lengths != None and i not in lengths:
341 continue # don't check this length
343 stop = range(len(self)-i, len(self))
344 while indices != stop:
345 yield Hand([self[i] for i in indices])
346 indices = self._increment(indices, stop)
347 yield Hand([self[i] for i in indices])
348 def _increment(self, indices, stop):
351 >>> h = Hand([Card(d, 1, Territory('a'))])
352 >>> h._increment([0, 1, 2], [2, 3, 4])
354 >>> h._increment([0, 1, 3], [2, 3, 4])
356 >>> h._increment([0, 1, 4], [2, 3, 4])
359 moveable = [i for i,m in zip(indices, stop) if i < m]
360 assert len(moveable) > 0, 'At stop? indices: %s, stop: %s' % (indices, stop)
361 key = indices.index(moveable[-1])
362 new = indices[key] + 1
363 for i in range(key, len(indices)):
364 indices[i] = new + i-key
367 """Return a list of all possible scoring subhands.
369 for h in self.subhands():
373 class Player (NameMixin, ID_CmpMixin):
374 """Represent a risk player.
376 This class implements a very basic AI player. Subclasses should
377 consider overriding the "action-required" methods:
385 And the "report" methods:
390 def __init__(self, name):
391 NameMixin.__init__(self, name)
392 ID_CmpMixin.__init__(self)
395 self._message_index = 0
396 def territories(self, world):
397 """Iterate through all territories owned by this player.
399 for t in world.territories():
402 def border_territories(self, world):
403 """Iterate through all territories owned by this player which
404 border another player's territories.
406 for t in self.territories(world):
408 if neighbor.player != self:
411 def report(self, world, log):
412 """Send reports about death and game endings.
414 These events mark the end of contact and require no change in
415 player status or response, so they get a special command
416 seperate from the usual action family. The action commands in
417 Player subclasses can notify the player (possibly by calling
418 report internally) if they feel so inclined.
422 draw - another notification-only method
424 print 'Reporting for %s:\n %s' \
425 % (self, '\n '.join(log[self._message_index:]))
426 self._message_index = len(log)
427 def draw(self, world, log, cards=[]):
428 """Only called if you earned a new card (or cards).
432 report - another notification-only method
435 def select_territory(self, world, log):
436 """Return the selected territory's name.
438 free_territories = [t for t in world.territories() if t.player == None]
439 return random.sample(free_territories, 1)[0].name
440 def play_cards(self, world, log, play_required=True):
441 """Decide whether or not to turn in a set of cards.
443 Return a list of cards to turn in or None. If play_required
444 is True, you *must* play.
446 if play_required == True:
447 return random.sample(list(self.hand.possible()), 1)[0]
448 def place_armies(self, world, log, remaining=1, this_round=1):
449 """Both during setup and before each turn.
451 Return {territory_name: num_armies, ...}
453 t = random.sample(list(self.border_territories(world)), 1)[0]
454 return {t.name: this_round}
455 def attack_and_fortify(self, world, log, mode='attack'):
456 """Return list of (source, target, armies) tuples. Place None
457 in the list to end this phase.
459 assert mode != 'fortify', mode
460 possible_attacks = []
461 for t in self.border_territories(world):
462 if t.armies <= 3: #1: # be more conservative, only attack with 3 dice
464 targets = [border_t for border_t in t if border_t.player != self]
466 possible_attacks.append((t.name, tg.name, min(3, t.armies-1)))
467 if len(possible_attacks) == 0:
468 return [None, None] # stop attack phase, then stop fortification phase
469 return random.sample(possible_attacks, 1) # + [None]
470 def support_attack(self, world, log, source, target):
471 """Follow up on a conquest by moving additional armies.
473 return source.armies-1
475 class Engine (ID_CmpMixin):
478 Basic usage will be along the lines of
480 >>> world = generate_earth()
481 >>> players = [Player('Alice'), Player('Bob'), Player('Charlie')]
482 >>> e = Engine(world, players)
483 >>> e.run() # doctest: +ELLIPSIS
486 def __init__(self, world, players, deck_class=Deck, logger_class=Logger):
487 ID_CmpMixin.__init__(self)
489 self.deck = deck_class(world.territories())
490 self.log = logger_class()
491 self.players = players
493 return '<engine %s %s>' % (self.world, self.players)
495 return self.__str__()
497 """The main entry point.
503 """Setup phase. Pick territories, place initial armies, and
506 for p in self.players:
508 random.shuffle(self.players)
509 self.log('Game started with %s' % self.players)
511 self.select_territories()
512 self.place_initial_armies()
513 for p in self.players:
516 """Main gameplay phase. Take turns until only one Player survives.
520 living = len(self.living_players())
522 self.play_turn(self.players[active_player])
523 living = len(self.living_players())
524 active_player = (active_player + 1) % len(self.players)
526 while self.players[active_player].alive == False:
527 active_player = (active_player + 1) % len(self.players)
530 """The end of the game.
532 Currently just a notification hook.
534 self.log('Game over.')
535 for p in self.players:
536 p.report(self.world, self.log)
537 def play_turn(self, player):
538 """Work through the phases of player's turn.
540 self.log("%s's turn (territory score: %s)"
541 % (player, [(p,len(list(p.territories(self.world))))
542 for p in self.players]))
543 self.play_cards_and_place_armies(player)
544 captures = self.attack_and_fortify(player)
545 self.end_of_turn_cards(player, captures)
546 def select_territories(self):
547 for t in self.world.territories():
549 for i in range(len(list(self.world.territories()))):
550 p = self.players[i % len(self.players)]
551 t_name = p.select_territory(self.world, self.log)
552 t = self.world.territory_by_name(t_name)
554 raise PlayerError('Cannot select %s owned by %s'
556 self.log('%s selects %s' % (p, t))
559 def place_initial_armies(self):
560 already_placed = [len(list(p.territories(self.world))) for p in self.players]
561 s = list(set(already_placed))
562 assert len(s) in [1,2], already_placed
563 if len(s) == 2: # catch up the players who are one territory short
564 assert min(s) == max(s)-1, 'Min %d, max %d' % (min(s), max(s))
565 for p,placed in zip(self.players, already_placed):
567 self.player_place_armies(p, remaining, 1)
568 remaining = self.world.initial_armies[len(self.players)] - max(s)
570 for p in self.players:
571 self.player_place_armies(p, remaining, 1)
573 def player_place_armies(self, player, remaining=1, this_round=1):
574 placements = player.place_armies(self.world, self.log, remaining, this_round)
575 if sum(placements.values()) != this_round:
576 raise PlayerError('Placing more than %d armies' % this_round)
577 for ter_name,armies in placements.items():
578 t = self.world.territory_by_name(ter_name)
579 if t.player != player:
580 raise PlayerError('Placing armies in %s owned by %s'
583 raise PlayerError('Placing a negative number of armies (%d) in %s'
585 self.log('%s places %s' % (player, placements))
586 for terr_name,armies in placements.items():
587 t = self.world.territory_by_name(terr_name)
589 def deal(self, player, number):
591 for i in range(number):
592 cards.append(self.deck.pop())
593 player.hand.extend(cards)
594 player.draw(self.world, self.log, cards)
595 self.log('%s dealt %d cards' % (player, number))
596 def play_cards_and_place_armies(self, player, additional_armies=0):
597 cards_required = len(player.hand) >= 5
598 cards = player.play_cards(
599 self.world, self.log, play_required=cards_required)
600 if cards_required == True and cards == None:
601 raise PlayerError('You have %d >= 5 cards in your hand, you must play'
603 w_prod,w_terr_prod = self.world.production(player)
604 self.log('%s earned %d armies from territories' % (player, w_prod))
605 c_prod,c_terr_prod = self.deck.production(player, cards)
607 self.log('%s played %s, earning %d armies'
608 % (player, cards, c_prod+sum(c_terr_prod.values())))
611 player.hand.remove(c)
612 for terr,prod in c_terr_prod.items():
613 if terr in w_terr_prod:
614 w_terr_prod[terr] += prod
616 w_terr_prod[terr] = prod
617 self.world.place_territory_production(w_terr_prod)
618 if len(w_terr_prod) > 0:
619 self.log('%s was required to place %s' % (player, w_terr_prod))
620 armies = w_prod + c_prod
621 self.player_place_armies(player, armies, armies)
622 def attack_and_fortify(self, player):
626 actions = player.attack_and_fortify(self.world, self.log, mode)
627 for action in actions:
633 assert mode == 'fortify', mode
635 source_name,target_name,armies = action
636 source = self.world.territory_by_name(source_name)
637 target = self.world.territory_by_name(target_name)
638 if not source.borders(target):
639 raise PlayerError('Cannot reach %s from %s to %s'
640 % (target, source, mode))
642 tplayer = target.player
643 capture = self.attack(source, target, armies)
646 if len(list(tplayer.territories(self.world))) == 0:
647 self.player_killed(tplayer, killer=player)
649 assert mode == 'fortify', mode
650 self.fortify(source, target, armies)
651 def attack(self, source, target, armies):
652 if source.player == target.player:
653 raise PlayerError('%s attacking %s, but you own both.'
656 raise PlayerError('%s attacking %s with 0 armies.'
658 if armies >= source.armies:
659 raise PlayerError('%s attacking %s with %d armies, but only %d are available.'
660 % (source, target, armies, source.armies-1))
661 a_dice = sorted([random.randint(1, 6) for i in range(armies)],
663 t_dice = sorted([random.randint(1, 6) for i in range(min(2, target.armies))],
667 for a,d in zip(a_dice, t_dice):
672 source.armies -= a_dead
673 target.armies -= t_dead
674 if target.armies == 0:
675 self.takeover(source, target, remaining_attackers=armies-a_dead)
676 self.log('%s conquered %s from %s with %d:%d. Deaths %d:%d. Remaining %d:%d'
677 % (source.player, target, source, armies, len(t_dice),
678 a_dead, t_dead, source.armies, target.armies))
679 assert target.armies > 0, target
681 self.log('%s attacked %s from %s with %d:%d. Deaths %d:%d. Remaining %d:%d' \
682 % (source.player, target, source, armies, len(t_dice),
683 a_dead, t_dead, source.armies, target.armies))
684 assert target.armies > 0, target
686 def takeover(self, source, target, remaining_attackers):
687 source.armies -= remaining_attackers
688 target.armies += remaining_attackers
689 target.player = source.player
690 if source.armies > 1:
691 support = source.player.support_attack(
692 self.world, self.log, source, target)
693 if support < 0 or support >= source.armies:
695 'Cannot support from %s to %s with %d armies, only %d available'
696 % (source, target, support, source.armies-1))
697 source.armies -= support
698 target.armies += support
699 def player_killed(self, player, killer):
701 killer.hand.extend(player.hand)
702 if len(self.living_players()) > 1:
703 while len(killer.hand) > 5:
704 self.play_cards_and_place_armies(killer)
705 self.log('%s killed by %s' % (player, killer))
706 if len(self.living_players()) > 1:
707 player.report(self.world, self.log)
708 # else the game is over, and killed will hear about this then.
709 def end_of_turn_cards(self, player, captures):
710 """Deal end-of-turn reward for any territory captures.
712 if captures > 0 and len(self.deck) > 0 and len(self.living_players()) > 1:
714 def living_players(self):
715 return [p for p in self.players if p.alive == True]
718 def generate_earth():
720 c = Continent('North America', 5)
721 c.append(Territory('Alaska', 'ala', 1, ['kam', 'nwt']))
722 c.append(Territory('Northwest Territory', 'nwt', 2, ['alb', 'ont', 'gre']))
723 c.append(Territory('Greenland', 'gre', 3, ['ont', 'que', 'ice']))
724 c.append(Territory('Alberta', 'alb', 1, ['ont', 'wus']))
725 c.append(Territory('Ontario', 'ont', 2, ['wus', 'eus', 'que']))
726 c.append(Territory('Quebec', 'que', 3, ['eus']))
727 c.append(Territory('Western United States', 'wus', 1, ['eus', 'cam']))
728 c.append(Territory('Eastern United States', 'eus', 2, ['cam']))
729 c.append(Territory('Central America', 'cam', 3, ['ven']))
732 c = Continent('Europe', 5)
733 c.append(Territory('Iceland', 'ice', 1, ['gbr', 'sca']))
734 c.append(Territory('Scandanavia', 'sca', 2, ['gbr', 'neu', 'ukr']))
735 c.append(Territory('Ukraine', 'ukr', 3, ['neu', 'seu', 'ura', 'afg', 'mea']))
736 c.append(Territory('Great Britain', 'gbr', 1, ['neu', 'weu']))
737 c.append(Territory('Northern Europe', 'neu', 2, ['weu', 'seu']))
738 c.append(Territory('Western Europe', 'weu', 3, ['naf', 'seu']))
739 c.append(Territory('Southern Europe', 'seu', 1, ['naf', 'egy', 'mea']))
742 c = Continent('Asia', 7)
743 c.append(Territory('Urals', 'ura', 2, ['afg', 'chi', 'sib']))
744 c.append(Territory('Siberia', 'sib', 3, ['chi', 'mon', 'irk', 'yak']))
745 c.append(Territory('Yakutsk', 'yak', 1, ['irk', 'kam']))
746 c.append(Territory('Kamchatka', 'kam', 2, ['mon', 'jap']))
747 c.append(Territory('Irkutsk', 'irk', 3, ['mon']))
748 c.append(Territory('Mongolia', 'mon', 1, ['chi', 'jap']))
749 c.append(Territory('Japan', 'jap', 2))
750 c.append(Territory('Afghanistan', 'afg', 3, ['mea', 'indi', 'chi']))
751 c.append(Territory('China', 'chi', 1, ['indi', 'sia']))
752 c.append(Territory('Middle East', 'mea', 2, ['egy', 'eaf', 'indi']))
753 c.append(Territory('India', 'indi', 3, ['sia']))
754 c.append(Territory('Siam', 'sia', 1, ['indo']))
758 c = Continent('South America', 2)
759 c.append(Territory('Venezuala', 'ven', 2, ['per', 'bra']))
760 c.append(Territory('Peru', 'per', 3, ['arg', 'bra']))
761 c.append(Territory('Brazil', 'bra', 1, ['arg', 'naf']))
762 c.append(Territory('Argentina', 'arg', 2))
765 c = Continent('Africa', 3)
766 c.append(Territory('North Africa', 'naf', 3, ['egy', 'eaf', 'con']))
767 c.append(Territory('Egypt', 'egy', 1, ['eaf']))
768 c.append(Territory('East Africa', 'eaf', 2, ['con', 'saf', 'mad']))
769 c.append(Territory('Congo', 'con', 3, ['saf']))
770 c.append(Territory('South Africa', 'saf', 1, ['mad']))
771 c.append(Territory('Madagascar', 'mad', 2))
774 c = Continent('Australia', 2)
775 c.append(Territory('Indonesia', 'indo', 3, ['ngu', 'wau']))
776 c.append(Territory('New Guinea', 'ngu', 1, ['wau', 'eau']))
777 c.append(Territory('Western Australia', 'wau', 2, ['eau']))
778 c.append(Territory('Eastern Australia', 'eau', 3))
781 w._resolve_link_names()
786 failures,tests = doctest.testmod(sys.modules[__name__])
790 from player.email import IncomingEmailDispatcher, OutgoingEmailDispatcher, EmailPlayer
791 world = generate_earth()
792 ied = IncomingEmailDispatcher(fifo_path='/tmp/pyrisk.in')
793 oed = OutgoingEmailDispatcher(return_address='server@example.com')
794 players = [EmailPlayer('Alice', 'alice@big.edu'),
795 Player('Bob'), Player('Charlie')]
796 e = Engine(world, players)
800 if __name__ == '__main__':
802 failures = self.test()