GithubHelp home page GithubHelp logo

Comments (12)

dashersw avatar dashersw commented on August 13, 2024 1

Hey Aiden, here's a breakdown of the first refactoring actions. After these we'll be able to apply a second layer of refactoring for decreasing coupling and increasing coherence throughout the codebase.

1) Sticking to a given code style

The first refactoring should be on code style consistency. We had already built ESLint into the project, so every new commit should make sure it's consistent with the style. You can activate it by running npm lint and it will pour out a list of errors. ESLint also comes with automatic fixing of a lot of errors, and I made it available as another npm script. If you run npm run lint-fix, it will fix most of the styling issues that can be automatically fixed.

In order to streamline this, we can also build in a commit hook to make sure it will execute npm run lint-fix before anyone commits any code.

Most code editors today also support automatic linting — they can validate and fix your code using ESLint on every save. This is actually pretty useful, and a setting we use in Visual Studio Code all the time.

The biggest surprise element here could be that you are using tabs in your editor. While it's a never-ending debate whether to use tabs or spaces, the JavaScript community is leaning more towards two-character spaces. This is also reflected in the built-in ESLint config. Therefore I would appreciate if you made the switch.

Currently there are 812 problems in the codebase, and most of them are in fact about uninitialized variables. These needs to be cleared out, and the project should pass with 0 errors on an ESLint run.

2) Sticking to ES6/7

This is a multi-part refactoring. First, ES6 brought in const and let, and it's recommended to replace var with a proper use of these. Furthermore, it's good to make use of arrow functions as well. Further constructs like object destructuring, default parameters and rest parameters could also be applied.

3) Removing async library

As I have noted earlier, currently the code is still single-threaded. async library by default doesn't incorporate any parallelism. In fact, the codebase also doesn't need any at this point. Most of the operations are either very straightforward arithmetic operations or are operations on very simple data structures like small arrays and objects. In any case, in a multi-threaded operation, the overhead caused by context switching and object serialization / deserialization would destroy any benefits parallelism would bring.

At this point, it would make sense to remove all of the async calls with regular inline JavaScript.

4) Replacing promises with async/await

The code makes heavy use of promises, and in fact this reduces readability. The idea of promises was to avoid having callback hell — increasing indentation levels in async code — but that's exactly the case here even though the codebase is using promises.

Again I am at a split here. All of the functions are synchronous and it makes sense for them to be synchronous. In the future if the codebase gets 20x the size, this might be a very slight convenience and we may want to turn some functions into microservices — but now with async/await it's extremely straightforward.

So I would suggest to remove all the promises and async/await from the codebase at this point and make everything actually what they are — synchronous —, but it's your call. If you want to keep this approach though, I recommend to replace all the promises with async/await.

5) Multiple returns, early returns

The codebase makes heavy use of nested if/else clauses and reduces readability and maintainability. A careful rewrite can remove most of this by making use of multiple, early returns. Wherever one sees a resolve in the current codebase, one should replace it with return resolve. Simply because of the fact that a resolve in a Promise is already the end of the execution of that function.

This removes the need to use else clauses after resolve, because;

if (a) {
  resolve(b)
}
else {
  resolve(c)
}

is practically the same as;

if (a) {
  return resolve(b)
}

return resolve(c)

Making early returns also saves the reader from the trouble of having to read (and understand) the whole function, when the part of the execution they are interested in already actually is over.

6) Removing duplication

There's a pattern of code duplication throughout the codebase, and this should be eliminated to increase readability and maintainability.

The following code;

if (playerProximity[0] < 10 && playerProximity[1] < 10) {
  possibleActions = populatePossibleActions(possibleActions, 0, 20, 30, 20, 0, 0, 20, 0, 0, 0, 10)
  resolve(possibleActions)
} else if (player.skill.shooting > 85) {
  possibleActions = populatePossibleActions(possibleActions, 60, 10, 10, 0, 0, 0, 20, 0, 0, 0, 0)
  resolve(possibleActions)
} else if (player.position === 'LM' || player.position === 'CM' || player.position === 'RM') {
  possibleActions = populatePossibleActions(possibleActions, 0, 10, 10, 10, 0, 0, 0, 30, 40, 0, 0)
  resolve(possibleActions)
} else if (player.position === 'ST') {
  possibleActions = populatePossibleActions(possibleActions, 0, 0, 0, 0, 0, 0, 0, 50, 50, 0, 0)
  resolve(possibleActions)
} else {
  possibleActions = populatePossibleActions(possibleActions, 10, 10, 10, 10, 0, 0, 0, 30, 20, 0, 10)
  resolve(possibleActions)
}

