1 /*
2  * Copyright (C) 2004 Ivo Danihelka (ivo@danihelka.net)
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  */
9 #include "Rules.h"
10 
11 #include "Cube.h"
12 #include "MarkMask.h"
13 
14 #include "Log.h"
15 #include "LayoutException.h"
16 #include "OnStack.h"
17 #include "OnWall.h"
18 #include "OnStrongPad.h"
19 #include "OptionAgent.h"
20 
21 #include <assert.h>
22 
23 //-----------------------------------------------------------------
24 /**
25  * Create new rules for model.
26  */
Rules(Cube * model)27 Rules::Rules(Cube *model)
28 {
29     m_readyToDie = false;
30     m_readyToTurn = false;
31     m_readyToActive = false;
32     m_dir = Dir::DIR_NO;
33     m_pushing = false;
34     m_outDepth = 0;
35     m_touchDir = Dir::DIR_NO;
36 
37     m_model = model;
38     m_mask = NULL;
39     m_lastFall = false;
40 }
41 //-----------------------------------------------------------------
42 /**
43  * Unmask from field.
44  */
~Rules()45 Rules::~Rules()
46 {
47     if (m_mask) {
48         m_mask->unmask();
49         delete m_mask;
50     }
51 }
52 //-----------------------------------------------------------------
53 /**
54  * Connect model with field.
55  * @throws LayoutException when location is occupied
56  */
57     void
takeField(Field * field)58 Rules::takeField(Field *field)
59 {
60     if (m_mask) {
61         m_mask->unmask();
62         delete m_mask;
63         m_mask = NULL;
64     }
65 
66     m_mask = new MarkMask(m_model, field);
67     Cube::t_models resist = m_mask->getResist(Dir::DIR_NO);
68     if (!resist.empty()) {
69         throw LayoutException(ExInfo("position is occupied")
70                 .addInfo("model", m_model->toString())
71                 .addInfo("resist", resist.front()->toString()));
72     }
73 
74     m_mask->mask();
75 }
76 
77 //-----------------------------------------------------------------
78 /**
79  * Accomplish last move in m_dir direction.
80  * Mask to a new position.
81  * Change model position.
82  *
83  * It complements the unmasking in freeOldPos().
84  */
85     void
occupyNewPos()86 Rules::occupyNewPos()
87 {
88     m_touchDir = Dir::DIR_NO;
89     if (m_dir != Dir::DIR_NO) {
90         m_pushing = false;
91 
92         V2 shift = Dir::dir2xy(m_dir);
93         V2 oldLoc = m_model->getLocation();
94         m_model->change_setLocation(oldLoc.plus(shift));
95 
96         m_mask->mask();
97     }
98 }
99 //-----------------------------------------------------------------
100 /**
101  * Force model to a new position.
102  * Used just for undo loading.
103  */
104     void
change_setLocation(const V2 & loc)105 Rules::change_setLocation(const V2 &loc)
106 {
107     m_mask->unmask();
108     //HACK: model.out flag is recognized by its location
109     if (loc.getX() < 0 && loc.getY() < 0) {
110         m_model->change_goOut();
111     }
112     else {
113         m_model->change_setLocation(loc);
114         m_mask->mask();
115     }
116 }
117 //-----------------------------------------------------------------
118 /**
119  * Check dead fishes.
120  * Fish is dead:
121  * - when any model moves in dir != Dir::DIR_UP
122  *   and new position is SOLELY on a fish
123  * - when any model moves in Dir::DIR_DOWN
124  *   and new position is SOLELY on models SOLELY on a fish
125  * - when any model rests SOLELY on models SOLELY on a fish
126  *   with fish.power < model.weight
127  *
128  * @return true when fish has died
129  */
130     bool
checkDead(Cube::eAction lastAction)131 Rules::checkDead(Cube::eAction lastAction)
132 {
133     //NOTE: after falling phase is sufficient to check only DeadFall
134     bool dead = false;
135 
136     if (m_model->isAlive()) {
137         switch (lastAction) {
138             case Cube::ACTION_FALL:
139                 dead = checkDeadFall();
140                 break;
141             case Cube::ACTION_MOVE:
142                 dead = checkDeadMove();
143                 break;
144             default:
145                 dead = false;
146                 break;
147         }
148 
149         if (!dead) {
150             dead = checkDeadStress();
151         }
152 
153         if (dead) {
154             m_readyToDie = true;
155         }
156     }
157 
158     return dead;
159 }
160 
161 //-----------------------------------------------------------------
162 /**
163  * Return true when any model moves in dir != Dir::DIR_UP
164  * and new position is SOLELY on a fish.
165  * @return true when fish is dead
166  */
167     bool
checkDeadMove()168 Rules::checkDeadMove()
169 {
170     bool strict = OptionAgent::agent()->getAsBool("strict_rules", true);
171 
172     Cube::t_models resist = m_mask->getResist(Dir::DIR_UP);
173     Cube::t_models::iterator end = resist.end();
174     for (Cube::t_models::iterator i = resist.begin(); i != end; ++i) {
175         if (!(*i)->isAlive()) {
176             Dir::eDir resist_dir = (*i)->rules()->getDir();
177             if (resist_dir != Dir::DIR_NO && resist_dir != Dir::DIR_UP) {
178                 if (strict) {
179                     if ((*i)->rules()->isOnHolderBacks()) {
180                         return true;
181                     }
182                 } else {
183                     if (!(*i)->rules()->isOnStack()) {
184                         return true;
185                     }
186                 }
187             }
188         }
189     }
190 
191     return false;
192 }
193 //-----------------------------------------------------------------
194 /**
195  * Whether object is under falling object.
196  *
197  * @return true when fish is dead
198  */
199     bool
checkDeadFall()200 Rules::checkDeadFall()
201 {
202     Cube::t_models killers = whoIsFalling();
203 
204     Cube::t_models::iterator end = killers.end();
205     for (Cube::t_models::iterator i = killers.begin(); i != end; ++i) {
206         if (!(*i)->rules()->isOnWall()) {
207             return true;
208         }
209     }
210 
211     return false;
212 }
213 //-----------------------------------------------------------------
214 /**
215  * Whether object is under hight stress.
216  *
217  * @return true when fish is dead
218  */
219     bool
checkDeadStress()220 Rules::checkDeadStress()
221 {
222     Cube::t_models killers = whoIsHeavier(m_model->getPower());
223 
224     Cube::t_models::iterator end = killers.end();
225     for (Cube::t_models::iterator i = killers.begin(); i != end; ++i) {
226         if (!(*i)->rules()->isOnStrongPad((*i)->getWeight())) {
227             return true;
228         }
229     }
230 
231     return false;
232 }
233 
234 //-----------------------------------------------------------------
235 /**
236  * Finish events from last round.
237  * Change model state.
238  */
239     void
changeState()240 Rules::changeState()
241 {
242     m_dir = Dir::DIR_NO;
243 
244     if (!m_model->isLost() && m_model->isDisintegrated()) {
245         m_mask->unmask();
246         m_model->change_remove();
247     }
248 
249     if (m_readyToTurn) {
250         m_readyToTurn = false;
251         m_model->change_turnSide();
252     }
253 
254     m_readyToActive = false;
255 
256     if (m_readyToDie) {
257         m_readyToDie = false;
258         m_model->change_die();
259     }
260 }
261 //-----------------------------------------------------------------
262 /**
263  * Let model to go out of room.
264  * @return out depth, 0 for normal, 1 for going out,
265  * 2... for on the way, -1 for out of screen
266  */
267 int
actionOut()268 Rules::actionOut()
269 {
270     if (!m_model->isLost() && !m_model->isBusy() && m_dir == Dir::DIR_NO)
271     {
272         //NOTE: normal objects are not allowed to go out of screen
273         if (m_model->shouldGoOut()) {
274             if (m_mask->isFullyOut()) {
275                 m_model->change_goOut();
276                 m_outDepth = -1;
277             } else {
278                 Dir::eDir borderDir = m_mask->getBorderDir();
279                 if (borderDir != Dir::DIR_NO) {
280                     m_model->change_goingOut();
281                     moveDirBrute(borderDir);
282                     m_outDepth += 1;
283                 } else {
284                     m_outDepth = 0;
285                 }
286             }
287         }
288     }
289 
290     return m_outDepth;
291 }
292 //-----------------------------------------------------------------
293 /**
294  * Let model fall.
295  */
296     void
actionFall()297 Rules::actionFall()
298 {
299     m_dir = Dir::DIR_DOWN;
300     m_lastFall = true;
301 }
302 //-----------------------------------------------------------------
303 /**
304  * Unset falling flag.
305  * @return last value of the flag
306  */
307     bool
clearLastFall()308 Rules::clearLastFall()
309 {
310     bool last = m_lastFall;
311     m_lastFall = false;
312     return last;
313 }
314 //-----------------------------------------------------------------
315 /**
316  * Unmask from old position.
317  */
318     void
freeOldPos()319 Rules::freeOldPos()
320 {
321     if (m_dir != Dir::DIR_NO) {
322         m_mask->unmask();
323     }
324 }
325 
326 //-----------------------------------------------------------------
327 /**
328  * Whether object is direct or undirect on something specific.
329  * @param cond condition which will be satify when object is on.
330  */
331     bool
isOnCond(const OnCondition & cond)332 Rules::isOnCond(const OnCondition &cond)
333 {
334     bool result = false;
335     if (cond.isSatisfy(m_model)) {
336         result = true;
337     }
338     else if (cond.isWrong(m_model)) {
339         result = false;
340     }
341     else {
342         m_mask->unmask();
343 
344         result = false;
345         Cube::t_models resist = m_mask->getResist(Dir::DIR_DOWN);
346         Cube::t_models::iterator end = resist.end();
347         for (Cube::t_models::iterator i = resist.begin(); i != end; ++i) {
348             if ((*i)->rules()->isOnCond(cond)) {
349                 //NOTE: don't forget to mask()
350                 result = true;
351                 break;
352             }
353         }
354 
355         m_mask->mask();
356     }
357 
358     return result;
359 }
360 //-----------------------------------------------------------------
361 /**
362  * Whether object is on another unalive object.
363  * And this another object must be on something fixed
364  * (except on original object).
365  *
366  * Such object can be moved over fish.
367  */
368     bool
isOnStack()369 Rules::isOnStack()
370 {
371     return isOnCond(OnStack());
372 }
373 //-----------------------------------------------------------------
374 /**
375  * Whether object is direct or undirect on a wall.
376  */
377     bool
isOnWall()378 Rules::isOnWall()
379 {
380     return isOnCond(OnWall());
381 }
382 //-----------------------------------------------------------------
383 /**
384  * Whether object is direct or undirect on Wall or on powerful fish.
385  *
386  * @param weight stress weight which must fish carry
387  * @return whether Wall or a strong fish carry this object
388  */
389     bool
isOnStrongPad(Cube::eWeight weight)390 Rules::isOnStrongPad(Cube::eWeight weight)
391 {
392     return isOnCond(OnStrongPad(weight));
393 }
394 //-----------------------------------------------------------------
395 /**
396  * Returns true if the object is laying just on alive holders
397  * and they all have at least a part of their backs
398  * directly under this object.
399  *
400  * Pushing the object would kill all the holders.
401  * The object would be free to fall.
402  */
403     bool
isOnHolderBacks()404 Rules::isOnHolderBacks()
405 {
406     unsigned int numDirectHolders = 0;
407     Cube::t_models resist = m_mask->getResist(Dir::DIR_DOWN);
408     Cube::t_models::iterator end = resist.end();
409     for (Cube::t_models::iterator i = resist.begin(); i != end; ++i) {
410         if ((*i)->isAlive()) {
411             ++numDirectHolders;
412         }
413     }
414 
415     Cube::t_models pads = getPads();
416     MarkMask::unique(&pads);
417     return numDirectHolders == pads.size();
418 }
419 //-----------------------------------------------------------------
420 /**
421  * Returns all alive fish and walls under this object.
422  */
423     Cube::t_models
getPads()424 Rules::getPads()
425 {
426     Cube::t_models pads;
427     m_mask->unmask();
428 
429     Cube::t_models resist = m_mask->getResist(Dir::DIR_DOWN);
430     Cube::t_models::iterator end = resist.end();
431     for (Cube::t_models::iterator i = resist.begin(); i != end; ++i) {
432         if ((*i)->isAlive() || (*i)->isWall()) {
433             pads.push_back(*i);
434         } else {
435             Cube::t_models distance_pads = (*i)->rules()->getPads();
436             pads.insert(pads.end(), distance_pads.begin(),
437                     distance_pads.end());
438         }
439     }
440 
441     m_mask->mask();
442     return pads;
443 }
444 //-----------------------------------------------------------------
445 /**
446  * Whether object is falling.
447  */
448 bool
isFalling() const449 Rules::isFalling() const
450 {
451     bool result = false;
452     if (!m_model->isAlive()) {
453         result = (m_dir == Dir::DIR_DOWN);
454     }
455     return result;
456 }
457 //-----------------------------------------------------------------
458 /**
459  * Who is falling on us.
460  * @return array of killers, they can fall undirect on us
461  */
462     Cube::t_models
whoIsFalling()463 Rules::whoIsFalling()
464 {
465     Cube::t_models result;
466     m_mask->unmask();
467 
468     Cube::t_models resist = m_mask->getResist(Dir::DIR_UP);
469     Cube::t_models::iterator end = resist.end();
470     for (Cube::t_models::iterator i = resist.begin(); i != end; ++i) {
471         //NOTE: falling is not propagated over fish
472         if (!(*i)->isWall() && !(*i)->isAlive()) {
473             if ((*i)->rules()->isFalling()) {
474                 result.push_back(*i);
475             }
476             else {
477                 Cube::t_models distance_killers = (*i)->rules()->whoIsFalling();
478                 result.insert(result.end(), distance_killers.begin(),
479                         distance_killers.end());
480             }
481         }
482     }
483 
484     m_mask->mask();
485     return result;
486 }
487 //-----------------------------------------------------------------
488 /**
489  * Whether object is heavier than our power.
490  * @param power our max power
491  * @return whether object is heavier
492  */
493 bool
isHeavier(Cube::eWeight power) const494 Rules::isHeavier(Cube::eWeight power) const
495 {
496     bool result = false;
497     if (!m_model->isWall() && !m_model->isAlive()) {
498         if (m_model->getWeight() > power) {
499             result = true;
500         }
501     }
502 
503     return result;
504 }
505 //-----------------------------------------------------------------
506 /**
507  * Who is heavier than our power.
508  * @param power our max power
509  * @return array of killers, they can lie undirect on us
510  */
511     Cube::t_models
whoIsHeavier(Cube::eWeight power)512 Rules::whoIsHeavier(Cube::eWeight power)
513 {
514     Cube::t_models result;
515     m_mask->unmask();
516 
517     Cube::t_models resist = m_mask->getResist(Dir::DIR_UP);
518     Cube::t_models::iterator end = resist.end();
519     for (Cube::t_models::iterator i = resist.begin(); i != end; ++i) {
520         if (!(*i)->isWall()) {
521             if ((*i)->rules()->isHeavier(power)) {
522                 result.push_back(*i);
523             }
524             else {
525                 Cube::t_models distance_killers =
526                     (*i)->rules()->whoIsHeavier(power);
527                 result.insert(result.end(), distance_killers.begin(),
528                         distance_killers.end());
529             }
530         }
531     }
532 
533     m_mask->mask();
534     return result;
535 }
536 
537 //-----------------------------------------------------------------
538 /**
539  * Whether other will retreat before us.
540  *
541  * @param power we will use this power
542  */
543     bool
canMoveOthers(Dir::eDir dir,Cube::eWeight power)544 Rules::canMoveOthers(Dir::eDir dir, Cube::eWeight power)
545 {
546     bool result = true;
547     //NOTE: make place after oneself, e.g. fish in U
548     m_mask->unmask();
549 
550     Cube::t_models resist = m_mask->getResist(dir);
551     Cube::t_models::iterator end = resist.end();
552     for (Cube::t_models::iterator i = resist.begin(); i != end; ++i) {
553         if (m_model->shouldGoOut() && (*i)->isBorder()) {
554             continue;
555         }
556         if (!(*i)->rules()->canDir(dir, power)) {
557             result = false;
558             break;
559         }
560     }
561 
562     m_mask->mask();
563     return result;
564 }
565 //-----------------------------------------------------------------
566 /**
567  * Whether others can move us.
568  * Live models cannot be moved by others.
569  * Power and weight is compared.
570  *
571  * @param dir move direction
572  * @param power others power
573  * @return whether we can move
574  */
575     bool
canDir(Dir::eDir dir,Cube::eWeight power)576 Rules::canDir(Dir::eDir dir, Cube::eWeight power)
577 {
578     bool result = false;
579     if (!m_model->isAlive() && power >= m_model->getWeight()) {
580         // A special case when outgoing object is pushing with FIXED power.
581         if (m_model->isWall() && !m_model->shouldGoOut()) {
582             return false;
583         }
584         result = canMoveOthers(dir, power);
585     }
586 
587     return result;
588 }
589 //-----------------------------------------------------------------
590 /**
591  * There is one special case.
592  * When model touches output_DIR then it goes out.
593  * This is used only in level 'windoze'.
594  */
595     bool
touchSpec(Dir::eDir dir)596 Rules::touchSpec(Dir::eDir dir)
597 {
598     bool result = false;
599     Cube::t_models resist = m_mask->getResist(dir);
600     if (resist.size() == 1) {
601         if (resist[0]->isOutDir(dir)) {
602             resist[0]->decOutCapacity();
603             m_mask->unmask();
604             m_model->change_goOut();
605             result = true;
606         }
607     }
608     return result;
609 }
610 //-----------------------------------------------------------------
611 /**
612  * Marks all resisted models as touched.
613  */
614 void
setTouched(Dir::eDir dir)615 Rules::setTouched(Dir::eDir dir)
616 {
617     m_touchDir = dir;
618     if (!m_model->isWall()) {
619         m_mask->unmask();
620         Cube::t_models resist = m_mask->getResist(dir);
621         Cube::t_models::iterator end = resist.end();
622         for (Cube::t_models::iterator i = resist.begin(); i != end; ++i) {
623             if (!(*i)->isAlive()) {
624                 (*i)->rules()->setTouched(dir);
625             }
626         }
627         m_mask->mask();
628     }
629 }
630 //-----------------------------------------------------------------
631 /**
632  * Try to move.
633  * Only m_dir will be set.
634  * NOTE: we can move all resist or none
635  *
636  * @return whether we have moved
637  */
638     bool
actionMoveDir(Dir::eDir dir)639 Rules::actionMoveDir(Dir::eDir dir)
640 {
641     bool result = false;
642     if (canMoveOthers(dir, m_model->getPower())) {
643         moveDirBrute(dir);
644         result = true;
645     }
646     else {
647         if (touchSpec(dir)) {
648             result = true;
649         }
650         else {
651             setTouched(dir);
652         }
653     }
654 
655     return result;
656 }
657 //-----------------------------------------------------------------
658 /**
659  * Irrespective move.
660  * Set m_dir to this dir and do the same for all resist.
661  * Only m_dir and m_pushing will be set.
662  */
663     void
moveDirBrute(Dir::eDir dir)664 Rules::moveDirBrute(Dir::eDir dir)
665 {
666     //NOTE: make place after oneself, e.g. object in U
667     m_mask->unmask();
668 
669     Cube::t_models resist = m_mask->getResist(dir);
670     Cube::t_models::iterator end = resist.end();
671     for (Cube::t_models::iterator i = resist.begin(); i != end; ++i) {
672         if (!(*i)->isBorder()) {
673             (*i)->rules()->moveDirBrute(dir);
674             m_pushing = true;
675         }
676     }
677 
678     m_dir = dir;
679     m_mask->mask();
680 }
681 
682 //-----------------------------------------------------------------
683 /**
684  * Return what we do the last round.
685  * Useful for script functions.
686  * NOTE: dead is not action
687  */
688 std::string
getAction() const689 Rules::getAction() const
690 {
691     if (m_readyToTurn) {
692         return "turn";
693     }
694     else if (m_readyToActive) {
695         return "activate";
696     }
697     else if (m_model->isBusy()) {
698         return "busy";
699     }
700 
701     switch (m_dir) {
702         case Dir::DIR_LEFT: return "move_left";
703         case Dir::DIR_RIGHT: return "move_right";
704         case Dir::DIR_UP: return "move_up";
705         case Dir::DIR_DOWN: return "move_down";
706         case Dir::DIR_NO: return "rest";
707         default: assert(!"unknown dir"); break;
708     }
709 
710     return "rest";
711 }
712 //-----------------------------------------------------------------
713 /**
714  * Return how we have feel the last round.
715  * Useful for script functions.
716  *
717  * States:
718  * "goout" ... go out of room
719  * "dead" ... is dead
720  * "talking" ... is talking
721  * "pushing" ... is pushing
722  * "normal" ... is alive and resting
723  */
724 std::string
getState() const725 Rules::getState() const
726 {
727     if (m_outDepth == 1) {
728         return "goout";
729     }
730     else if (!m_model->isAlive()) {
731         return "dead";
732     }
733     else if (m_model->isTalking()) {
734         return "talking";
735     }
736     else if (m_pushing) {
737         return "pushing";
738     }
739     else {
740         return "normal";
741     }
742 }
743 //-----------------------------------------------------------------
744 bool
isAtBorder() const745 Rules::isAtBorder() const
746 {
747     return m_mask->getBorderDir() != Dir::DIR_NO;
748 }
749 //-----------------------------------------------------------------
750 bool
isFreePlace(const V2 & loc) const751 Rules::isFreePlace(const V2 &loc) const
752 {
753     return m_mask->getPlacedResist(loc).empty();
754 }
755 //-----------------------------------------------------------------
756 const Cube::t_models
getResist(Dir::eDir dir) const757 Rules::getResist(Dir::eDir dir) const
758 {
759     return m_mask->getResist(dir);
760 }
761 
762