patternpythonMinor
Movement code for a game that should be able to handle almost infinite coordinates
Viewed 0 times
infinitecoordinateshandlealmostmovementgamethatforshouldcode
Problem
Now I've grasped the very basics of Pygame, I thought it'd be useful to make a few classes that I could use later on if I try make a simple RPG style game. I've done this to handle the coordinates for the player movement (and possibly for other things too), and tried to do it in a way where you wouldn't get the floating point precision errors that Minecraft and similar games get when you travel out very far.
I tested it against the
It's only half a days worth of work so it's not perfect, I can't find any more bugs though. It's recommended you input the coordinates as strings though if you're using large ones to start with, especially if you're using floats.
```
class Movement(object):
"""This was built to allow large coordinates to be stored without
causing any floating point precision errors.
It is faster than the decimal module, especially with processing
large amounts of small movements.
It works by breaking down the coordinates into 'blocks', where each
new block is the squared amount of the previous one.
The method is very similar to the base number system, in which a
block size of 10 will split 235.9 into [5.9, 3, 2].
A large block size is faster than a small one, though the precision
will be worse. At 16 digits, Python can't store any more decimals,
so definitely keep it under that.
"""
BLOCK_SIZE = 65535
COORDINATES = range(3)
def __init__(self, x=0, y=0, z=0, block_size=None):
"""Convert the starting coordinates into the format accepted
by the class.
>>> m = Movement('15',
... '-31564.99933425584842',
... '1699446367870005.2')
>>> m.player_loc
[[15.0], [-31564.99933425585], [38640.2, 17514, 2485, 6]]
>>> print m
(15.0, -31564.9993343
I tested it against the
decimal module, and mine appeared to up to 10x faster for small movements, though the speeds evened out when using super large movements like +- 10000000000000.It's only half a days worth of work so it's not perfect, I can't find any more bugs though. It's recommended you input the coordinates as strings though if you're using large ones to start with, especially if you're using floats.
```
class Movement(object):
"""This was built to allow large coordinates to be stored without
causing any floating point precision errors.
It is faster than the decimal module, especially with processing
large amounts of small movements.
It works by breaking down the coordinates into 'blocks', where each
new block is the squared amount of the previous one.
The method is very similar to the base number system, in which a
block size of 10 will split 235.9 into [5.9, 3, 2].
A large block size is faster than a small one, though the precision
will be worse. At 16 digits, Python can't store any more decimals,
so definitely keep it under that.
"""
BLOCK_SIZE = 65535
COORDINATES = range(3)
def __init__(self, x=0, y=0, z=0, block_size=None):
"""Convert the starting coordinates into the format accepted
by the class.
>>> m = Movement('15',
... '-31564.99933425584842',
... '1699446367870005.2')
>>> m.player_loc
[[15.0], [-31564.99933425585], [38640.2, 17514, 2485, 6]]
>>> print m
(15.0, -31564.9993343
Solution
_get_integer and _get_decimal
A lot to say here. First, you’ve got a bug. Consider
An other thing to note, here, is that both functions will always be called together. And that they both convert the same argument into a string to split it right after. It’s redundant. Better have only one function that will split the string and return both the integral and the decimal part as a tuple. Moreover, it’ll be easier to fix the bug stated above:
Last and not least: do not confuse yourself between
On
The usual way to define custom exceptions is just:
you then raise it with any custom message you want. What is important to note, is that you raise it explicitly wherever you need to. Using a static method (instead of the class method you used) to do it for you is not common and impairs readability. If you really need to save on typing predifined error messages you can either:
On reinventing the wheel
There is a bunch of built-ins that you could use to simplify the reading of your code. For insance
Lastly,
And since you access
Proposed improvements
```
from math import copysign
from itertools import count, repeat, izip, chain
class Movement(object):
"""This was built to allow large coordinates to be stored without
causing any floating point precision errors.
It is faster than the decimal module, especially with processing
large amounts of small movements.
It works by breaking down the coordinates into 'blocks', where each
new block is the squared amount of the previous one.
The method is very similar to the base number system, in which a
block size of 10 will split 235.9 into [5.9, 3, 2].
A large block size is faster than a small one, though the precision
will be worse. At 16 digits, Python can't store any more decimals,
so definitely keep it under that.
"""
BLOCK_SIZE = 65535
def __init__(self, x=0, y=0, z=0, block_size=None):
"""Convert the starting coordinates into the format accepted
by the class.
>>> m = Movement('15',
... '-31564.99933425584842',
... '1699446367870005.2')
>>> m.player_loc
[[15.0], [-31564.99933425585], [38640.2, 17514, 2485, 6]]
>>> print m
(15.0, -31564.9993343, 1699446367870005.2)
"""
#Set a new block size if needed
if block_size is not None:
self.BLOCK_SIZE = block_size
#Store the initial coordinates
self.player_loc = [self.calculate(*self._split_decimal_part(i))
for i in (x, y, z)]
def __repr__(self):
"""This needs improving, currently it just converts back to
the absolute coordinates."""
return 'Movement{}'.format(self)
def __str__(self):
"""Return the absolute coordinates."""
return '({}, {}, {})'.format(*self._convert_to_world())
def __getitem__(self, i):
"""Return an absolute value for X, Y or Z.
Parameters:
i (int): Index of coordinate.
"""
try:
return self._convert_to_world()[i]
except Ind
A lot to say here. First, you’ve got a bug. Consider
float('.0') and float('3.') which are valid calls that anyone may expect to work alike with your Movement class. Unfortunately, both Movement('.0') and Movement('3.') fails. This is due to an empty string being returned as one part or the other of the split('.'). And int('') is not valid.An other thing to note, here, is that both functions will always be called together. And that they both convert the same argument into a string to split it right after. It’s redundant. Better have only one function that will split the string and return both the integral and the decimal part as a tuple. Moreover, it’ll be easier to fix the bug stated above:
def split_decimal_part(number):
n = str(number)
try:
integral, decimal = n.split('.')
except ValueError: # no '.' in `n`
return int(n), 0
else:
# Account for either integral or decimal being ''
return int(integral) if integral else 0, int(decimal) if decimal else 0Last and not least: do not confuse yourself between
@classmethod and @staticmethod. Class methods are implicitly passed the class instance as first parameter (usualy named cls) and not self which is an instance of the class. Static methods does not have implicit parameters at all. They just use the class or the instance type as a namespace. Since you make no use of the class parameter of your @classmethods, they should be @staticmethods instead.On
@classmethod againThe usual way to define custom exceptions is just:
class MyCustomException(Exception):
passyou then raise it with any custom message you want. What is important to note, is that you raise it explicitly wherever you need to. Using a static method (instead of the class method you used) to do it for you is not common and impairs readability. If you really need to save on typing predifined error messages you can either:
- store them as constants/class attributes;
- define them as default value for the constructor.
On reinventing the wheel
There is a bunch of built-ins that you could use to simplify the reading of your code. For insance
divmod, math.copysign or zip. You’re also using two methods for building your string outputs where they very much look alike.Lastly,
_move is very unintuitive with its two passes, its decimal flag (yes it takes a while to figure out it is both some kind of flag and some values stored) and its stop one. The least intuitive thing, however, is how you differentiate things whether you’re adding more or less than a block size on a given axis. That lead to all issues stated above. You should add the decimal parts and then take care of the integral ones using your block approach.And since you access
self.player_loc[direction] a lot, you should make it a local variable.Proposed improvements
```
from math import copysign
from itertools import count, repeat, izip, chain
class Movement(object):
"""This was built to allow large coordinates to be stored without
causing any floating point precision errors.
It is faster than the decimal module, especially with processing
large amounts of small movements.
It works by breaking down the coordinates into 'blocks', where each
new block is the squared amount of the previous one.
The method is very similar to the base number system, in which a
block size of 10 will split 235.9 into [5.9, 3, 2].
A large block size is faster than a small one, though the precision
will be worse. At 16 digits, Python can't store any more decimals,
so definitely keep it under that.
"""
BLOCK_SIZE = 65535
def __init__(self, x=0, y=0, z=0, block_size=None):
"""Convert the starting coordinates into the format accepted
by the class.
>>> m = Movement('15',
... '-31564.99933425584842',
... '1699446367870005.2')
>>> m.player_loc
[[15.0], [-31564.99933425585], [38640.2, 17514, 2485, 6]]
>>> print m
(15.0, -31564.9993343, 1699446367870005.2)
"""
#Set a new block size if needed
if block_size is not None:
self.BLOCK_SIZE = block_size
#Store the initial coordinates
self.player_loc = [self.calculate(*self._split_decimal_part(i))
for i in (x, y, z)]
def __repr__(self):
"""This needs improving, currently it just converts back to
the absolute coordinates."""
return 'Movement{}'.format(self)
def __str__(self):
"""Return the absolute coordinates."""
return '({}, {}, {})'.format(*self._convert_to_world())
def __getitem__(self, i):
"""Return an absolute value for X, Y or Z.
Parameters:
i (int): Index of coordinate.
"""
try:
return self._convert_to_world()[i]
except Ind
Code Snippets
def split_decimal_part(number):
n = str(number)
try:
integral, decimal = n.split('.')
except ValueError: # no '.' in `n`
return int(n), 0
else:
# Account for either integral or decimal being ''
return int(integral) if integral else 0, int(decimal) if decimal else 0class MyCustomException(Exception):
passfrom math import copysign
from itertools import count, repeat, izip, chain
class Movement(object):
"""This was built to allow large coordinates to be stored without
causing any floating point precision errors.
It is faster than the decimal module, especially with processing
large amounts of small movements.
It works by breaking down the coordinates into 'blocks', where each
new block is the squared amount of the previous one.
The method is very similar to the base number system, in which a
block size of 10 will split 235.9 into [5.9, 3, 2].
A large block size is faster than a small one, though the precision
will be worse. At 16 digits, Python can't store any more decimals,
so definitely keep it under that.
"""
BLOCK_SIZE = 65535
def __init__(self, x=0, y=0, z=0, block_size=None):
"""Convert the starting coordinates into the format accepted
by the class.
>>> m = Movement('15',
... '-31564.99933425584842',
... '1699446367870005.2')
>>> m.player_loc
[[15.0], [-31564.99933425585], [38640.2, 17514, 2485, 6]]
>>> print m
(15.0, -31564.9993343, 1699446367870005.2)
"""
#Set a new block size if needed
if block_size is not None:
self.BLOCK_SIZE = block_size
#Store the initial coordinates
self.player_loc = [self.calculate(*self._split_decimal_part(i))
for i in (x, y, z)]
def __repr__(self):
"""This needs improving, currently it just converts back to
the absolute coordinates."""
return 'Movement{}'.format(self)
def __str__(self):
"""Return the absolute coordinates."""
return '({}, {}, {})'.format(*self._convert_to_world())
def __getitem__(self, i):
"""Return an absolute value for X, Y or Z.
Parameters:
i (int): Index of coordinate.
"""
try:
return self._convert_to_world()[i]
except IndexError:
raise MovementError(MovementError.COORDINATE_INDEX_ERROR)
def __setitem__(self, i, n):
"""Set an absolute value for X, Y or Z.
Parameters:
i (int): Index of coordinate.
n (int/float/str): New value to set.
"""
try:
self.player_loc[i] = self.calculate(*self._split_decimal_part(n))
except IndexError:
raise MovementError(MovementError.COORDINATE_INDEX_ERROR)
@staticmethod
def _split_decimal_part(n):
"""Split the input into its integral part and its decimal part.
Parameters:
n (str/int/float): Integer to convert.
>>> Movement._split_decimal_part('15.35321')
(15, 35321)
"""
n = str(n)
try:
integral, decimal = n.split('.')
except ValueError:
return int(n), 0
else:
return int(integrcoordinates = [str(sum(int(amount) * self.BLOCK_SIZE**i
for i, amount in enumerate(coordinate)))
+ '.' + str(coordinate[0]).split('.')[1]
for coordinate in self.player_loc]Context
StackExchange Code Review Q#110186, answer score: 3
Revisions (0)
No revisions yet.