Cycling through a list using yield

Let’s say I have a list of nodes in my game, that are represented here as self.ships[i]=“string”. I want to cycle through them via the Tab key, where a Tab press increases the index by 1. However, I don’t want to use an external index holder, like self.index = i. Instead I’d like to initialize the scanner with 0, increase the inxed via Tab press by 1, and yield the index within the scanner function. Then, the index would be increased to 2 by the next Tab press.

I guess a different container type, like a ring buffer would be better for this. Still, it should be possible doing this via this yield code bit, where a StopIteration exception thrown by the scanner indicates that the index has grown over the length of the list, resetting it to 0.

Can anybody help?

import direct.directbase.DirectStart
from direct.showbase.ShowBase import ShowBase
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import *

class GeneratorTest(DirectObject):
	def __init__(self):
		self.index = None
		# or = 0; but should not even be necessary
		self.ships = {1:"node1", 2:"node2", 3:"node3", 4:"node4", 5:"node5"}
		# nodes in my game, here as strings
		self.scanner = self.scan(0)
		self.accept("tab", self.do_scan)
		
	def do_scan(self):
		# do not: self.index += 1
		try:
			self.scanner.next()
		except StopIteration:
			print "no items left"
			# somehow reset the index to 0
		
	def scan(self, n):
		print "scan me"
		scannable = [i for i in self.ships if i != 3]
		print scannable[n]
		yield n
		n += 1
		
gt = GeneratorTest()		
run()

I don’t get it. First you say you don’t want any index, but on the other side you use a counter “n” and have an attribute “index”, which seem to serve exactly that purpose.
What is the n for?
Do you plan to cycle back or only in one direction?
Do you want to create a new list of ships every time tab is pressed or cycle through the same list?
And what’s so bad about having the index as attribute?

Well I put the self.index in the init to show what I not want. That’s why there’s a comment indicating that. That’s why inside the scanner there’s a comment, too. Why do I not want that? Well, because there are a lot of indexes to cycle through in my game: weapons, ships, shop items etc. That aside, it’s just clutter. Preferably, the index exists only within the scanner function, is initialized with 0 and is yielded in the scanner.

There is list.iter(), an iterator, which behaves exactly as what you currently have. Keep the iterator around, and call next().

You do not need to reset the counter to 0 yourself. Python comes with itertools.cycle (1) for that purpose.

import itertools
...
    def __init__(self):
        self.ships = {1:"node1", 2:"node2", 3:"node3", 4:"node4", 5:"node5"}
        self.nextShipNumber = itertools.cycle(self.ships).next
        self.accept("tab", self.do_scan) 

    def do_scan(self):
        ship_number = self.nextShipNumber()
        print "Hello", ship_number

If you want NOT to scan ship number 3, you could combine it with a filter (2).

    def __init__(self):
        self.ships = {1:"node1", 2:"node2", 3:"node3", 4:"node4", 5:"node5"}
        do_not_scan = [3]
        scannable_ships = itertools.ifilterfalse(do_not_scan.__contains__, self.ships)
        self.nextShipNumber = itertools.cycle(scannable_ships).next

One worry though: this leads to a very static code, because as soon as the dictionary changes size, the whole thing breaks with RuntimeError: dictionary changed size during iteration. Your code wouldn’t have sploded like that, but it was working with a copy of the dictionary keys so it was frozen as well.

(1) http://docs.python.org/library/itertools.html#itertools.cycle
(2) http://docs.python.org/library/itertools.html#itertools.ifilter

Well, this code works. But the problem mentioned by Niriel, where the list of ships remains the same even though ships were added, is a serious one. It has to be dealt with and I am not sure how.

import direct.directbase.DirectStart
from direct.showbase.ShowBase import ShowBase
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import *

from itertools import cycle, ifilter, ifilterfalse

class GeneratorTest(DirectObject):
	def __init__(self):
	
		self.ships = {1:"node1", 2:"node2", 3:"node3", 4:"node4", 5:"node5", 6:"node6", 7:"node7"}
		self.ships_iscannable = cycle(ifilter(self.filter_ships, self.ships))
		
		self.accept("tab", self.do_filtered_scan)
		self.accept("a", self.add_ship)
		
	def filter_ships(self, n):
		scannable = [i for i in self.ships if i != 3]
		if n in scannable:
			return True
			
	def do_filtered_scan(self):
		print self.ships_iscannable.next()
		
	def add_ship(self):
		plus_i = sorted(self.ships, reverse=True)[-1] + 1
		self.ships[plus_i] = "node%s" % plus_i
		print plus_i, self.ships
		
