1 | /* |
2 | * Copyright 2006-2009 Parker Coates <coates@kde.org> |
3 | * |
4 | * This file is part of Killbots. |
5 | * |
6 | * Killbots is free software: you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation, either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * Killbots is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License |
17 | * along with Killbots. If not, see <http://www.gnu.org/licenses/>. |
18 | */ |
19 | |
20 | #include "engine.h" |
21 | |
22 | #include "coordinator.h" |
23 | #include "settings.h" |
24 | #include "sprite.h" |
25 | |
26 | #include <KDE/KDebug> |
27 | #include <KDE/KRandom> |
28 | |
29 | uint qHash( const QPoint & point ) |
30 | { |
31 | return qHash( point.x() * 1000 + point.y() ); |
32 | } |
33 | |
34 | |
35 | inline int sign( int num ) |
36 | { |
37 | return (num > 0) ? 1 : (num == 0) ? 0 : -1; |
38 | } |
39 | |
40 | |
41 | Killbots::Engine::Engine( Killbots::Coordinator* scene, QObject* parent ) |
42 | : QObject( parent ), |
43 | m_coordinator( scene ), |
44 | m_hero( 0 ), |
45 | m_rules( 0 ), |
46 | m_round( 0 ), |
47 | m_score( 0 ), |
48 | m_energy( 0 ), |
49 | m_maxEnergy( 0.0 ), |
50 | m_robotCount( 0.0 ), |
51 | m_fastbotCount( 0.0 ), |
52 | m_junkheapCount( 0.0 ), |
53 | m_heroIsDead( false ), |
54 | m_waitingOutRound( false ), |
55 | m_spriteMap() |
56 | { |
57 | } |
58 | |
59 | |
60 | Killbots::Engine::~Engine() |
61 | { |
62 | delete m_rules; |
63 | } |
64 | |
65 | void Killbots::Engine::setRuleset( const Ruleset * ruleset ) |
66 | { |
67 | if ( ruleset && ruleset != m_rules ) |
68 | { |
69 | delete m_rules; |
70 | m_rules = ruleset; |
71 | } |
72 | } |
73 | |
74 | |
75 | const Killbots::Ruleset * Killbots::Engine::ruleset() const |
76 | { |
77 | return m_rules; |
78 | } |
79 | |
80 | |
81 | bool Killbots::Engine::gameHasStarted() const |
82 | { |
83 | return m_hero && m_score > 0; |
84 | } |
85 | |
86 | |
87 | bool Killbots::Engine::isRoundComplete() const |
88 | { |
89 | return m_bots.isEmpty(); |
90 | } |
91 | |
92 | |
93 | bool Killbots::Engine::isHeroDead() const |
94 | { |
95 | return m_heroIsDead; |
96 | } |
97 | |
98 | |
99 | bool Killbots::Engine::isBoardFull() const |
100 | { |
101 | return m_robotCount + m_fastbotCount + m_junkheapCount |
102 | > m_rules->rows() * m_rules->columns() / 2; |
103 | } |
104 | |
105 | |
106 | bool Killbots::Engine::canSafeTeleport() const |
107 | { |
108 | return m_rules->safeTeleportEnabled() |
109 | && m_energy >= m_rules->costOfSafeTeleport(); |
110 | } |
111 | |
112 | |
113 | bool Killbots::Engine::canUseVaporizer() const |
114 | { |
115 | return m_rules->vaporizerEnabled() |
116 | && m_energy >= m_rules->costOfVaporizer(); |
117 | } |
118 | |
119 | |
120 | void Killbots::Engine::startNewGame() |
121 | { |
122 | Q_ASSERT( m_rules != 0 ); |
123 | |
124 | // Don't show the new game message on first start. |
125 | if ( m_round != 0 ) |
126 | emit showNewGameMessage(); |
127 | |
128 | m_heroIsDead = false; |
129 | |
130 | m_round = 1; |
131 | m_score = 0; |
132 | m_maxEnergy = m_rules->energyEnabled() ? m_rules->maxEnergyAtGameStart() : 0; |
133 | m_energy = m_rules->energyEnabled() ? m_rules->energyAtGameStart() : 0; |
134 | m_robotCount = m_rules->enemiesAtGameStart(); |
135 | m_fastbotCount = m_rules->fastEnemiesAtGameStart(); |
136 | m_junkheapCount = m_rules->junkheapsAtGameStart(); |
137 | |
138 | emit teleportAllowed( true ); |
139 | emit waitOutRoundAllowed( true ); |
140 | emit teleportSafelyAllowed( canSafeTeleport() ); |
141 | emit vaporizerAllowed( canUseVaporizer() ); |
142 | |
143 | // Code used to generate theme previews |
144 | //newRound( " r\nhjf", false ); |
145 | |
146 | startNewRound( false ); |
147 | } |
148 | |
149 | |
150 | void Killbots::Engine::startNewRound( bool incrementRound, const QString & layout ) |
151 | { |
152 | cleanUpRound(); |
153 | |
154 | m_waitingOutRound = false; |
155 | |
156 | m_coordinator->beginNewAnimationStage(); |
157 | |
158 | if ( incrementRound ) |
159 | { |
160 | ++m_round; |
161 | |
162 | if ( m_rules->energyEnabled() ) |
163 | { |
164 | m_maxEnergy += m_rules->maxEnergyAddedEachRound(); |
165 | updateEnergy( m_rules->energyAddedEachRound() ); |
166 | } |
167 | m_robotCount += m_rules->enemiesAddedEachRound(); |
168 | m_fastbotCount += m_rules->fastEnemiesAddedEachRound(); |
169 | m_junkheapCount += m_rules->junkheapsAddedEachRound(); |
170 | } |
171 | |
172 | if ( layout.isEmpty() ) |
173 | { |
174 | // Place the hero in the centre of the board. |
175 | const QPoint centre = QPoint( qRound( m_rules->columns() / 2 ), qRound( m_rules->rows() / 2 ) ); |
176 | m_hero = m_coordinator->createSprite( Hero, centre ); |
177 | |
178 | // Create and randomly place junkheaps. |
179 | for ( int i = m_junkheapCount; i > 0 ; --i ) |
180 | { |
181 | const QPoint point = randomEmptyCell(); |
182 | m_junkheaps << m_coordinator->createSprite( Junkheap, point ); |
183 | m_spriteMap.insert( point, m_junkheaps.last() ); |
184 | } |
185 | |
186 | // Create and randomly place robots. |
187 | for ( int i = m_robotCount; i > 0; --i ) |
188 | { |
189 | const QPoint point = randomEmptyCell(); |
190 | m_bots << m_coordinator->createSprite( Robot, point ); |
191 | m_spriteMap.insert( point, m_bots.last() ); |
192 | } |
193 | |
194 | // Create and randomly place fastbots. |
195 | for ( int i = m_fastbotCount; i > 0; --i ) |
196 | { |
197 | const QPoint point = randomEmptyCell(); |
198 | m_bots << m_coordinator->createSprite( Fastbot, point ); |
199 | m_spriteMap.insert( point, m_bots.last() ); |
200 | } |
201 | } |
202 | else |
203 | { |
204 | const QStringList rows = layout.split('\n'); |
205 | for ( int r = 0; r < rows.size(); ++r ) |
206 | { |
207 | for ( int c = 0; c < rows.at( r ).size(); ++c ) |
208 | { |
209 | const QChar ch = rows.at( r ).at( c ); |
210 | const QPoint point( c, r ); |
211 | |
212 | if ( ch == 'h' && m_hero == 0 ) |
213 | m_hero = m_coordinator->createSprite( Hero, point ); |
214 | else if ( ch == 'r' ) |
215 | m_bots << m_coordinator->createSprite( Robot, point ); |
216 | else if ( ch == 'f' ) |
217 | m_bots << m_coordinator->createSprite( Fastbot, point ); |
218 | else if ( ch == 'j' ) |
219 | m_junkheaps << m_coordinator->createSprite( Junkheap, point ); |
220 | } |
221 | } |
222 | } |
223 | |
224 | emit roundChanged( m_round ); |
225 | emit scoreChanged( m_score ); |
226 | emit enemyCountChanged( m_bots.size() ); |
227 | emit energyChanged( m_energy ); |
228 | |
229 | refreshSpriteMap(); |
230 | } |
231 | |
232 | |
233 | // Returns true if the move was performed, returns false otherwise. |
234 | bool Killbots::Engine::moveHero( Killbots::HeroAction direction ) |
235 | { |
236 | refreshSpriteMap(); |
237 | const QPoint newCell = m_hero->gridPos() + offsetFromDirection( direction ); |
238 | const bool preventUnsafeMoves = Settings::preventUnsafeMoves() || direction < 0; |
239 | |
240 | if ( moveIsValid( newCell, direction ) && ( moveIsSafe( newCell, direction ) || !preventUnsafeMoves ) ) |
241 | { |
242 | if ( direction != Hold ) |
243 | { |
244 | m_coordinator->beginNewAnimationStage(); |
245 | |
246 | if ( spriteTypeAt( newCell ) == Junkheap ) |
247 | pushJunkheap( m_spriteMap.value( newCell ), direction ); |
248 | |
249 | m_coordinator->slideSprite( m_hero, newCell ); |
250 | } |
251 | return true; |
252 | } |
253 | else |
254 | { |
255 | return false; |
256 | } |
257 | } |
258 | |
259 | |
260 | // Always returns true as teleports always succeed. |
261 | bool Killbots::Engine::teleportHero() |
262 | { |
263 | refreshSpriteMap(); |
264 | const QPoint point = randomEmptyCell(); |
265 | m_coordinator->beginNewAnimationStage(); |
266 | m_coordinator->teleportSprite( m_hero, point ); |
267 | return true; |
268 | } |
269 | |
270 | |
271 | // Returns true if a safe cell was found. If no safe cell was found than |
272 | // the board must be full. |
273 | bool Killbots::Engine::teleportHeroSafely() |
274 | { |
275 | refreshSpriteMap(); |
276 | |
277 | // Choose a random cell... |
278 | const QPoint startPoint = QPoint( KRandom::random() % m_rules->columns(), KRandom::random() % m_rules->rows() ); |
279 | QPoint point = startPoint; |
280 | |
281 | // ...and step through all the cells on the board looking for a safe cell. |
282 | do |
283 | { |
284 | if ( point.x() < m_rules->columns() - 1 ) |
285 | point.rx()++; |
286 | else |
287 | { |
288 | point.rx() = 0; |
289 | if ( point.y() < m_rules->rows() - 1 ) |
290 | point.ry()++; |
291 | else |
292 | point.ry() = 0; |
293 | } |
294 | |
295 | // Looking for an empty and safe cell. |
296 | if ( spriteTypeAt( point ) == NoSprite && point != m_hero->gridPos() && moveIsSafe( point, Teleport ) ) |
297 | break; |
298 | } |
299 | while ( point != startPoint ); |
300 | |
301 | // If we stepped through every cell and found none that were safe, reset the robot counts. |
302 | if ( point == startPoint ) |
303 | { |
304 | return false; |
305 | } |
306 | else |
307 | { |
308 | m_coordinator->beginNewAnimationStage(); |
309 | updateEnergy( -m_rules->costOfSafeTeleport() ); |
310 | m_coordinator->teleportSprite( m_hero, point ); |
311 | |
312 | return true; |
313 | } |
314 | } |
315 | |
316 | |
317 | // Returns true if any enemies were within range. |
318 | bool Killbots::Engine::useVaporizer() |
319 | { |
320 | refreshSpriteMap(); |
321 | QList<Sprite *> neighbors; |
322 | for ( int i = Right; i <= DownRight; ++i ) |
323 | { |
324 | const QPoint neighbor = m_hero->gridPos() + offsetFromDirection( i ); |
325 | if ( cellIsValid( neighbor ) && ( spriteTypeAt( neighbor ) == Robot || spriteTypeAt( neighbor ) == Fastbot ) ) |
326 | neighbors << m_spriteMap.value(neighbor); |
327 | } |
328 | |
329 | if ( !neighbors.isEmpty() ) |
330 | { |
331 | m_coordinator->beginNewAnimationStage(); |
332 | foreach ( Sprite * sprite, neighbors ) |
333 | destroySprite( sprite ); |
334 | updateEnergy( -m_rules->costOfVaporizer() ); |
335 | return true; |
336 | } |
337 | else |
338 | { |
339 | return false; |
340 | } |
341 | } |
342 | |
343 | |
344 | bool Killbots::Engine::waitOutRound() |
345 | { |
346 | m_waitingOutRound = true; |
347 | return true; |
348 | } |
349 | |
350 | |
351 | void Killbots::Engine::moveRobots( bool justFastbots ) |
352 | { |
353 | m_coordinator->beginNewAnimationStage(); |
354 | |
355 | if ( justFastbots ) |
356 | { |
357 | refreshSpriteMap(); |
358 | foreach( Sprite * bot, m_bots ) |
359 | { |
360 | if ( bot->spriteType() == Fastbot ) |
361 | { |
362 | const QPoint offset( sign( m_hero->gridPos().x() - bot->gridPos().x() ), sign( m_hero->gridPos().y() - bot->gridPos().y() ) ); |
363 | const QPoint target = bot->gridPos() + offset; |
364 | if ( spriteTypeAt( target ) != Robot || !m_rules->fastEnemiesArePatient() ) |
365 | m_coordinator->slideSprite( bot, target ); |
366 | } |
367 | } |
368 | } |
369 | else |
370 | { |
371 | foreach( Sprite * bot, m_bots ) |
372 | { |
373 | const QPoint offset( sign( m_hero->gridPos().x() - bot->gridPos().x() ), sign( m_hero->gridPos().y() - bot->gridPos().y() ) ); |
374 | m_coordinator->slideSprite( bot, bot->gridPos() + offset ); |
375 | } |
376 | } |
377 | } |
378 | |
379 | |
380 | void Killbots::Engine::assessDamage() |
381 | { |
382 | refreshSpriteMap(); |
383 | |
384 | m_coordinator->beginNewAnimationStage(); |
385 | |
386 | if ( m_spriteMap.count( m_hero->gridPos() ) > 0 ) |
387 | m_heroIsDead = true; |
388 | |
389 | // Check junkheaps for dead robots |
390 | foreach ( Sprite * junkheap, m_junkheaps ) |
391 | destroyAllCollidingBots( junkheap, !m_heroIsDead ); |
392 | |
393 | // Check for robot-on-robot violence |
394 | int i = 0; |
395 | while ( i < m_bots.size() ) |
396 | { |
397 | Sprite * bot = m_bots[i]; |
398 | if ( bot->gridPos() != m_hero->gridPos() && destroyAllCollidingBots( bot, !m_heroIsDead ) ) |
399 | { |
400 | m_junkheaps << m_coordinator->createSprite( Junkheap, bot->gridPos() ); |
401 | destroySprite( bot, !m_heroIsDead ); |
402 | } |
403 | else |
404 | { |
405 | i++; |
406 | } |
407 | } |
408 | |
409 | if ( isRoundComplete() ) |
410 | { |
411 | m_coordinator->beginNewAnimationStage(); |
412 | emit showRoundCompleteMessage(); |
413 | } |
414 | } |
415 | |
416 | |
417 | void Killbots::Engine::resetBotCounts() |
418 | { |
419 | m_coordinator->beginNewAnimationStage(); |
420 | emit showBoardFullMessage(); |
421 | |
422 | m_maxEnergy = m_rules->maxEnergyAtGameStart(); |
423 | m_robotCount = m_rules->enemiesAtGameStart(); |
424 | m_fastbotCount = m_rules->fastEnemiesAtGameStart(); |
425 | m_junkheapCount = m_rules->junkheapsAtGameStart(); |
426 | |
427 | m_coordinator->beginNewAnimationStage(); |
428 | startNewRound(false); |
429 | } |
430 | |
431 | |
432 | void Killbots::Engine::endGame() |
433 | { |
434 | emit showGameOverMessage(); |
435 | emit teleportAllowed( false ); |
436 | emit waitOutRoundAllowed( false ); |
437 | emit teleportSafelyAllowed( false ); |
438 | emit vaporizerAllowed( false ); |
439 | emit gameOver( m_score, m_round ); |
440 | } |
441 | |
442 | |
443 | // The hero action functions and the assessDamage functions must know the |
444 | // contents of each cell. This function updates the hash that maps cells to |
445 | // their contents. |
446 | void Killbots::Engine::refreshSpriteMap() |
447 | { |
448 | m_spriteMap.clear(); |
449 | foreach( Sprite * bot, m_bots ) |
450 | m_spriteMap.insert( bot->gridPos(), bot ); |
451 | foreach( Sprite * junkheap, m_junkheaps ) |
452 | m_spriteMap.insert( junkheap->gridPos(), junkheap ); |
453 | } |
454 | |
455 | |
456 | // A convenience function to query the type of a sprite any the given cell. |
457 | int Killbots::Engine::spriteTypeAt( const QPoint & cell ) const |
458 | { |
459 | if ( m_spriteMap.contains( cell ) ) |
460 | return m_spriteMap.value( cell )->spriteType(); |
461 | else |
462 | return NoSprite; |
463 | } |
464 | |
465 | |
466 | QPoint Killbots::Engine::offsetFromDirection( int direction ) const |
467 | { |
468 | if ( direction < 0 ) |
469 | direction = -direction - 1; |
470 | |
471 | switch( direction ) |
472 | { |
473 | case Right: |
474 | return QPoint( 1, 0 ); |
475 | case UpRight: |
476 | return QPoint( 1, -1 ); |
477 | case Up: |
478 | return QPoint( 0, -1 ); |
479 | case UpLeft: |
480 | return QPoint( -1, -1 ); |
481 | case Left: |
482 | return QPoint( -1, 0 ); |
483 | case DownLeft: |
484 | return QPoint( -1, 1 ); |
485 | case Down: |
486 | return QPoint( 0, 1 ); |
487 | case DownRight: |
488 | return QPoint( 1, 1 ); |
489 | default: |
490 | return QPoint( 0, 0 ); |
491 | }; |
492 | } |
493 | |
494 | |
495 | // Returns a random empty cell on the grid. Depends on a fresh spritemap. |
496 | QPoint Killbots::Engine::randomEmptyCell() const |
497 | { |
498 | QPoint point; |
499 | do |
500 | point = QPoint( KRandom::random() % m_rules->columns(), KRandom::random() % m_rules->rows() ); |
501 | while ( spriteTypeAt( point ) != NoSprite || point == m_hero->gridPos() ); |
502 | return point; |
503 | } |
504 | |
505 | |
506 | // Returns true if the given cell lies inside the game grid. |
507 | bool Killbots::Engine::cellIsValid( const QPoint & cell ) const |
508 | { |
509 | return ( 0 <= cell.x() |
510 | && cell.x() < m_rules->columns() |
511 | && 0 <= cell.y() |
512 | && cell.y() < m_rules->rows() |
513 | ); |
514 | } |
515 | |
516 | |
517 | bool Killbots::Engine::moveIsValid( const QPoint & cell, HeroAction direction ) const |
518 | { |
519 | // The short version |
520 | return ( cellIsValid( cell ) |
521 | && ( spriteTypeAt( cell ) == NoSprite |
522 | || ( spriteTypeAt( cell ) == Junkheap |
523 | && canPushJunkheap( m_spriteMap.value( cell ), direction ) |
524 | ) |
525 | ) |
526 | ); |
527 | |
528 | |
529 | /* // The debuggable version |
530 | bool result = true; |
531 | |
532 | if ( cellIsValid( cell ) ) |
533 | { |
534 | if ( spriteTypeAt( cell ) != NoSprite ) |
535 | { |
536 | if ( spriteTypeAt( cell ) == Junkheap ) |
537 | { |
538 | if ( !canPushJunkheap( m_spriteMap.value( cell ), direction ) ) |
539 | { |
540 | result = false; |
541 | kDebug() << "Move is invalid. Cannot push junkheap."; |
542 | } |
543 | } |
544 | else |
545 | { |
546 | result = false; |
547 | kDebug() << "Move is invalid. Cell is occupied by an unpushable object."; |
548 | } |
549 | } |
550 | } |
551 | else |
552 | { |
553 | result = false; |
554 | kDebug() << "Move is invalid. Cell is lies outside grid."; |
555 | } |
556 | |
557 | return result; |
558 | */ |
559 | } |
560 | |
561 | |
562 | bool Killbots::Engine::moveIsSafe( const QPoint & cell, HeroAction direction ) const |
563 | { |
564 | /* |
565 | Warning: This algorithm might break your head. The following diagrams and descriptions try to help. |
566 | |
567 | Note: This algorithm assumes that the proposed move has already been checked for validity. |
568 | |
569 | Legend |
570 | H = The position of the hero after the proposed move (the cell who's safeness we're trying to determine). |
571 | J = The position of a junkheap after the proposed move, whether moved by the hero or sitting there already. |
572 | R = The position of a robot. |
573 | F = The position of a fastbot. |
574 | * = A cell that we don't particularly care about in this diagram. |
575 | |
576 | +---+---+---+---+---+ |
577 | | * | * | * | * | * | |
578 | +---+---+---+---+---+ |
579 | | * | | | F | * | |
580 | +---+---+---+---+---+ |
581 | | * | | H | | * | If any of the neighbouring cells contain a robot or fastbot, the move is unsafe. |
582 | +---+---+---+---+---+ |
583 | | * | | R | | * | |
584 | +---+---+---+---+---+ |
585 | | * | * | * | * | * | |
586 | +---+---+---+---+---+ |
587 | |
588 | +---+---+---+---+---+ |
589 | | | | | | | |
590 | +---+---+---+---+---+ |
591 | | | | | | | |
592 | +---+---+---+---+---+ |
593 | | | *<==J<==H | | If the proposed move involved pushing a junkheap, we can ignore the cell that the junkheap |
594 | +---+---+---+---+---+ will end up in, because if there were an enemy there, it would be crushed. |
595 | | | | | | | |
596 | +---+---+---+---+---+ |
597 | | | | | | | |
598 | +---+---+---+---+---+ |
599 | |
600 | +---+---+---+---+---+ |
601 | |C01| | | | | |
602 | +---+---+---+---+---+ Fastbots can attack from two cells away, making it trickier to determine whether they |
603 | | |N01| | |E01| pose a threat. First we have to understand the attack vector of a fastbot. A fastbot |
604 | +---+---+---+---+---+ attacking from a "corner" cell such as C01 will pass through a diagonal neighbour like |
605 | | | | H |N02|E02| like N01. Any fastbot attacking from an "edge" cell like E01, E02 or E03 will have to |
606 | +---+---+---+---+---+ pass through a horizontal or vertical neighbour like N02. This mean that when checking |
607 | | | | | |E03| a diagonal neighbour we only need to check the one cell "behind" it for fastbots, but |
608 | +---+---+---+---+---+ when checking a horizontal or vertical neighbour we need to check the three cells |
609 | | | | | | | "behind" it for fastbots. |
610 | +---+---+---+---+---+ |
611 | |
612 | +---+---+---+---+---+ |
613 | | | | | | * | |
614 | +---+---+---+---+---+ |
615 | | * | | | J | | |
616 | +---+---+---+---+---+ Back to junkheaps. If a neighbouring cell contains a junkheap, we don't need to check |
617 | | * | J | H | | | the cells behind it for fastbots because if there were any there, they'd just collide |
618 | +---+---+---+---+---+ with the junkheap anyway. |
619 | | * | | | | | |
620 | +---+---+---+---+---+ |
621 | | | | | | | |
622 | +---+---+---+---+---+ |
623 | |
624 | +---+---+---+---+---+ |
625 | | * | * | * | * | F | |
626 | +---+---+---+---+---+ |
627 | | * | * | * | | * | |
628 | +---+---+---+---+---+ |
629 | | * | * | H | * | * | "Corner" fastbot threats are easy enough to detect. If a diagonal neighbour is empty |
630 | +---+---+---+---+---+ and the cell behind it contains a fastbot, the move is unsafe. |
631 | | * | * | * | * | * | |
632 | +---+---+---+---+---+ |
633 | | * | * | * | * | * | |
634 | +---+---+---+---+---+ |
635 | |
636 | +---+---+---+---+---+ |
637 | | * | * | * | * | * | |
638 | +---+---+---+---+---+ |
639 | | R | * | * | * | * | |
640 | +---+---+---+---+---+ "Edge" fastbots threats are much harder to detect because any fastbots on an edge might |
641 | | F | | H | * | * | collide with robots or other fastbots on their way to the neighbouring cell. For example, |
642 | +---+---+---+---+---+ the hero in this diagram is perfectly safe because all the fastbots will be destroyed |
643 | | | * | | * | * | before they can become dangerous. |
644 | +---+---+---+---+---+ |
645 | | * | F | | F | * | |
646 | +---+---+---+---+---+ |
647 | |
648 | +---+---+---+---+---+ |
649 | | * | F | | | * | |
650 | +---+---+---+---+---+ |
651 | | * | * | | * | | |
652 | +---+---+---+---+---+ With a bit of thought, it's easy to see that an "edge" fastbot is only a threat if there |
653 | | * | * | H | | | is exactly one fastbot and zero robots on that edge. |
654 | +---+---+---+---+---+ |
655 | | * | * | | * | F | When you put all of the above facts together you (hopefully) get the following algorithm. |
656 | +---+---+---+---+---+ |
657 | | * | | F | | * | |
658 | +---+---+---+---+---+ |
659 | */ |
660 | |
661 | // The move is assumed safe until proven unsafe. |
662 | bool result = true; |
663 | |
664 | // If we're pushing a junkheap, store the cell that the junkheap will end up in. Otherwise store an invalid cell. |
665 | const QPoint cellBehindJunkheap = ( spriteTypeAt( cell ) != Junkheap ) |
666 | ? QPoint( -1, -1 ) |
667 | : cell + offsetFromDirection( direction ); |
668 | |
669 | // We check the each of the target cells neighbours. |
670 | for ( int i = Right; i <= DownRight && result; ++i ) |
671 | { |
672 | const QPoint neighbor = cell + offsetFromDirection( i ); |
673 | |
674 | // If the neighbour is invalid or the cell behind the junkheap, continue to the next neighbour. |
675 | if ( !cellIsValid( neighbor ) || spriteTypeAt( neighbor ) == Junkheap || neighbor == cellBehindJunkheap ) |
676 | continue; |
677 | |
678 | // If the neighbour contains an enemy, the move is unsafe. |
679 | if ( spriteTypeAt( neighbor ) == Robot || spriteTypeAt( neighbor ) == Fastbot ) |
680 | { |
681 | result = false; |
682 | } |
683 | else |
684 | { |
685 | // neighboursNeighbour is the cell behind the neighbour, with respect to the target cell. |
686 | const QPoint neighborsNeighbor = neighbor + offsetFromDirection( i ); |
687 | |
688 | // If we're examining a diagonal neighbour (an odd direction)... |
689 | if ( i % 2 == 1 ) |
690 | { |
691 | // ...and neighboursNeighbour is a fastbot then the move is unsafe. |
692 | if ( spriteTypeAt( neighborsNeighbor ) == Fastbot ) |
693 | result = false; |
694 | } |
695 | // If we're examining an vertical or horizontal neighbour, things are more complicated... |
696 | else |
697 | { |
698 | // Assemble a list of the cells behind the neighbour. |
699 | QList<QPoint> cellsBehindNeighbor; |
700 | cellsBehindNeighbor << neighborsNeighbor; |
701 | // Add neighboursNeighbour's anticlockwise neighbour. |
702 | // ( i + 2 ) % 8 is the direction a quarter turn anticlockwise from i. |
703 | cellsBehindNeighbor << neighborsNeighbor + offsetFromDirection( ( i + 2 ) % 8 ); |
704 | // Add neighboursNeighbour's clockwise neighbour. |
705 | // ( i + 6 ) % 8 is the direction a quarter turn clockwise from i. |
706 | cellsBehindNeighbor << neighborsNeighbor + offsetFromDirection( ( i + 6 ) % 8 ); |
707 | |
708 | // Then we just count the number of fastbots and robots in the list of cells. |
709 | int fastbotsFound = 0; |
710 | int robotsFound = 0; |
711 | foreach( const QPoint & cell, cellsBehindNeighbor ) |
712 | { |
713 | if ( spriteTypeAt( cell ) == Fastbot ) |
714 | ++fastbotsFound; |
715 | else if ( spriteTypeAt( cell ) == Robot ) |
716 | ++robotsFound; |
717 | } |
718 | |
719 | // If there is exactly one fastbots and zero robots, the move is unsafe. |
720 | if ( fastbotsFound == 1 && robotsFound == 0 ) |
721 | result = false; |
722 | } |
723 | } |
724 | } |
725 | |
726 | return result; |
727 | } |
728 | |
729 | |
730 | bool Killbots::Engine::canPushJunkheap( const Sprite * junkheap, HeroAction direction ) const |
731 | { |
732 | Q_ASSERT( junkheap->spriteType() == Junkheap ); |
733 | |
734 | const QPoint nextCell = junkheap->gridPos() + offsetFromDirection( direction ); |
735 | |
736 | if ( m_rules->pushableJunkheaps() != Ruleset::None && cellIsValid( nextCell ) ) |
737 | { |
738 | if ( spriteTypeAt( nextCell ) == NoSprite ) |
739 | return true; |
740 | else if ( spriteTypeAt( nextCell ) == Junkheap ) |
741 | return m_rules->pushableJunkheaps() == Ruleset::Many && canPushJunkheap( m_spriteMap.value( nextCell ), direction ); |
742 | else |
743 | return m_rules->squaskKillsEnabled(); |
744 | } |
745 | else |
746 | { |
747 | return false; |
748 | } |
749 | } |
750 | |
751 | |
752 | void Killbots::Engine::pushJunkheap( Sprite * junkheap, HeroAction direction ) |
753 | { |
754 | const QPoint nextCell = junkheap->gridPos() + offsetFromDirection( direction ); |
755 | Sprite * currentOccupant = m_spriteMap.value( nextCell ); |
756 | if ( currentOccupant ) |
757 | { |
758 | if ( currentOccupant->spriteType() == Junkheap ) |
759 | { |
760 | pushJunkheap( currentOccupant, direction ); |
761 | } |
762 | else |
763 | { |
764 | destroySprite( currentOccupant ); |
765 | updateScore( m_rules->squashKillPointBonus() ); |
766 | updateEnergy( m_rules->squashKillEnergyBonus() ); |
767 | } |
768 | } |
769 | |
770 | m_coordinator->slideSprite( junkheap, nextCell ); |
771 | } |
772 | |
773 | |
774 | void Killbots::Engine::cleanUpRound() |
775 | { |
776 | m_coordinator->beginNewAnimationStage(); |
777 | |
778 | if ( m_hero ) |
779 | destroySprite( m_hero ); |
780 | m_hero = 0; |
781 | |
782 | foreach( Sprite * bot, m_bots ) |
783 | destroySprite( bot, false ); |
784 | m_bots.clear(); |
785 | |
786 | foreach( Sprite * junkheap, m_junkheaps ) |
787 | destroySprite( junkheap ); |
788 | m_junkheaps.clear(); |
789 | |
790 | m_spriteMap.clear(); |
791 | } |
792 | |
793 | |
794 | void Killbots::Engine::destroySprite( Sprite * sprite, bool calculatePoints ) |
795 | { |
796 | const SpriteType type = sprite->spriteType(); |
797 | |
798 | if ( type == Robot || type == Fastbot) |
799 | { |
800 | if ( calculatePoints ) |
801 | { |
802 | if ( type == Robot ) |
803 | updateScore( m_rules->pointsPerEnemyKilled() ); |
804 | else |
805 | updateScore( m_rules->pointsPerFastEnemyKilled() ); |
806 | |
807 | if ( m_waitingOutRound ) |
808 | { |
809 | updateScore( m_rules->waitKillPointBonus() ); |
810 | updateEnergy( m_rules->waitKillEnergyBonus() ); |
811 | } |
812 | } |
813 | m_bots.removeOne( sprite ); |
814 | emit enemyCountChanged( m_bots.size() ); |
815 | } |
816 | else if ( type == Junkheap ) |
817 | { |
818 | m_junkheaps.removeOne( sprite ); |
819 | } |
820 | |
821 | m_coordinator->destroySprite( sprite ); |
822 | } |
823 | |
824 | |
825 | bool Killbots::Engine::destroyAllCollidingBots( const Sprite * sprite, bool calculatePoints ) |
826 | { |
827 | bool result = false; |
828 | |
829 | foreach ( Sprite * robot, m_spriteMap.values( sprite->gridPos() ) ) |
830 | { |
831 | if ( robot != sprite && ( robot->spriteType() == Robot || robot->spriteType() == Fastbot ) ) |
832 | { |
833 | destroySprite( robot, calculatePoints ); |
834 | result = true; |
835 | } |
836 | } |
837 | |
838 | return result; |
839 | } |
840 | |
841 | |
842 | void Killbots::Engine::updateScore( int changeInScore ) |
843 | { |
844 | if ( changeInScore != 0 ) |
845 | { |
846 | m_score = m_score + changeInScore; |
847 | emit scoreChanged( m_score ); |
848 | } |
849 | } |
850 | |
851 | |
852 | void Killbots::Engine::updateEnergy( int changeInEnergy ) |
853 | { |
854 | if ( m_rules->energyEnabled() && changeInEnergy != 0 ) |
855 | { |
856 | if ( changeInEnergy > 0 && m_energy > int( m_maxEnergy ) ) |
857 | { |
858 | m_score += changeInEnergy * m_rules->pointsPerEnergyAboveMax(); |
859 | } |
860 | else if ( changeInEnergy > 0 && m_energy + changeInEnergy > int( m_maxEnergy ) ) |
861 | { |
862 | m_score += ( m_energy + changeInEnergy - int( m_maxEnergy ) ) * m_rules->pointsPerEnergyAboveMax(); |
863 | m_energy = int( m_maxEnergy ); |
864 | } |
865 | else |
866 | { |
867 | m_energy = m_energy + changeInEnergy; |
868 | } |
869 | |
870 | emit energyChanged( m_energy ); |
871 | emit teleportSafelyAllowed( canSafeTeleport() ); |
872 | emit vaporizerAllowed( canUseVaporizer() ); |
873 | } |
874 | } |
875 | |
876 | |
877 | QString Killbots::Engine::gridToString() const |
878 | { |
879 | QString string; |
880 | for ( int r = 0; r < m_rules->rows(); ++r ) |
881 | { |
882 | for ( int c = 0; c < m_rules->columns(); ++c ) |
883 | { |
884 | switch ( spriteTypeAt( QPoint( c, r ) ) ) |
885 | { |
886 | case Robot: |
887 | string += 'r'; |
888 | break; |
889 | case Fastbot: |
890 | string += 'f'; |
891 | break; |
892 | case Junkheap: |
893 | string += 'j'; |
894 | break; |
895 | default: |
896 | string += ' '; |
897 | break; |
898 | } |
899 | } |
900 | string += '\n'; |
901 | } |
902 | return string; |
903 | } |
904 | |