Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding MemoryDecay strategy. Check if ready to merge, please. #1137

Merged
merged 27 commits into from
Nov 14, 2017
Merged

Conversation

vdeni
Copy link
Contributor

@vdeni vdeni commented Nov 9, 2017

Hi! I think I've managed to implement the strategy I wanted. Following a short discussion with dr Knight on Gitter, I've decided to make the strategy a child class of the MetaPlayer, and have appended it to the meta.py file (MemoryDecay class) in the strategies.

The test file is test_memory_decay.py.

Also, I've fixed a few typos in the docs.

I'd appreciate it if you'd take a look at the code and see if it's ready for merging. If there's anything wrong, please inform me, and I'll do my best to fix it.

Regards,

Denis Vlašiček

@vdeni vdeni changed the title Decay Adding MemoryDecay strategy. Check if ready to merge, please. Nov 9, 2017
Copy link
Member

@drvinceknight drvinceknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've picked up a few things that are causing errors and test failures.

A couple of other things (that might be worth trying to pick up):

👍 :)

@@ -193,7 +193,6 @@
MathConstantHunter,
NaiveProber,
MEM2,
Michaelos,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not delete this line.

@@ -0,0 +1,78 @@
"""Tests for the Memory Decay strategy"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the strategy is going to stay in the meta.py module, have the tests in the test_meta.py module please.

@@ -0,0 +1,78 @@
"""Tests for the Memory Decay strategy"""

import axelrod as axe
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention throughout the library is to import axelrod as axl

#Test TitForTat behavior in first 15 turns
opponent = axe.Cooperator()
actions = list([(C, C)]) * 15
self.versus_test(opponent, expected_actions = actions)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8 require no spaces around the = in function calls:

self.versus_test(opponent, expected_actions=actions)

(Throughout this file.)

@@ -102,6 +102,9 @@ Here are the docstrings of all the strategies in the library.
.. automodule:: axelrod.strategies.mathematicalconstants
:members:
:undoc-members:
.. automodule:: axelrod.strategies.memorydecay
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need for this as the strategy is not in this module but in axelrod.strategies.meta (which is already included).


class MemoryDecay(MetaPlayer):
"""
A player utilizes the (default) Tit for Tat stretegy for the first (default) 15 turns,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strategy

passed, the player calculates a 'net cooperation score' (NCS) for his opponent,
weighing decisions to Cooperate as (default) 1, and to Defect as (default)
-2. If the opponent's NCS is below 0, the player Defects; otherwise,
he Cooperates.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the play defects

they cooperate.

-2. If the opponent's NCS is below 0, the player Defects; otherwise,
he Cooperates.

The player's memories of his opponent's decisions have a random chance to be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of the opponent's


