I'm making a simple web app that helps people organize teams by skill level, you enter the player name, skill level ( 0 - 10 ), and the player's avatar.
For example, you add 3 players in team1 and 4 in team2 and then you get them displayed in a way where the sum of skill value of team1 is equal to team2.
I ended up making the app create a new player instance each time the user presses the add button and then push that instance to an array. just like you see here:
class Player {
constructor(name, image, skill) {
this.name = name;
this.image = image;
this.skill = skill;
}
}
const createPlayer = (name, image, skill) => {
// creates a player with given data and pushes it to the playersList array
const playerIns = new Player(name, image, skill);
playersList.push(playerIns);
};
Then the app maps through the playersList array and returns a list element for each player just like a basic todo-app.
I need help with implementing the logic that organizes the players correctly as described above.
I can't really think of a way to make this work so I'd appreciate any help.
Related
I have hopefully explained my problem below by writing a short simplified example with the same conceptual question as my actual larger application. My question is centred around ways in which I can de-couple classes from each other. Whilst I accept no application can be free of coupling completely and modules have to interact with each other, I am trying to establish best ways in which I can keep each class as a separate unit so that if changes are made in one class it won't affect another. I want to keep changes simple.
Below I have outlined an example to the regular problem I face. As an example I have centred this example around a simple game where we can add a player named 'Kevin' everytime we press enter. Then everytime we click a button we add a score of 10 to every players running score and print the updated score of the current player to the screen.
Like most things I have used the MVC pattern to achieve this small example game. The Model has a Player class with data and methods. One of the methods adds 10 to the players score.
I also have a PlayerCircle class in the Model which returns things like the current player as-well as changing which players turn it is.
Lastly in the Model I have a Game Class which imports PlayerCircle. PlayerCircle is then a property of the Game class. Its the only property at the moment but as the game grows, further classes will be added e.g. Dice class
The controller module calls methods from the Model and passes things into view much like how an MVC pattern should work.
This is my PlayerCircle class
export class PlayersCircle {
constructor() {
this.players = [];
this.playersTurn = 0;
}
addPlayer(name) {
const player = new Player(name);
this.players.push(player);
}
changePlayer() {
this.playersTurn++;
}
getPlayer() {
return this.players[this.playersTurn];
}
increaseAllScores() {
this.players.forEach((player) => {
player.add10();
});
}
getAllPlayers() {
return this.players;
}
}
This is my player class
export class Player {
constructor(name) {
this.name = name;
this.score = 0;
}
add10() {
this.score += 10;
}
getScore() {
return this.score;
}
}
This is my game class
import { PlayersCircle } from "./PlayersCircle.js";
export class Game {
constructor() {
this.playersCircle = new PlayersCircle();
}
}
Now I have set up the classes for this very simple game, I now write this very simple function from the controller which adds a player to the game.
import { Game } from "./model/Game.js";
import { viewShowScore } from "./view/viewShowScore.js";
const game = new Game();
const addPlayer = () => {
game.playersCircle.addPlayer("kevin");
};
document.querySelector("body").addEventListener("keypress", addPlayer);
Ok so now I want to add 10 points to all the players in the game. Lets say I have fired the keypress 5 times and have 5 players. This is where I often struggle to establish the best Architectural Plan going forward with an Application.
The two options I normally have are
Call the add10 method on each player from the playerCircle class :-
This will enable me to use my controller just to call methods which change data or get data to the render the state into the view. This keeps my controller short and concise.However it means that the PlayersCircle class is now tightly coupled with the Player Class as one of the PlayerCircle methods uses a method of the Player class ( the add10 method) which means any changes in the Player Class on the add10 method will now break the PlayerCircle class.
My function from the controller to add 10 point to each player looks like this:
const increaseAllScores = () => {
game.playersCircle.increaseAllScores();
const currentPlayer = game.playersCircle.getPlayer();
viewShowScore(currentPlayer.getScore()); // function from view prints 10
};
document.querySelector("button").addEventListener("click", increaseAllScores);
Return all the players to the controller and then from inside the controller module run a loop on the returned array which holds the players-:
This will enable my PlayersCircle class to be completely independent from the Player class and just control the state of who'es turn it is, return all the players at once or separately. It won't have to concern itself with the inner methods of the Player class. The Player class as the game grows will have many methods. If I want to call a method on more than one player at a time then (like the add10 method) then the playersCircle class is very tightly coupled and changes from both classes will almost have to simultaneously occur.
However this would mean that my controller module isn't now just calling methods from the Model and passing results into the view but is now containing loops. Whilst I prefer this way whenever I see examples of MVC the controller is full of simple methods calls from the Model to the view.
The function inside the controller now looks like this:
const increaseAllScores = () => {
const allPlayers = game.playersCircle.getAllPlayers();
allPlayers.forEach((player) => {
player.add10();
});
const currentPlayer = game.playersCircle.getPlayer();
viewShowScore(currentPlayer.getScore()); // function from view prints 10
};
Both Pieces of code give the same result. However for long term code management when the game increases in complexity and may have new features in the future, which one is better than the other and why - option 1 which calls the add10 from the playersCircle targeting every player or option 2 which runs a loop from the controller targeting every player which is returned from the playerCircle class ?
I am finishing a weather app in TypeScipt and React, and the last feature I need to include is the ability to add a queried location to a favorites list. This is the second-page "favorites".
Using a button on the card displayed when a location is queried sets the favorite state to true.
<button className="add-favorite btn" onClick={() => addToFavoritesHandler()}>Add to Favorites</button>
Though, when searching for a new location, the array gets mutated. And even when attempting to add a new location to your favorites, it only shows that one location, no previous locations.
This is where if there is a favorited location is mapped out.
{favorites.length === 0 ? (
<p> Add locations to favorites! </p>
) : (
favorite &&
arr?.map((location, id) => {
return (
I simply want to be able to add multiple locations to the favorites list when pressing the button.
This function handles the pushing to the array and is drilled down to the button on the queried location.
const [favorite, setFavorite] = useState(true);
let favorites: WeatherType[] = [];
// Easier readability
let location = weatherData;
// Pushing the favorite location into the favorite array
const addToFavoritesHandler = () => {
setFavorite(true);
if (location && favorite && !favorites.find((o) => o === location)) {
favorites.push(location);
}
return favorites;
};
The GitHub Repo -
https://github.com/carbondesigned/granular-ai-assignement-weather-app
Thank you.
I quickly read the code inside the repository and I've noticed that the favorites array isn't keep inside a useState, I think that this is the problem.
In this way that array will be recreated on every render.
If you change with:
const [favorites, setFavorites] = useState<WeatherType[]>([]);
and, inside the function addToFavoritesHandler you update favorites with:
setFavorites(favorites)
you should make this works.
I'm working on a simple drawing example, where the user can draw straight lines by clicking and dragging.
One of the features I want to implement is the ability to delete a line by double clicking on it, the problem I have is removing such line from the state array, I have a bare bones example here.
Unfortunately, when I try to console.log() the state array allPaths the app freezes but that doesn't happen on my local setup.
For example:
User draw 4 lines
On double click on the first line the console logs, its ID and empty state array[]
On double click the fourth line the log displays line ID and state array of length 3, that contains the first 3 lines, but it does not show the entire array... ?
On my local setup, when I console.log(allPaths) on doubleclick I get the allPaths state before that particular lines has been created, it doesn't show all entries, but when I log in the render method it shows all entries fine.
I have no idea what I'm doing wrong ...
In every render cycle you create new layers, instead use a ref to keep track of the layer, preventing the creation of new ones.
Make the following changes:
const markersLayer = useRef(new Paper.Group());
const floorplanLayer = useRef(new Paper.Group());
const trashAllPaths = () => {
setAllPaths([]);
};
const removePath = (id) => {
setAllPaths((paths) => paths.filter((p) => p.id !== id));
};
const savePath = (path) => {
setAllPaths((paths) => [...paths, path]);
};
markersLayer.current.addChildren([ghostLine]);
useEffect(() => {
floorplanLayer.current.removeChildren();
floorplanLayer.current.addChildren(allPaths);
}, [allPaths, floorplanLayer]);
The reason it freezes is because Path is a heavy object. For what I can see, you have allPaths, which is an array of objects that contain the property id. I can also see that on a path double click you get the path id.
So, in order to delete a line, I assume this would work:
const removeLine = (id) => {
const allPathsCopy = [...allPaths]
setAllPaths(allPathsCopy.filter(path => path.id !== id));
}
I'm writing a memory game in javascript. I have made a web-component for the cards, <memory-card> and a web-component to contain the cards and handle the game state <memory-game>. The <memory-card> class contains its image path for when its turned over, the default image to display as the back of the card, its turned state and an onclick function to handle switching between the states and the images.
The <memory-game> class has a setter that receives an array of images to generate <memory-cards> from. What would be the best method to handle updating the game state in the <memory-game> class? Should I attach an additional event listener to the <memory-card> elements there or is there a better way to solve it? I would like the <memory-card> elements to only handle their own functionality as they do now, ie changing images depending on state when clicked.
memory-game.js
class memoryGame extends HTMLElement {
constructor () {
super()
this.root = this.attachShadow({ mode: 'open' })
this.cards = []
this.turnedCards = 0
}
flipCard () {
if (this.turnedCards < 2) {
this.turnedCards++
} else {
this.turnedCards = 0
this.cards.forEach(card => {
card.flipCard(true)
})
}
}
set images (paths) {
paths.forEach(path => {
const card = document.createElement('memory-card')
card.image = path
this.cards.push(card)
})
}
connectedCallback () {
this.cards.forEach(card => {
this.root.append(card)
})
}
}
customElements.define('memory-game', memoryGame)
memory-card.js
class memoryCard extends HTMLElement {
constructor () {
super()
this.root = this.attachShadow({ mode: 'open' })
// set default states
this.turned = false
this.path = 'image/0.png'
this.root.innerHTML = `<img src="${this.path}"/>`
this.img = this.root.querySelector('img')
}
set image (path) {
this.path = path
}
flipCard (turnToBack = false) {
if (this.turned || turnToBack) {
this.turned = false
this.img.setAttribute('src', 'image/0.png')
} else {
this.turned = true
this.img.setAttribute('src', this.path)
}
}
connectedCallback () {
this.addEventListener('click', this.flipCard())
}
}
customElements.define('memory-card', memoryCard)
implementing the custom event after Supersharp's answer
memory-card.js (extract)
connectedCallback () {
this.addEventListener('click', (e) => {
this.flipCard()
const event = new CustomEvent('flippedCard')
this.dispatchEvent(event)
})
}
memory-game.js (extract)
set images (paths) {
paths.forEach(path => {
const card = document.createElement('memory-card')
card.addEventListener('flippedCard', this.flipCard.bind(this))
card.image = path
this.cards.push(card)
})
}
In the <memory-card>:
Create with CustomEvent() and dispatch a custom event with dispatchEvent()
In the <memory-game>:
Listen to your custom event with addEventListener()
Because the cards are nested in the game, the event will bubble naturally to the container.
This way the 2 custom elements will stay loosley coupled.
Supersharps answer is not 100% correct.
click events bubble up the DOM,
but CustomEvents (inside shadowDOM) do not
Why firing a defined event with dispatchEvent doesn't obey the bubbling behavior of events?
So you have to add the bubbles:true yourself:
[yoursender].dispatchEvent(new CustomEvent([youreventName], {
bubbles: true,
detail: [yourdata]
}));
more: https://javascript.info/dispatch-events
note: detail can be a function: How to communicate between Web Components (native UI)?
For an Eventbased programming challenge
this.cards.forEach(card => {
card.flipCard(true)
})
First of all that this.cards is not required, as all cards are available in [...this.children]
!! Remember, in JavaScript Objects are passed by reference, so your this.cards is pointing to the exact same DOM children
You have a dependency here,
the Game needs to know about the .flipCard method in Card.
► Make your Memory Game send ONE Event which is received by EVERY card
hint: every card needs to 'listen' at Game DOM level to receive a bubbling Event
in my code that whole loop is:
game.emit('allCards','close');
Cards are responsible to listen for the correct EventListener
(attached to card.parentNode)
That way it does not matter how many (or What ever) cards there are in your game
The DOM is your data-structure
If your Game no longer cares about how many or what DOM children it has,
and it doesn't do any bookkeeping of elements it already has,
shuffling becomes a piece of cake:
shuffle() {
console.log('► Shuffle DOM children');
let game = this,
cards = [...game.children],//create Array from a NodeList
idx = cards.length;
while (idx--) game.insertBefore(rand(cards), rand(cards));//swap 2 random DOM elements
}
My global rand function, producing a random value from an Array OR a number
rand = x => Array.isArray(x) ? x[rand(x.length)] : 0 | x * Math.random(),
Extra challenge
If you get your Event based programming right,
then creating a Memory Game with three matching cards is another piece of cake
.. or 4 ... or N matching cards
It would be very helpful to see some of your existing code to know what you have tried. But without it you ca do what #Supersharp has proposed, or you can have the <memory-game> class handle all events.
If you go this way then your code for <memory-card> would listen for click events on the entire field. It would check to see if you clicked on a card that is still face down and, if so, tell the card to flip. (Either through setting a property or an attribute, or through calling a function on the <memory-card> element.)
All of the rest of the logic would exist in the <memory-game> class to determine if the two selected cards are the same and assign points, etc.
If you want the cards to handle the click event then you would have that code generate a new CustomEvent to indicate that the card had flipped. Probably including the coordinates of the card within the grid and the type of card that is being flipped.
The <memory-game> class would then listen for the flipped event and act upon that information.
However you do this isn't really a problem. It is just how you want to code it and how tied together you want the code. If you never plan to use this code in any other games, then it does not matter as much.
I have multiple video's on a page. When I click the play button on one of the video's (app.js) the data is retrieved and myVideo.init is called (video.js). This works but the problem is that the data of the previous video instance is always overwritten, I would somehow like to keep this intact because it contains information on when the player was started/paused etc. In theory it would need to be possible to:
Start video A
Pause video A
Start video B
Start video A again from the same coordinates it was paused, but this data was lost when I started video B.
I hope my question is clear enough. Help would be much appreciated.
// video.js
export default new (Base.extend({
myVideo: null,
init(clip) {
this.myVideo = new videoObject({}, 'https://localhost.nl?video123');
this.myVideo.setProperties()
}
}))();
// app.js
export default Base.extend({
state: {
isFirstTimePlaying: []
},
util.getJson({url: 'test.json'}).done((data) => {
let id = this.getPlayerId();
if (!this.state.isFirstTimePlaying.includes(id)) {
myVideo.init(data, id);
this.state.isFirstTimePlaying.push(id);
}
});
}))();