Passed
Pull Request — master (#291)
by Marek
03:37
created

ProblemsDlg._getTargetSlotDict()   B

Complexity

Conditions 8

Size

Total Lines 25
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 20
nop 2
dl 0
loc 25
rs 7.3333
c 0
b 0
f 0
1
#
2
#  Copyright 2001 - 2016 Ludek Smid [http://www.ospace.net/]
3
#
4
#  This file is part of Outer Space.
5
#
6
#  Outer Space 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
#  Outer Space 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 Outer Space; if not, write to the Free Software
18
#  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19
#
20
import copy
21
import math
22
import re
23
import string
24
25
import pygameui as ui
26
from osci import gdata, client, res
27
import ige.ospace.Const as Const
28
from ige.ospace import Utils, Rules
29
30
class ProblemsDlg:
31
    """Displays 'Problem locator' dialog.
32
33
    """
34
    def __init__(self, app):
35
        self.app = app
36
        self.createUI()
37
38
    def display(self):
39
        self.show()
40
        self.win.show()
41
        # register for updates
42
        if self not in gdata.updateDlgs:
43
            gdata.updateDlgs.append(self)
44
45
    def hide(self):
46
        self.win.setStatus(_("Ready."))
47
        self.win.hide()
48
        # unregister updates
49
        if self in gdata.updateDlgs:
50
            gdata.updateDlgs.remove(self)
51
52
    def update(self):
53
        self.show()
54
55
    class Problems:
56
        def __init__(self, win):
57
            self.items = []
58
            self.checkboxes = {gdata.CRI: win.vCritical.checked,
59
                               gdata.MAJ: win.vMajor.checked,
60
                               gdata.MIN: win.vMinor.checked,
61
                               gdata.INFO: win.vInfo.checked}
62
63
        def append(self, severity, item):
64
            if self.checkboxes[severity]:
65
                item.foreground = gdata.sevColors[severity]
66
                self.items.append(item)
67
68
    def _addProblemsStructStatus(self, problems, struct, planet):
69
        player = client.getPlayer()
70
        status = struct[Const.STRUCT_IDX_STATUS]
71
        tech = client.getFullTechInfo(struct[Const.STRUCT_IDX_TECHID])
72
73
        if hasattr(player, 'techs'):
74
            techEff = Rules.techImprEff[player.techs.get(struct[Const.STRUCT_IDX_TECHID], Rules.techBaseImprovement)]
75
        else:
76
            techEff = Rules.techImprEff[Rules.techBaseImprovement]
77
78
        HPturn = max(1, int(0.02 * tech.maxHP * techEff))
79
        turnsToDestroy = math.ceil(struct[Const.STRUCT_IDX_HP] / HPturn)
80
81
        if turnsToDestroy < Rules.turnsPerDay * 2:
82
            severity = gdata.MAJ
83
            if turnsToDestroy < Rules.turnsPerDay:
84
                severity = gdata.CRI
85
        else:
86
            severity = gdata.MIN
87
88
        if not status & Const.STRUCT_STATUS_ON:
89
            # structure is off
90
            problems.append(severity,
91
                            ui.Item(planet.name, tOID=planet.oid, tType=Const.T_PLANET,
92
                                    vDescription=_('Structure (%s) is off and will be destroyed in %s turns.') % (tech.name, res.formatTime(turnsToDestroy))))
93
94
        if status & Const.STRUCT_STATUS_DETER:
95
            problems.append(gdata.MAJ,
96
                            ui.Item(planet.name, tOID=planet.oid, tType=Const.T_PLANET,
97
                                    vDescription=_('Structure (%s) is deteriorating.') % (tech.name,)))
98
        if status & Const.STRUCT_STATUS_NOBIO:
99
            problems.append(gdata.INFO,
100
                            ui.Item(planet.name, tOID=planet.oid, tType=Const.T_PLANET,
101
                                    vDescription=_('Structure (%s) has insufficient supply of biomatter.') % (tech.name,)))
102
        if status & Const.STRUCT_STATUS_NOEN:
103
            problems.append(gdata.INFO,
104
                            ui.Item(planet.name, tOID=planet.oid, tType=Const.T_PLANET,
105
                                    vDescription=_('Structure (%s) has insufficient supply of energy.') % (tech.name,)))
106
        if status & Const.STRUCT_STATUS_NOPOP:
107
            problems.append(gdata.INFO,
108
                            ui.Item(planet.name, tOID=planet.oid, tType=Const.T_PLANET,
109
                                    vDescription=_('Structure (%s) has insufficient supply of workers.') % (tech.name,)))
110
        if status & Const.STRUCT_STATUS_REPAIRING:
111
            problems.append(gdata.INFO,
112
                            ui.Item(planet.name, tOID=planet.oid, tType=Const.T_PLANET,
113
                                    vDescription=_('Structure (%s) is repairing.') % (tech.name,)))
114
115
    def _getSystemRefuel(self, system):
116
        player = client.getPlayer()
117
        maxRefuelMax = 0
118
        hasRefuel = False
119
        for planetID in system.planets:
120
            planet = client.get(planetID, noUpdate=1)
121
            if hasattr(planet, 'owner'):
122
                if hasattr(planet, 'hasRefuel'):
123
                    hasRefuel = hasRefuel or planet.hasRefuel
124
                if hasattr(planet, 'refuelMax'):
125
                    if planet.owner == player.oid:
126
                        maxRefuelMax = max(maxRefuelMax, planet.refuelMax)
127
                    elif planet.owner in player.diplomacyRels:
128
                        dipl = client.getDiplomacyWith(planet.owner)
129
                        if Const.PACT_ALLOW_TANKING in dipl.pacts and dipl.pacts[Const.PACT_ALLOW_TANKING][0] == Const.PACT_ACTIVE:
130
                            maxRefuelMax = max(maxRefuelMax, planet.refuelMax)
131
                elif hasattr(planet, 'hasRefuel'):
132
                    maxRefuelMax
133
        return hasRefuel, maxRefuelMax
134
135
    def _addProblemsFleets(self, problems, fleet):
136
        energyReserve = fleet.storEn * 100 / fleet.maxEn
137
        system = None
138
139
        if hasattr(fleet, 'orbiting') and fleet.orbiting:
140
            system = client.get(fleet.orbiting, noUpdate=1)
141
            hasRefuel, maxRefuelMax = self._getSystemRefuel(system)
142
        else:
143
            # we do not report fleets in flight
144
            return
145
146
        systemName = res.getUnknownName()
147
        if system and hasattr(system, "name"):
148
            systemName = system.name
149
150
        # is fleet named?
151
        if hasattr(fleet, 'customname') and fleet.customname:
152
            name = fleet.customname
153
        else:
154
            name = getattr(fleet, "name", None)
155
156
        note = _(' and is NOT refueling')
157
        if 0 < maxRefuelMax <= energyReserve:
158
            severity = gdata.INFO
159
            if maxRefuelMax <= energyReserve:
160
                note = _(' and CAN refuel, but reached planet maximum refuel amount')
161
            else:
162
                note = _(' and IS refueling')
163
        elif hasRefuel:
164
            severity = gdata.MIN
165
            note = _(' and utilizes unknown refueling capacities')
166
        elif energyReserve < 25:
167
            severity = gdata.CRI
168
        elif energyReserve < 50:
169
            severity = gdata.MAJ
170
        elif energyReserve == 100:
171
            problems.append(gdata.INFO,
172
                            ui.Item(systemName, tOID=fleet.oid, tType=Const.T_FLEET,
173
                                    vDescription=_('Fleet "%s" has full fuel tanks.') % (name)))
174
            return
175
        else:
176
            severity = gdata.INFO
177
        problems.append(severity,
178
                        ui.Item(systemName, tOID=fleet.oid, tType=Const.T_FLEET,
179
                                vDescription=_('Fleet "%s" is low on fuel [%d %%]%s.') % (name, energyReserve, note)))
180
181
    def _addProblemsMaterial(self, problems, system, mat, totalMat, material):
182
        if mat >= 0:
183
            return
184
        surplusTurns = totalMat / (-mat)
185
        if surplusTurns < Rules.turnsPerDay * 7:
186
            severity = gdata.MAJ
187
        elif surplusTurns < Rules.turnsPerDay * 2:
188
            severity = gdata.CRI
189
        else:
190
            severity = gdata.MIN
191
192
        if totalMat > 0:
193
            note = _(' surplus %d (%s)' % (totalMat, res.formatTime(surplusTurns)))
194
        else:
195
            note = _(' with no surplus left!')
196
        problems.append(severity,
197
                        ui.Item(system.name, tOID=system.oid, tType=Const.T_SYSTEM,
198
                                vDescription=_('%s decreasing - last turn change %d, %s.') % (material, mat, note)))
199
200
    def _getTaskSciValue(self, task):
201
        player = client.getPlayer()
202
        fulltech = client.getFullTechInfo(task.techID)
203
        researchSci = Utils.getTechRCost(player, task.techID, task.improvement)
204
        maxImprovement = min(Rules.techMaxImprovement, fulltech.maxImprovement)
205
206
        if task.improveToMax:
207
            # account next levels
208
            for impr in range(task.improvement + 1, maxImprovement + 1):
209
                researchSci += Utils.getTechRCost(player, task.techID, impr)
210
        return researchSci - task.currSci
211
212
    def _addProblemsResearch(self, problems):
213
        player = client.getPlayer()
214
        sciProd = max(sum(task.changeSci for task in player.rsrchQueue),
215
                      player.effSciPoints)
216
        if sciProd < 0:
217
            problems.append(gdata.CRI,
218
                            ui.Item(_('Research'), tType=Const.T_TECHNOLOGY,
219
                                    vDescription=_('We are losing our researched knowledge by %d pts per turn!') % (sciProd,)))
220
            return
221
        elif sciProd == 0:
222
            return
223
        elif len(player.rsrchQueue) == 0:
224
            problems.append(gdata.CRI,
225
                            ui.Item(_('Research'), tType=Const.T_TECHNOLOGY,
226
                                    vDescription=_('Research queue is empty.')))
227
228
        queueValue = sum(self._getTaskSciValue(task) for task in player.rsrchQueue)
229
        totalEtc = math.ceil(float(queueValue) / sciProd)
230
231
        # check short reseach queue
232
        if totalEtc < Rules.turnsPerDay * 2:
233
            severity = gdata.MIN
234
            if totalEtc < Rules.turnsPerDay:
235
                severity = gdata.MAJ
236
            problems.append(severity,
237
                            ui.Item(_('Research'), tType=Const.T_TECHNOLOGY,
238
                                    vDescription=_('Research queue ends in %s turns, %d item(s) on list.') % (res.formatTime(totalEtc), len(player.rsrchQueue))))
239
240
    def _addProblemsGlobalQueues(self, problems):
241
        # go through all planets to understand the state of global queues
242
        player = client.getPlayer()
243
        # holder for (number , eff production) of planets set to each queue
244
        globalQueueStats = [(0, 0), (0, 0), (0, 0), (0, 0), (0, 0)]
245
        queConstValues = [0, 0, 0, 0, 0]
246
        queEtc = [0, 0, 0, 0, 0]
247
248
        for planetID in player.planets:
249
            planet = client.get(planetID, noUpdate=1)
250
            globalQueueStats[planet.globalQueue] = tuple([sum(x) for x in zip(globalQueueStats[planet.globalQueue], (1, planet.effProdProd))])
251
252
        # evaluate depletion rate of the global queue
253
        for queue in range(5):
254
            quePlanets, queEffProd = globalQueueStats[queue]
255
            for task in player.prodQueues[queue]:
256
                if task.isShip:
257
                    tech = player.shipDesigns[task.techID]
258
                else:
259
                    tech = client.getFullTechInfo(task.techID)
260
                queConstValues[queue] += task.quantity * tech.buildProd
261
            if queEffProd > 0:
262
                queEtc[queue] = math.ceil(float(queConstValues[queue])/queEffProd)
263
            else:
264
                queEtc[queue] = 99999
265
266
        # creation of items with global queue problems
267
        for queue in range(1, 5):
268
            queName = res.globalQueueName(queue)
269
            quePlanets = globalQueueStats[queue][0]
270
            # check empty global production queue with at least one planet [so its relevant]
271
            if queConstValues[queue] == 0 and quePlanets > 0:
272
                problems.append(gdata.CRI,
273
                                ui.Item(_('Global queue ' + queName), tType=Const.T_QUEUE,
274
                                        vDescription=_('Global production queue {0} used by {1} planet(s) is empty.'.format(queName, quePlanets))))
275
            # check end of global production queue
276
            elif queEtc[queue] < Rules.turnsPerDay * 2:
277
                severity = gdata.MIN
278
                if queEtc[queue] < Rules.turnsPerDay:
279
                    severity = gdata.MAJ
280
                problems.append(severity,
281
                                ui.Item(_('Global queue ' + queName), tType=Const.T_QUEUE,
282
                                        vDescription=_('Global production queue {0} used by {1} planet(s) runs out in {2} turns.'.format(queName, quePlanets, res.formatTime(queEtc[queue])))))
283
        return queEtc[0]  # time of depletion of the default queue will be reused later
284
285
    def _getTargetSlotDict(self, planetID):
286
        assert planetID in client.getPlayer().planets
287
        targets = {}
288
        planet = client.get(planetID, noUpdate=1)
289
        if planet.effProdProd <= 0:
290
            return targets
291
292
        nonShipTasks = (task for task in planet.prodQueue if not task.isShip)
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable task does not seem to be defined.
Loading history...
293
        for task in nonShipTasks:
294
            tech = client.getFullTechInfo(task.techID)
295
            if tech.isStructure and task.demolishStruct == 0:
296
                quantity = task.quantity
297
            elif tech.isProject and tech.id == 3802:
298
                # we are constructing Habitable Surface Expansion
299
                # so after construction we will have new slot on the planet
300
                quantity = -1
301
            else:
302
                continue
303
            assert quantity != 0
304
305
            try:
306
                targets[task.targetID] += task.quantity
307
            except KeyError:
308
                targets[task.targetID] = task.quantity
309
        return targets
310
311
312
    def _addProblemsSlots(self, problems, system):
313
        player = client.getPlayer()
314
        playerPlanets = set(system.planets) & set(player.planets)
315
        freeSlots = {}
316
        structSources = dict.fromkeys(system.planets)
317
        for planetID in system.planets:
318
            planet = client.get(planetID, noUpdate=1)
319
            freeSlots[planetID] = planet.plSlots - len(planet.slots)
320
321
        for planetID in playerPlanets:
322
            for targetID, quantity in self._getTargetSlotDict(planetID).items():
323
                freeSlots[targetID] -= quantity
324
                if quantity > 0:
325
                    try:
326
                        structSources[targetID].add(planetID)
327
                    except AttributeError:
328
                        structSources[targetID] = set([planetID])
329
330
        for planetID, free in freeSlots.items():
331
            if free < 0:
332
                # not enough space, report for every planet that builds on this one
333
                planet = client.get(planetID, noUpdate=1)
334
                for sourceID in structSources[planetID]:
335
                    source = client.get(sourceID, noUpdate=1)
336
                    problems.append(gdata.MAJ,
337
                                    ui.Item(source.name, tOID=sourceID, tType=Const.T_PLANET,
338
                                            vDescription=_('There is no space for all constructed buildings on %s.') % (planet.name)))
339
340
    def _addProblemsDefaultQueue(self, problems, planet, defaultQueueEtc):
341
        player = client.getPlayer()
342
        if not planet.effProdProd > 0:
343
            return
344
        planetEtc = 0
345
        # compute length of production queue
346
        for task in planet.prodQueue:
347
            if task.isShip:
348
                tech = player.shipDesigns[task.techID]
349
            else:
350
                tech = client.getFullTechInfo(task.techID)
351
            modifier = Rules.buildOnAnotherPlanetMod if task.targetID != planet.oid else 1
352
            planetEtc += math.ceil(float(task.quantity * tech.buildProd * modifier - task.currProd) / planet.effProdProd)
353
354
        etc = planetEtc + defaultQueueEtc
355
        # check empty production queue
356
        if not etc:
357
            problems.append(gdata.CRI,
358
                            ui.Item(planet.name, tOID=planet.oid, tType=Const.T_PLANET,
359
                                    vDescription=_('Production queue is empty.')))
360
            return
361
        if etc < Rules.turnsPerDay:
362
            severity = gdata.MAJ
363
        elif etc < Rules.turnsPerDay * 2:
364
            severity = gdata.MIN
365
        else:
366
            severity = gdata.INFO
367
368
        problems.append(severity,
369
                        ui.Item(planet.name, tOID=planet.oid, tType=Const.T_PLANET,
370
                                vDescription=_('Production queue may end in {0} turns ({1} directly in planet queue).'.format(res.formatTime(etc), res.formatTime(planetEtc)))))
371
372
    def show(self):
373
        critical = self.win.vCritical.checked
374
        major = self.win.vMajor.checked
375
        minor = self.win.vMinor.checked
376
        info = self.win.vInfo.checked
377
378
        disp = 1
379
380
        player = client.getPlayer()
381
        problems = self.Problems(self.win)
382
        systems = set([])
383
        for planetID in player.planets:
384
            planet = client.get(planetID, noUpdate=1)
385
            systems.add(planet.compOf)
386
387
        defaultQueueEtc = self._addProblemsGlobalQueues(problems)
388
389
        for systemID in systems:
390
            system = client.get(systemID, noUpdate=1)
391
            bio = 0
392
            totalBio = 0
393
            en = 0
394
            totalEn = 0
395
            # holds modified planets
396
            planetCopies = {}
397
398
            for planetID in system.planets:
399
                planet = client.get(planetID, noUpdate=1)
400
                # copy of planet to change plSlots count
401
                if hasattr(planet, 'owner') and planet.owner == player.oid:
402
                    # add planet to the global queue stats
403
                    # compute bio and en for system
404
                    bio += planet.changeBio
405
                    totalBio += max(0, planet.storBio - planet.minBio)
406
                    en += planet.changeEn
407
                    totalEn += max(0, planet.storEn - planet.minEn)
408
                    # the planet needs to have global queue 0 - the default one - to have its queue reported
409
                    if self.win.vPlanets.checked:
410
                        if not planet.globalQueue:
411
                            self._addProblemsDefaultQueue(problems, planet, defaultQueueEtc)
412
                        for struct in planet.slots:
413
                            self._addProblemsStructStatus(problems, struct, planet)
414
415
            # free slots within the system
416
            self._addProblemsSlots(problems, system)
417
            # check bio for system
418
            if self.win.vSystems.checked:
419
                self._addProblemsMaterial(problems, system, bio, totalBio, 'Bio')
420
                self._addProblemsMaterial(problems, system, en, totalEn, 'En')
421
422
        # check fleets
423
        if self.win.vFleets.checked:
424
            for fleetID in player.fleets:
425
                fleet = client.get(fleetID, noUpdate=1)
426
                self._addProblemsFleets(problems, fleet)
427
428
        # check research queue
429
        if self.win.vResearch.checked:
430
            self._addProblemsResearch(problems)
431
432
        self.win.vProblems.items = problems.items
433
        self.win.vProblems.itemsChanged()
434
435
    def onClose(self, widget, action, data):
436
        self.hide()
437
438
    def onShowSource(self, widget, action, data):
439
        item = self.win.vProblems.selection[0]
440
        if item.tType == Const.T_FLEET:
441
            object = client.get(item.tOID, noUpdate=1)
442
            # center on map
443
            if hasattr(object, "x"):
444
                gdata.mainGameDlg.win.vStarMap.highlightPos = (object.x, object.y)
445
                gdata.mainGameDlg.win.vStarMap.setPos(object.x, object.y)
446
                self.hide()
447
                return
448
        elif item.tType in (Const.T_SYSTEM, Const.T_PLANET):
449
            if item.tOID != Const.OID_NONE:
450
                gdata.mainGameDlg.onSelectMapObj(None, None, item.tOID)
451
                return
452
        elif item.tType == Const.T_TECHNOLOGY:
453
            gdata.mainGameDlg.researchDlg.display()
454
            return
455
        elif item.tType == Const.T_QUEUE:
456
            gdata.mainGameDlg.globalQueuesDlg.display()
457
        self.win.setStatus(_("Cannot show location."))
458
459
    def onShowLocation(self, widget, action, data):
460
        item = self.win.vProblems.selection[0]
461
        if item.tType in (Const.T_SYSTEM, Const.T_PLANET, Const.T_FLEET):
462
            object = client.get(item.tOID, noUpdate=1)
463
            # center on map
464
            if hasattr(object, "x"):
465
                gdata.mainGameDlg.win.vStarMap.highlightPos = (object.x, object.y)
466
                gdata.mainGameDlg.win.vStarMap.setPos(object.x, object.y)
467
                self.hide()
468
                return
469
        self.win.setStatus(_("Cannot show location."))
470
471
    def onToggleCondition(self, widget, action, data):
472
        self.update()
473
474
    def createUI(self):
475
        screenWidth, screenHeight = gdata.scrnSize
476
        # size of dialog in layout metrics (for SimpleGridLM)
477
        cols = 40
478
        rows = 29
479
        # dialog width and height in pixels
480
        isSmallWin = screenHeight == 600 and screenWidth == 800
481
        width = cols * 20 + 4 * (not isSmallWin)
482
        height = rows * 20 + 4 * (not isSmallWin)
483
        #creating dialog window
484
        self.win = ui.Window(self.app,
485
                             modal=1,
486
                             escKeyClose=1,
487
                             movable=0,
488
                             title=_("Problems Locator"),
489
                             titleOnly=isSmallWin,
490
                             rect=ui.Rect((screenWidth - 800 - 4 * (not isSmallWin)) / 2,
491
                                          (screenHeight - 600 - 4 * (not isSmallWin)) / 2,
492
                                          width,
493
                                          height),
494
                             layoutManager=ui.SimpleGridLM())
495
        self.win.subscribeAction('*', self)
496
        # first row is window title
497
        rows -= 1
498
499
        ui.Listbox(self.win, layout=(0, 0, cols, rows - 2), id='vProblems',
500
                   columns=[(_('Location'), 'text', 10, ui.ALIGN_W),
501
                   (_('Problem description'), 'vDescription', 30, ui.ALIGN_W)],
502
                   columnLabels=1, action='onShowSource', rmbAction='onShowLocation')
503
504
        btnWidth = 4
505
        ui.Check(self.win, layout=(btnWidth * 0, rows - 2, btnWidth, 1), id='vSystems',
506
                 text=_('Systems'), action='onToggleCondition', checked=1)
507
        ui.Check(self.win, layout=(btnWidth * 1, rows - 2, btnWidth, 1), id='vPlanets',
508
                 text=_('Planets'), action='onToggleCondition', checked=1)
509
        ui.Check(self.win, layout=(btnWidth * 2, rows - 2, btnWidth, 1), id='vFleets',
510
                 text=_('Fleets'), action='onToggleCondition', checked=1)
511
        ui.Check(self.win, layout=(btnWidth * 3, rows - 2, btnWidth, 1), id='vResearch',
512
                 text=_('Research'), action='onToggleCondition', checked=1)
513
514
        ui.Check(self.win, layout=(btnWidth * 6, rows - 2, btnWidth, 1), id='vCritical',
515
                 text=_('Critical'), action='onToggleCondition', checked=1)
516
        ui.Check(self.win, layout=(btnWidth * 7, rows - 2, btnWidth, 1), id='vMajor',
517
                 text=_('Major'), action='onToggleCondition', checked=1)
518
        ui.Check(self.win, layout=(btnWidth * 8, rows - 2, btnWidth, 1), id='vMinor',
519
                 text=_('Minor'), action='onToggleCondition', checked=1)
520
        ui.Check(self.win, layout=(btnWidth * 9, rows - 2, btnWidth, 1), id='vInfo',
521
                 text=_('Info'), action='onToggleCondition', checked=0)
522
523
        # dialog bottom line
524
        ui.Title(self.win, layout=(0, rows - 1, cols - 5, 1))
525
        ui.TitleButton(self.win, layout=(cols - 5, rows - 1, 5, 1), text=_("Close"), action='onClose')
526