def __init__(self, p_memory_delete: float = 0.1, p_memory_alter: float = 0.03,
loss_value: float = -2, gain_value: float = 1,
memory: list = None, start_strategy: str = 'Tit For Tat',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just pass the actual strategy class: start_strategy: axl.Player = axl.TitForTat, then the complicated search code can be simplified.

Something like:

self.team = [start_strategy()]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, so, if i change the __init__ to

    def __init__(self, p_memory_delete: float = 0.1, p_memory_alter: float = 0.03,
                 loss_value: float = -2, gain_value: float = 1,
                 memory: list = None, start_strategy: axelrod.Player = axelrod.TitForTat,
                 start_strategy_duration: int = 15):

I get this when trying to import the library:

>>> import axelrod
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/denis/Documents/Axelrod/axelrod/__init__.py", line 17, in <module>
    from .strategies import *
  File "/home/denis/Documents/Axelrod/axelrod/strategies/__init__.py", line 9, in <module>
    from .meta import (
  File "/home/denis/Documents/Axelrod/axelrod/strategies/meta.py", line 538, in <module>
    class MemoryDecay(MetaPlayer):
  File "/home/denis/Documents/Axelrod/axelrod/strategies/meta.py", line 569, in MemoryDecay
    memory: list = None, start_strategy: axelrod.Player = axelrod.TitForTat,
NameError: name 'axelrod' is not defined

If I add import axelrod to the beginning of meta.py (which, I suppose, shouldn't be necessary?), I get this:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/denis/Documents/Axelrod/axelrod/__init__.py", line 17, in <module>
    from .strategies import *
  File "/home/denis/Documents/Axelrod/axelrod/strategies/__init__.py", line 9, in <module>
    from .meta import (
  File "/home/denis/Documents/Axelrod/axelrod/strategies/meta.py", line 539, in <module>
    class MemoryDecay(MetaPlayer):
  File "/home/denis/Documents/Axelrod/axelrod/strategies/meta.py", line 570, in MemoryDecay
    memory: list = None, start_strategy: axelrod.Player = axelrod.TitForTat,
AttributeError: module 'axelrod' has no attribute 'TitForTat'

Any ideas?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go with:

from axelrod.strategies import TitForTat
...
..., axelrod.Player = TitForTat

(This is due to the order of imports when loading the library itself.)

@@ -1,4 +1,5 @@
from numpy.random import choice
from random import random,choice
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is overwriting the choice imported in numpy.random and causing numerous failures of other tests.

I suggest importing import random and using random.random but using numpy's choice (which has similar behaviour) - alternatively you could use random.choice.

@drvinceknight
Copy link
Member

axelrod/tests/strategies/.test_memory_decay.py.swp will need to be deleted.

@vdeni
Copy link
Contributor Author

vdeni commented Nov 10, 2017

I think I've applied all the suggested changes. I ran the doctest and it says:

----------------------------------------------------------------------
Ran 59 tests in 37.711s

OK

self.gain_value = gain_value
self.memory = [] if memory == None else memory
self.start_strategy_duration = start_strategy_duration
self.team = start_strategy()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are numerous unit tests that are failing. This is because self.team needs to be a list (even if it only contains 1 strategy):

self.team = [start_strategy()]

There's information on running tests here: http://axelrod.readthedocs.io/en/stable/tutorials/contributing/running_tests.html#running-tests

but let me know if you'd like a hand :)

Copy link
Contributor Author

@vdeni vdeni Nov 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed self.team to a list and ran unittest.

There are a few errors I don't know how to handle, so I'd appreciate it if you'd take a look.

1 > python -m unittest axelrod.tests.strategies.test_meta
.F.F..E..........................................................................................................................................................
======================================================================
ERROR: test_strategy (axelrod.tests.strategies.test_meta.TestMemoryDecay)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/denis/Documents/Axelrod/axelrod/tests/strategies/test_meta.py", line 633, in test_strategy
    init_kwargs = {'start_strategy': 'Defector'})
  File "/home/denis/Documents/Axelrod/axelrod/tests/strategies/test_player.py", line 490, in versus_test
    player = self.player(**init_kwargs)
  File "/home/denis/Documents/Axelrod/axelrod/strategies/meta.py", line 579, in __init__
    self.team = [start_strategy()]
TypeError: 'str' object is not callable

======================================================================
FAIL: test_initialisation (axelrod.tests.strategies.test_meta.TestMemoryDecay)
Test that the player initiates correctly.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/denis/Documents/Axelrod/axelrod/tests/strategies/test_player.py", line 372, in test_initialisation
    self.classifier_test(self.expected_class_classifier)
  File "/home/denis/Documents/Axelrod/axelrod/tests/strategies/test_player.py", line 512, in classifier_test
    self.assertEqual(expected_class_classifier, self.player.classifier)
AssertionError: {'mem[72 chars]of': {'length', 'game'}, 'inspects_source': Fa[56 chars]alse} != {'mem[72 chars]of': set(), 'inspects_source': False, 'manipul[43 chars]alse}
  {'inspects_source': False,
   'long_run_time': False,
-  'makes_use_of': {'length', 'game'},
+  'makes_use_of': set(),
   'manipulates_source': False,
   'manipulates_state': False,
   'memory_depth': inf,
   'stochastic': True}

======================================================================
FAIL: test_repr (axelrod.tests.strategies.test_meta.TestMemoryDecay)
Test that the representation is correct.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/denis/Documents/Axelrod/axelrod/tests/strategies/test_player.py", line 377, in test_repr
    self.assertEqual(str(self.player()), self.name)
AssertionError: 'Memory Decay: 1 player' != 'MemoryDecay'
- Memory Decay: 1 player
+ MemoryDecay


----------------------------------------------------------------------
Ran 161 tests in 56.801s

FAILED (failures=2, errors=1)

Also, thank you for being so patient and helpful. Really means a lot to me, being new to all this :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, thank you for being so patient and helpful. Really means a lot to me, being new to all this :)

No problem at all, appreciate the contribution. We're not far now, hopefully below I've pointed out everything that needs fixing. Not a problem if it's still not quite there though and we need to figure something else out :) 👍

Copy link
Member

@drvinceknight drvinceknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few tests are still failing:

======================================================================
ERROR: test_strategy (axelrod.tests.strategies.test_meta.TestMemoryDecay)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\projects\axelrod\axelrod\tests\strategies\test_meta.py", line 633, in test_strategy
    init_kwargs = {'start_strategy': 'Defector'})
  File "C:\projects\axelrod\axelrod\tests\strategies\test_player.py", line 490, in versus_test
    player = self.player(**init_kwargs)
  File "C:\projects\axelrod\axelrod\strategies\meta.py", line 579, in __init__
    self.team = [start_strategy()]
TypeError: 'str' object is not callable
======================================================================
FAIL: test_initialisation (axelrod.tests.strategies.test_meta.TestMemoryDecay)
Test that the player initiates correctly.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\projects\axelrod\axelrod\tests\strategies\test_player.py", line 372, in test_initialisation
    self.classifier_test(self.expected_class_classifier)
  File "C:\projects\axelrod\axelrod\tests\strategies\test_player.py", line 512, in classifier_test
    self.assertEqual(expected_class_classifier, self.player.classifier)
AssertionError: {'mem[13 chars]nf, 'manipulates_source': False, 'stochastic':[115 chars]alse} != {'mem[13 chars]nf, 'inspects_source': False, 'manipulates_sou[102 chars]alse}
  {'inspects_source': False,
   'long_run_time': False,
-  'makes_use_of': {'length', 'game'},
+  'makes_use_of': set(),
   'manipulates_source': False,
   'manipulates_state': False,
   'memory_depth': inf,
   'stochastic': True}
======================================================================
FAIL: test_repr (axelrod.tests.strategies.test_meta.TestMemoryDecay)
Test that the representation is correct.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\projects\axelrod\axelrod\tests\strategies\test_player.py", line 377, in test_repr
    self.assertEqual(str(self.player()), self.name)
AssertionError: 'Memory Decay: 1 player' != 'MemoryDecay'
- Memory Decay: 1 player
+ MemoryDecay

  • Looks like there's a test that's still passing Defector (and others below) as a string (need to pass axl.Defector)

  • The classifier test isn't passing. It's because the classification is being updated by the parent class's init. So let's remove self.team = [starting_strategy()] and modify line 572 to be:

    572         super().__init__(team=[start_strategy])
    

    This ensures that the classification of the strategy gets updated properly.

  • The repr is still using the parent classe's __repr__ can you write a new __repr__ along the lines of:

    def __repr__(self):
         return axl.Player.__repr__(self)
    

    This way the string representation of the strategy will use the default repr method which includes all init keywords.

👍

class TestMemoryDecay(TestPlayer):

name = 'MemoryDecay'
player = axelrod.meta.MemoryDecay
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This strategy will need to be added to the axelrod/strategies/__init__.py file (where all meta strategies get imported), when you've done that you should be able to go with player = axelrod.MemoryDecay.

@vdeni
Copy link
Contributor Author

vdeni commented Nov 13, 2017

I ran python -m unittest discover:

----------------------------------------------------------------------
Ran 2771 tests in 522.636s

OK (expected failures=1)

coverage says:

----------------------------------------------------------------------
Ran 2771 tests in 735.579s

OK (expected failures=1)

The report says everything except tests/property.py is 100% covered.

doctest.py says:

----------------------------------------------------------------------
Ran 59 tests in 79.398s

OK

Besides the changes you've suggested, I had to change the name variable of the TestMemoryDecay class to Memory Decay: 0.1, 0.03, -2, 1, <class 'axelrod.strategies.titfortat.TitForTat'>, 15, and change the stochastic property in the classifier from True to False in order for unittest not to throw errors. Is this fine?

I thought the strategy was supposed to be stochastic since it has a random component (memory deletion and alteration).

Copy link
Member

@drvinceknight drvinceknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the change to stochastic: True causes other errors we'll need to understand them :)

classifier = {
'memory_depth' : float('inf'),
'long_run_time' : False,
'stochastic' : False,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strategy is certainly stochastic. This should be True

expected_classifier = {
'memory_depth': float('inf'),
'long_run_time': False,
'stochastic': False,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be need to be changed to True

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed it to True in both test_meta.py and meta.py, and I get this error:

======================================================================
FAIL: test_initialisation (axelrod.tests.strategies.test_meta.TestMemoryDecay)
Test that the player initiates correctly.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/denis/Documents/Axelrod/axelrod/tests/strategies/test_player.py", line 372, in test_initialisation
    self.classifier_test(self.expected_class_classifier)
  File "/home/denis/Documents/Axelrod/axelrod/tests/strategies/test_player.py", line 512, in classifier_test
    self.assertEqual(expected_class_classifier, self.player.classifier)
AssertionError: {'mem[50 chars]ic': False, 'makes_use_of': set(), 'inspects_s[66 chars]alse} != {'mem[50 chars]ic': True, 'makes_use_of': set(), 'inspects_so[65 chars]alse}
  {'inspects_source': False,
   'long_run_time': False,
   'makes_use_of': set(),
   'manipulates_source': False,
   'manipulates_state': False,
   'memory_depth': inf,
-  'stochastic': False}
?                ^^^^

+  'stochastic': True}
?                ^^^


----------------------------------------------------------------------
Ran 2771 tests in 441.809s

FAILED (failures=1, expected failures=1)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes I think I know why that is (the init of the parent class is overwriting it).

Add (to the init):

572         super().__init__(team = [start_strategy])
573         self.classifier["stochastic"] = True

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try it now. Another thing I've noticed is that I used the TestPlayer as the parent class, not the TestMetaPlayer. Could that be the cause of the issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your solution solves the error. Should I stick with it or try changing the parent class to TestMetaPlayer? Or both?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's stick with TestPlayer, this player is a bit different so I think it makes sense :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I've added the code suggested for player.py and am currently running the tests on my machine. I'll report if there are any errors.


class TestMemoryDecay(TestPlayer):

name = "Memory Decay: 0.1, 0.03, -2, 1, <class 'axelrod.strategies.titfortat.TitForTat'>, 15"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of a mouthful but I think it's fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we look at what happens if we modify the axl.Player __repr__ method to be:

  196     def __repr__(self):                                                                                                                         
  197         """The string method for the strategy.                                                                                                  
  198         Appends the `__init__` parameters to the strategy's name."""                                                                            
  199         name = self.name                                                                                                                        
  200         prefix = ': '                                                                                                                           
  201         gen = (value for value in self.init_kwargs.values() if value is not None)                                                               
  202         for value in gen:                                                                                                                       
+ 203             if issubclass(value, Player):                                                                                                       
+ 204                 value = value.name                                                                                                              
  205             name = ''.join([name, prefix, str(value)])                                                                                          
  206             prefix = ', '                                                                                                                       
  207         return name   

(Lines 203, 204 of https://github.com/Axelrod-Python/Axelrod/blob/master/axelrod/player.py, will need a from axelrod.player import Player at the top.)

This way it ensures the default player repr displays the strategy name and not the class...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry that won't work. This does:

  195     def __repr__(self):                                                                                                                         
  196         """The string method for the strategy.                                                                                                  
  197         Appends the `__init__` parameters to the strategy's name."""                                                                            
  198         name = self.name                                                                                                                        
  199         prefix = ': '                                                                                                                           
  200         gen = (value for value in self.init_kwargs.values() if value is not None)                                                               
  201         for value in gen:                                                                                                                       
+ 202             try:                                                                                                                                
+ 203                 if issubclass(value, Player):                                                                                                   
+ 204                     value = value.name                                                                                                          
+ 205             except TypeError:                                                                                                                   
+ 206                 pass                                                                                                                            
  207             name = ''.join([name, prefix, str(value)])                                                                                          
  208             prefix = ', '                                                                                                                       
  209         return name  

…, fixed stochastic classifier for memdecay
@vdeni
Copy link
Contributor Author

vdeni commented Nov 13, 2017

I ran the tests locally on the latest commit and everything seems fine!

@drvinceknight
Copy link
Member

Yup, this looks good to me! Thanks! Let's see what the other core developers have to say (there might be a nicer modification of the Player.__repr__ class perhaps).

@vdeni
Copy link
Contributor Author

vdeni commented Nov 13, 2017

Great! Thank you for the help!

"""
A player utilizes the (default) Tit for Tat strategy for the first (default) 15 turns,
at the same time memorizing the opponent's decisions. After the 15 turns have
passed, the player calculates a 'net cooperation score' (NCS) for his opponent,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use 'its' or 'their' instead of 'his'

at the same time memorizing the opponent's decisions. After the 15 turns have
passed, the player calculates a 'net cooperation score' (NCS) for his opponent,
weighing decisions to Cooperate as (default) 1, and to Defect as (default)
-2. If the opponent's NCS is below 0, the play defects; otherwise,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: 'play' should be 'player'

altered (i.e., a C decision becomes D or vice versa; default probability
is 0.03) or deleted (default probability is 0.1).

It's necessary to specify EXACT name of the starting strategy if changing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for the capitals and should instead read '... specify the exact name ...'


# translates the actions (D and C) to numeric values (loss_value and
# gain_value)
def gain_loss_tr(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you rename this to gain_loss_translate and use a docstring for this method instead of the inline comment?

self.gain_value, self.memory)]

# alters memory entry, i.e. puts C if there's a D and vice versa
def mem_alter(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use a full name for the method memory_alter, alter_memory or flip_memory and a docstring instead of a comment

# gain_value)
def gain_loss_tr(self):
self.gloss_values = [*map(lambda x: self.loss_value if x == D else
self.gain_value, self.memory)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tricky to read - could you expand it to use a few much simpler lines of code rather than fitting all into one line?

self.memory[alter] = self.memory[alter].flip()

# deletes memory entry
def mem_delete(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use a fuller name e.g. memory_delete or delete_memory and a docstring instead of a comment?

is 0.03) or deleted (default probability is 0.1).

It's necessary to specify EXACT name of the starting strategy if changing
the default. Possible strategies can be accessed with the .team attribute.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: this is no longer correct. This argument is now an player class. Perhaps change this whole paragraph to just say something like:

It is possible to pass a different axelrod player class to change the initial player behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed this according to your recommendation :)

if action == D:
self.gloss_values.append(self.loss_value)
else:
self.gloss_values.append(self.gain_value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a suggestion rather than a request but Github doesn't really cope with that idea!

def gain_loss_translate(self):
    """
    Translates the actions (D and C) to numeric values (loss_value and
    gain_value).
    """
    values = {
        C: self.gain_value,
        D: self.loss_value
    }
    self.gloss_values = [values[action] for action in self.memory]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go with this, it's more elegant. Thanks!

@drvinceknight drvinceknight merged commit febeb9d into Axelrod-Python:master Nov 14, 2017
@drvinceknight
Copy link
Member

Awesome! Thanks for the contribution @vdeni! 👍

@vdeni
Copy link
Contributor Author

vdeni commented Nov 14, 2017

Thank you for the help! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants