Passed
Pull Request — master (#291)
by Marek
01:41
created

ProblemsDlg._addProblemsDefaultQueue()   B

Complexity

Conditions 8

Size

Total Lines 31
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 25
nop 4
dl 0
loc 31
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
    def _addProblemsSlots(self, problems, system):
312
        player = client.getPlayer()
313
        playerPlanets = set(system.planets) & set(player.planets)
314
        freeSlots = {}
315
        structSources = dict.fromkeys(system.planets)
316
        for planetID in system.planets:
317
            planet = client.get(planetID, noUpdate=1)
318
            freeSlots[planetID] = planet.plSlots - len(planet.slots)
319
320
        for planetID in playerPlanets:
321
            for targetID, quantity in self._getTargetSlotDict(planetID).items():
322
                freeSlots[targetID] -= quantity
323
                if quantity > 0:
324
                    try:
325
                        structSources[targetID].add(planetID)
326
                    except AttributeError:
327
                        structSources[targetID] = set([planetID])
328
329
        for planetID, free in freeSlots.items():
330
            if free < 0:
331
                # not enough space, report for every planet that builds on this one
332
                planet = client.get(planetID, noUpdate=1)
333
                for sourceID in structSources[planetID]:
334
                    source = client.get(sourceID, noUpdate=1)
335
                    problems.append(gdata.MAJ,
336
                                    ui.Item(source.name, tOID=sourceID, tType=Const.T_PLANET,
337
                                            vDescription=_('There is no space for all constructed buildings on %s.') % (planet.name)))
338
339
    def _addProblemsDefaultQueue(self, problems, planet, defaultQueueEtc):
340
        player = client.getPlayer()
341
        if not planet.effProdProd > 0:
342
            return
343
        planetEtc = 0
344
        # compute length of production queue
345
        for task in planet.prodQueue:
346
            if task.isShip:
347
                tech = player.shipDesigns[task.techID]
348
            else:
349
                tech = client.getFullTechInfo(task.techID)
350
            modifier = Rules.buildOnAnotherPlanetMod if task.targetID != planet.oid else 1
351
            planetEtc += math.ceil(float(task.quantity * tech.buildProd * modifier - task.currProd) / planet.effProdProd)
352
353
        etc = planetEtc + defaultQueueEtc
354
        # check empty production queue
355
        if not etc:
356
            problems.append(gdata.CRI,
357
                            ui.Item(planet.name, tOID=planet.oid, tType=Const.T_PLANET,
358
                                    vDescription=_('Production queue is empty.')))
359
            return
360
        if etc < Rules.turnsPerDay:
361
            severity = gdata.MAJ
362
        elif etc < Rules.turnsPerDay * 2:
363
            severity = gdata.MIN
364
        else:
365
            severity = gdata.INFO
366
367
        problems.append(severity,
368
                        ui.Item(planet.name, tOID=planet.oid, tType=Const.T_PLANET,
369
                                vDescription=_('Production queue may end in {0} turns ({1} directly in planet queue).'.format(res.formatTime(etc), res.formatTime(planetEtc)))))
370
371
    def show(self):
372
        critical = self.win.vCritical.checked
373
        major = self.win.vMajor.checked
374
        minor = self.win.vMinor.checked
375
        info = self.win.vInfo.checked
376
377
        disp = 1
378
379
        player = client.getPlayer()
380
        problems = self.Problems(self.win)
381
        systems = set([])
382
        for planetID in player.planets:
383
            planet = client.get(planetID, noUpdate=1)
384
            systems.add(planet.compOf)
385
386
        defaultQueueEtc = self._addProblemsGlobalQueues(problems)
387
388
        for systemID in systems:
389
            system = client.get(systemID, noUpdate=1)
390
            bio = 0
391
            totalBio = 0
392
            en = 0
393
            totalEn = 0
394
            # holds modified planets
395
            planetCopies = {}
396
397
            for planetID in system.planets:
398
                planet = client.get(planetID, noUpdate=1)
399
                # copy of planet to change plSlots count
400
                if hasattr(planet, 'owner') and planet.owner == player.oid:
401
                    # add planet to the global queue stats
402
                    # compute bio and en for system
403
                    bio += planet.changeBio
404
                    totalBio += max(0, planet.storBio - planet.minBio)
405
                    en += planet.changeEn
406
                    totalEn += max(0, planet.storEn - planet.minEn)
407
                    # the planet needs to have global queue 0 - the default one - to have its queue reported
408
                    if self.win.vPlanets.checked:
409
                        if not planet.globalQueue:
410
                            self._addProblemsDefaultQueue(problems, planet, defaultQueueEtc)
411
                        for struct in planet.slots:
412
                            self._addProblemsStructStatus(problems, struct, planet)
413
414
            # free slots within the system
415
            self._addProblemsSlots(problems, system)
416
            # check bio for system
417
            if self.win.vSystems.checked:
418
                self._addProblemsMaterial(problems, system, bio, totalBio, 'Bio')
419
                self._addProblemsMaterial(problems, system, en, totalEn, 'En')
420
421
        # check fleets
422
        if self.win.vFleets.checked:
423
            for fleetID in player.fleets:
424
                fleet = client.get(fleetID, noUpdate=1)
425
                self._addProblemsFleets(problems, fleet)
426
427
        # check research queue
428
        if self.win.vResearch.checked:
429
            self._addProblemsResearch(problems)
430
431
        self.win.vProblems.items = problems.items
432
        self.win.vProblems.itemsChanged()
433
434
    def onClose(self, widget, action, data):
435
        self.hide()
436
437
    def onShowSource(self, widget, action, data):
438
        item = self.win.vProblems.selection[0]
439
        if item.tType == Const.T_FLEET:
440
            object = client.get(item.tOID, noUpdate=1)
441
            # center on map
442
            if hasattr(object, "x"):
443
                gdata.mainGameDlg.win.vStarMap.highlightPos = (object.x, object.y)
444
                gdata.mainGameDlg.win.vStarMap.setPos(object.x, object.y)
445
                self.hide()
446
                return
447
        elif item.tType in (Const.T_SYSTEM, Const.T_PLANET):
448
            if item.tOID != Const.OID_NONE:
449
                gdata.mainGameDlg.onSelectMapObj(None, None, item.tOID)
450
                return
451
        elif item.tType == Const.T_TECHNOLOGY:
452
            gdata.mainGameDlg.researchDlg.display()
453
            return
454
        elif item.tType == Const.T_QUEUE:
455
            gdata.mainGameDlg.globalQueuesDlg.display()
456
        self.win.setStatus(_("Cannot show location."))
457
458
    def onShowLocation(self, widget, action, data):
459
        item = self.win.vProblems.selection[0]
460
        if item.tType in (Const.T_SYSTEM, Const.T_PLANET, Const.T_FLEET):
461
            object = client.get(item.tOID, noUpdate=1)
462
            # center on map
463
            if hasattr(object, "x"):
464
                gdata.mainGameDlg.win.vStarMap.highlightPos = (object.x, object.y)
465
                gdata.mainGameDlg.win.vStarMap.setPos(object.x, object.y)
466
                self.hide()
467
                return
468
        self.win.setStatus(_("Cannot show location."))
469
470
    def onToggleCondition(self, widget, action, data):
471
        self.update()
472
473
    def createUI(self):
474
        screenWidth, screenHeight = gdata.scrnSize
475
        # size of dialog in layout metrics (for SimpleGridLM)
476
        cols = 40
477
        rows = 29
478
        # dialog width and height in pixels
479
        isSmallWin = screenHeight == 600 and screenWidth == 800
480
        width = cols * 20 + 4 * (not isSmallWin)
481
        height = rows * 20 + 4 * (not isSmallWin)
482
        #creating dialog window
483
        self.win = ui.Window(self.app,
484
                             modal=1,
485
                             escKeyClose=1,
486
                             movable=0,
487
                             title=_("Problems Locator"),
488
                             titleOnly=isSmallWin,
489
                             rect=ui.Rect((screenWidth - 800 - 4 * (not isSmallWin)) / 2,
490
                                          (screenHeight - 600 - 4 * (not isSmallWin)) / 2,
491
                                          width,
492
                                          height),
493
                             layoutManager=ui.SimpleGridLM())
494
        self.win.subscribeAction('*', self)
495
        # first row is window title
496
        rows -= 1
497
498
        ui.Listbox(self.win, layout=(0, 0, cols, rows - 2), id='vProblems',
499
                   columns=[(_('Location'), 'text', 10, ui.ALIGN_W),
500
                   (_('Problem description'), 'vDescription', 30, ui.ALIGN_W)],
501
                   columnLabels=1, action='onShowSource', rmbAction='onShowLocation')
502
503
        btnWidth = 4
504
        ui.Check(self.win, layout=(btnWidth * 0, rows - 2, btnWidth, 1), id='vSystems',
505
                 text=_('Systems'), action='onToggleCondition', checked=1)
506
        ui.Check(self.win, layout=(btnWidth * 1, rows - 2, btnWidth, 1), id='vPlanets',
507
                 text=_('Planets'), action='onToggleCondition', checked=1)
508
        ui.Check(self.win, layout=(btnWidth * 2, rows - 2, btnWidth, 1), id='vFleets',
509
                 text=_('Fleets'), action='onToggleCondition', checked=1)
510
        ui.Check(self.win, layout=(btnWidth * 3, rows - 2, btnWidth, 1), id='vResearch',
511
                 text=_('Research'), action='onToggleCondition', checked=1)
512
513
        ui.Check(self.win, layout=(btnWidth * 6, rows - 2, btnWidth, 1), id='vCritical',
514
                 text=_('Critical'), action='onToggleCondition', checked=1)
515
        ui.Check(self.win, layout=(btnWidth * 7, rows - 2, btnWidth, 1), id='vMajor',
516
                 text=_('Major'), action='onToggleCondition', checked=1)
517
        ui.Check(self.win, layout=(btnWidth * 8, rows - 2, btnWidth, 1), id='vMinor',
518
                 text=_('Minor'), action='onToggleCondition', checked=1)
519
        ui.Check(self.win, layout=(btnWidth * 9, rows - 2, btnWidth, 1), id='vInfo',
520
                 text=_('Info'), action='onToggleCondition', checked=0)
521
522
        # dialog bottom line
523
        ui.Title(self.win, layout=(0, rows - 1, cols - 5, 1))
524
        ui.TitleButton(self.win, layout=(cols - 5, rows - 1, 5, 1), text=_("Close"), action='onClose')
525