can be written as;

let parameters = []

if (playerProximity[0] < 10 && playerProximity[1] < 10) {
  parameters = [0, 20, 30, 20, 0, 0, 20, 0, 0, 0, 10]
} else if (player.skill.shooting > 85) {
  parameters = [60, 10, 10, 0, 0, 0, 20, 0, 0, 0, 0]
} else if (['LM', 'CM', 'RM'].includes(player.position)) {
  parameters = [0, 10, 10, 10, 0, 0, 0, 30, 40, 0, 0]
} else if (player.position === 'ST') {
  parameters = [0, 0, 0, 0, 0, 0, 0, 50, 50, 0, 0]
} else {
  parameters = [10, 10, 10, 10, 0, 0, 0, 30, 20, 0, 10]
}

resolve(populatePossibleActions(possibleActions, ...parameters))

This is not only a 36% reduction in character count, but it's also much easier to read. It's also a lot easier to refactor when you want to change how populatePossibleActions work, for example.

Conclusion

These are the first layers of refactoring that the codebase needs. The first focus should be on increasing style consistency, reducing duplication and thereby increasing readability and maintainability.

Then we will be able to apply certain design patterns like strategy, factory, command, chain of responsibility, and further object oriented programming patterns like polymorphism and other patterns like composition.

Hope this helps.

from footballsimulationengine.

dashersw avatar dashersw commented on August 13, 2024 1

Here's another example. The following;

if (thatTeamPlayer) {
  if (thatTeamPlayer.startPOS[0] === thisPos[0] && thatTeamPlayer.startPOS[1] === thisPos[1]) {
    if (!deflectionPlayer) {
      if (thisPos[2] < thatTeamPlayer.skill.jumping && thisPos[2] < 49) {
        deflectionPlayer = thatTeamPlayer
        deflectionPosition = thisPos
        deflectionTeam = opposition.name
        thisPosCallback()
      } else {
        thisPosCallback()
      }
    } else {
      thisPosCallback()
    }
  } else {
    thisPosCallback()
  }
} else {
  thisPosCallback()
}

can be rewritten as;

if (!thatTeamPlayer) return thisPosCallback()
if (!(thatTeamPlayer.startPOS[0] === thisPos[0] && thatTeamPlayer.startPOS[1] === thisPos[1])) return thisPosCallback()
if (deflectionPlayer) return thisPosCallback()
if (thisPos[2] >= thatTeamPlayer.skill.jumping || thisPos[2] >= 49) return thisPosCallback()

deflectionPlayer = thatTeamPlayer
deflectionPosition = thisPos
deflectionTeam = opposition.name
thisPosCallback()

from footballsimulationengine.

GallagherAiden avatar GallagherAiden commented on August 13, 2024

@dashersw - could you write out a list of refactoring tasks and I'll start actioning them

from footballsimulationengine.

GallagherAiden avatar GallagherAiden commented on August 13, 2024

This is exactly what I was looking for, great depth to the comments and the detail means I can start the refactoring as my next task. Thanks! :)

from footballsimulationengine.

GallagherAiden avatar GallagherAiden commented on August 13, 2024

@dashersw, I've completed the above refactoring changes. (Stage 1). The only remaining lint issues are as follows:

footballSimulationEngine/lib/playerMovement.js
   60:16  error  Unexpected `await` inside a loop  no-await-in-loop
   81:24  error  Unexpected `await` inside a loop  no-await-in-loop
   93:15  error  Unexpected `await` inside a loop  no-await-in-loop
  110:13  error  Unexpected `await` inside a loop  no-await-in-loop
  119:22  error  Unexpected `await` inside a loop  no-await-in-loop
  131:13  error  Unexpected `await` inside a loop  no-await-in-loop
  148:11  error  Unexpected `await` inside a loop  no-await-in-loop
  170:29  error  Unexpected `await` inside a loop  no-await-in-loop
  179:29  error  Unexpected `await` inside a loop  no-await-in-loop
  189:29  error  Unexpected `await` inside a loop  no-await-in-loop
  198:29  error  Unexpected `await` inside a loop  no-await-in-loop

