Passed
Pull Request — master (#291)
by Marek
12:50
created

ProblemsDlg._addProblemsResearch()   B

Complexity

Conditions 6

Size

Total Lines 28
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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