gt = GeneratorTest()		

run()

Not sure about that. next(iterable) was added in Python 2.6, but ppython here on my Mac reports as 2.5.4. So,

def do_iterscan(self):
		iscannable = iter([i for i in self.ships if i != 3])
		print next(iscannable)

does not work here.

What panda version are you using? 1.7 uses python 2.6 and 1.8 uses python 2.7.2. Time to upgrade :wink:

I am using a builtbot build dating May 12th, 2012.

Panda3D uses Python 2.5 on Mac, regardless of the OS X version or the Panda3D version.

I dealt with the problem of iterating through a list/dictionary that could change, in the past.

It goes like this:

def getNext(lst, elem):
    if elem in lst:
        idx = (lst.index(elem) + 1) % len(lst)
    else:
        idx = 0
    return lst[idx]

def getPrevious(lst, elem):
    if elem in lst:
        idx = lst.index(elem) - 1
    else:
        idx = 0
    return lst[idx]

# How to use it:
>>> ships = ['Death Star', 'Galactica', 'Serenity']
>>> ship = ships[0]
>>> ship = getNext(ships, ship)
>>> print ship
'Galactica'
>>> ships.remove('Galactica')
>>> ship = getNext(ships, ship)
>>> print ship
'Death Star' # Galactica's gone so it has no next, we start at 0 again.
>>> ship = getNext(ships, ship)
>>> print ship
'Serenity' # Since no Galactica, jumps to Serenity directly.
>>> ship = getNext(ships, ship)
>>> print ship
'Death Star' # It wraps around.

Working with dictionary’s isn’t much harder.

def getNextDic(dic, key):
    keys = dic.keys()
    return getNext(keys, key) # Defined earlier in this post.

def getPreviousDic(dic, key):
    keys = dic.keys()
    return getPrevious(keys, key)

>>> ships = {'big':'Death Star', 'med':'Galactica', 'small':'Serenity'}
>>> ship_key = 'big'
>>> print ship_key, ships[ship_key]
big Death Star
>>> ship_key = getNextDic(ships, ship_key)
>>> print ship_key, ships[ship_key]
med Galactica

Note that the order in a dictionary is arbitrary BUT consistent. You could get the order big - med - small, or the order big - small - med. It doesn’t matter, what matters is that you will go through the whole thing without repetitions.

And that gives you iterations over a list of a dictionary that can change at runtime :slight_smile:.

Alright, thanks for your help. This piece is what finally works.

The following criteria are met:

  • ships are stored in a dict, that can lose and gain elements
  • hitting tab, progress the scanner by one ship at a time
  • exclude player ships and their escorts from scanning
  • when arriving at the last ship, quit the scanning process
  • when no target is selected, select the first
  • scan the ships in a predictable order
# self.ships:
# players: id 1 to 16
# ships: 17+

# self.accept("tab", self.sel_target)
# reparent_target_frame moves the selection frame to the next node
# detach_target_frame detaches the selection frame, making it invisible

### TARGETING ###
		
def sel_target(self):
	if len(shared.env.ships) <= 1: # player is alone
		self.unset_target()
		return # quit here already, play failure sound
		
	# more conditions, like excluding special ai, multiplayer elements
	selectable = [id for id in shared.env.ships if (id not in self.escort_ids and 16 < id)]
	selectable = sorted(selectable)
	
	if not self.flags["targetid"]: # has no target set		
		self.set_target(selectable[0])
		shared.env.reparent_target_frame(self.flags["targetid"])
		return
		
	if self.flags["targetid"] in selectable:
		idx = selectable.index(self.flags["targetid"]) + 1
		if idx >= len(selectable):
			self.unset_target()
			shared.env.detach_target_frame()
		else:
			self.set_target(selectable[idx])
			shared.env.reparent_target_frame(self.flags["targetid"])
	else:
		self.set_target(selectable[0])
		shared.env.reparent_target_frame(self.flags["targetid"])