Any thoughts about changing the code to facilitate the lint errors?

from footballsimulationengine.

dashersw avatar dashersw commented on August 13, 2024

@GallagherAiden I've went through your changes, great work as always! How about removing async/await from these pieces in the code? I still don't believe this engine will ever require an async operation.

from footballsimulationengine.

GallagherAiden avatar GallagherAiden commented on August 13, 2024

It requires async/await in the decideMovement function unfortunately. I think I'll have to rework the whole function to remove them. I'll keep playing because I think I should be able to sync the whole simulator.

Feel free to add more refactoring tips and I'll work them in at the same time

from footballsimulationengine.

dashersw avatar dashersw commented on August 13, 2024

Two examples;

For async/await you can make use of the try / catch blocks;

  let team1 = await common.readFile(t1)
    .catch(function(err) {
      throw err.stack
    })
  let team2 = await common.readFile(t2)
    .catch(function(err) {
      throw err.stack
    })
  let pitch = await common.readFile(p)
    .catch(function(err) {
      throw err.stack
    })
  let matchSetup = engine.initiateGame(team1, team2, pitch)
    .catch(function(err) {
      throw err.stack
    })
  return matchSetup

can be rewritten as;

try {
  let team1 = await common.readFile(t1)
  let team2 = await common.readFile(t2)
  let pitch = await common.readFile(p)
  let matchSetup = engine.initiateGame(team1, team2, pitch)
  return matchSetup
} catch (e) {
  throw e
}

Also,

    const error = 'Please provide two teams and a pitch JSON'
    throw error

can be reduced down to

throw new Error('Please provide two teams and a pitch JSON')

new Error is important because it's what gives you the stack.

from footballsimulationengine.

dashersw avatar dashersw commented on August 13, 2024
    if (direction === 'wait') {
      newPosition[0] = position[0] + common.getRandomNumber(0, (power / 2))
      newPosition[1] = position[1] + common.getRandomNumber(0, (power / 2))
    } else if (direction === 'east') {
      newPosition[0] = position[0] + common.getRandomNumber((power / 2), power)
      newPosition[1] = position[1] + common.getRandomNumber(-20, 20)
    } else if (direction === 'west') {
      newPosition[0] = common.getRandomNumber(position[0] - 120, position[0])
      newPosition[1] = common.getRandomNumber(position[1] - 30, position[1] + 30)
    } else if (direction === 'south') {
      newPosition[0] = position[0] + common.getRandomNumber(-20, 20)
      newPosition[1] = position[1] + common.getRandomNumber((power / 2), power)
    } else if (direction === 'southeast') {
      newPosition[0] = position[0] + common.getRandomNumber(0, (power / 2))
      newPosition[1] = position[1] + common.getRandomNumber((power / 2), power)
    } else if (direction === 'southwest') {
      newPosition[0] = position[0] + common.getRandomNumber(-(power / 2), 0)
      newPosition[1] = position[1] + common.getRandomNumber((power / 2), power)
    }

This part could also benefit from reduction — again mostly what changes here is the parameters to common.getRandomNumber

from footballsimulationengine.

dashersw avatar dashersw commented on August 13, 2024

There's one more emerging pattern in the catch blocks, like

catch (error) {
  throw new Error(error)
}

If you don't do any other operation in the catch block than to re-throw the error, you don't need a try-catch block at all, since the behavior is exactly the same as not having it.

from footballsimulationengine.

GallagherAiden avatar GallagherAiden commented on August 13, 2024

hi @dashersw, sorry for the delay in responses, I had some time away from changing the code. There seems to be more traffic so making wider changes for improvements in two phases. 2.2.0 and eventually 3.0.0 (where v3 will be high impact changes to the general way the game works).

I think I've addressed the catch block issues as discussed in your last comment. The only part I couldn't figure out how to properly improve is #6 (comment)

Any thoughts? Any other changes I can make to modernise the code?

from footballsimulationengine.

GallagherAiden avatar GallagherAiden commented on August 13, 2024

Closing for now, but any other changes you think will help with modernisation please let me know

from footballsimulationengine.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.